├── src ├── main │ ├── aspect │ │ └── .gitkeep │ ├── resources │ │ ├── META-INF │ │ │ ├── services │ │ │ │ ├── javax.xml.xpath.XPathFactory │ │ │ │ └── javax.xml.transform.TransformerFactory │ │ │ └── MANIFEST.MF │ │ ├── images │ │ │ ├── logo.png │ │ │ └── logo.svg │ │ ├── io │ │ │ └── jare │ │ │ │ └── tk │ │ │ │ └── error.html.vm │ │ ├── log4j.properties │ │ └── xsl │ │ │ ├── domains.xsl │ │ │ ├── layout.xsl │ │ │ └── index.xsl │ ├── java │ │ └── io │ │ │ └── jare │ │ │ ├── package-info.java │ │ │ ├── tk │ │ │ ├── package-info.java │ │ │ ├── TkRefresh.java │ │ │ ├── RqUser.java │ │ │ ├── TkAdd.java │ │ │ ├── TkIndex.java │ │ │ ├── TkDelete.java │ │ │ ├── Destination.java │ │ │ ├── TkDomains.java │ │ │ ├── TkAppAuth.java │ │ │ ├── TkInvalidate.java │ │ │ ├── TkAppFallback.java │ │ │ ├── RsPage.java │ │ │ ├── TkRelay.java │ │ │ └── TkApp.java │ │ │ ├── fake │ │ │ ├── package-info.java │ │ │ ├── FkUser.java │ │ │ ├── FkDomain.java │ │ │ ├── FkUsage.java │ │ │ └── FkBase.java │ │ │ ├── cached │ │ │ ├── package-info.java │ │ │ ├── CdDomain.java │ │ │ ├── CdUsage.java │ │ │ └── CdBase.java │ │ │ ├── dynamo │ │ │ ├── package-info.java │ │ │ ├── DyDomain.java │ │ │ ├── Dynamo.java │ │ │ ├── DyBase.java │ │ │ ├── DyUser.java │ │ │ └── DyUsage.java │ │ │ ├── model │ │ │ ├── package-info.java │ │ │ ├── User.java │ │ │ ├── Base.java │ │ │ ├── Domain.java │ │ │ └── Usage.java │ │ │ ├── smarts │ │ │ ├── package-info.java │ │ │ └── SafeUser.java │ │ │ ├── Entrance.java │ │ │ └── Logs.java │ └── scss │ │ └── main.scss └── test │ ├── aspect │ └── .gitkeep │ ├── java │ └── io │ │ └── jare │ │ ├── package-info.java │ │ ├── tk │ │ ├── package-info.java │ │ ├── RqUserTest.java │ │ ├── TkInvalidateITCase.java │ │ ├── PingingTest.java │ │ ├── DestinationTest.java │ │ ├── TkIndexTest.java │ │ ├── TkAppTest.java │ │ └── TkRelayTest.java │ │ ├── cached │ │ ├── package-info.java │ │ └── CdUsageTest.java │ │ ├── dynamo │ │ ├── package-info.java │ │ ├── DyUsageITCase.java │ │ ├── DyDomainITCase.java │ │ ├── DyBaseITCase.java │ │ ├── DyUserITCase.java │ │ └── DyUsageTest.java │ │ ├── smarts │ │ ├── package-info.java │ │ └── SafeUserTest.java │ │ └── LogsTest.java │ ├── resources │ ├── META-INF │ │ └── MANIFEST.MF │ ├── log4j.properties │ └── io │ │ └── jare │ │ └── test │ └── dynamodb │ └── domains.json ├── .gitignore ├── renovate.json ├── system.properties ├── .pdd ├── Procfile ├── .0pdd.yml ├── .gitattributes ├── deploy.sh ├── .github └── workflows │ ├── reuse.yml │ ├── typos.yml │ ├── xcop.yml │ ├── pdd.yml │ ├── yamllint.yml │ ├── copyrights.yml │ ├── shellcheck.yml │ ├── markdown-lint.yml │ ├── bashate.yml │ ├── actionlint.yml │ ├── codecov.yml │ └── mvn.yml ├── REUSE.toml ├── LICENSE.txt ├── LICENSES └── MIT.txt ├── .rultor.yml ├── README.md ├── nginx.conf.sigil └── pom.xml /src/main/aspect/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/aspect/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | *.iml 4 | *.log 5 | ajcore.* 6 | node_modules/ 7 | target/ 8 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/javax.xml.xpath.XPathFactory: -------------------------------------------------------------------------------- 1 | net.sf.saxon.xpath.XPathFactoryImpl 2 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/javax.xml.transform.TransformerFactory: -------------------------------------------------------------------------------- 1 | net.sf.saxon.TransformerFactoryImpl 2 | -------------------------------------------------------------------------------- /src/main/resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yegor256/jare/HEAD/src/main/resources/images/logo.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | 4 | java.runtime.version=11 5 | -------------------------------------------------------------------------------- /.pdd: -------------------------------------------------------------------------------- 1 | --source=. 2 | --verbose 3 | --exclude target/**/* 4 | --exclude src/main/resources/images/**/* 5 | --rule min-words:20 6 | --rule min-estimate:15 7 | --rule max-estimate:90 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -Dfile.encoding=UTF-8 -Xmx400m -XX:MaxPermSize=64m -XX:+UnlockExperimentalVMOptions -cp target/jare.jar:target/deps/* io.jare.Entrance --port=${PORT} --threads=50 --max-latency=45000 2 | -------------------------------------------------------------------------------- /src/main/java/io/jare/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Jare. 8 | * 9 | * @since 1.0 10 | */ 11 | package io.jare; 12 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Takes. 8 | * 9 | * @since 1.0 10 | */ 11 | package io.jare.tk; 12 | -------------------------------------------------------------------------------- /src/test/java/io/jare/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Jare, tests. 8 | * 9 | * @since 0.7 10 | */ 11 | package io.jare; 12 | -------------------------------------------------------------------------------- /.0pdd.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | errors: 5 | - yegor256@gmail.com 6 | # alerts: 7 | # github: 8 | # - yegor256 9 | 10 | tags: 11 | - pdd 12 | - bug 13 | -------------------------------------------------------------------------------- /src/main/java/io/jare/fake/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Fakes. 8 | * 9 | * @since 1.0 10 | */ 11 | package io.jare.fake; 12 | -------------------------------------------------------------------------------- /src/test/java/io/jare/tk/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Takes, tests. 8 | * 9 | * @since 1.0 10 | */ 11 | package io.jare.tk; 12 | -------------------------------------------------------------------------------- /src/main/java/io/jare/cached/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Cached. 8 | * 9 | * @since 0.7 10 | */ 11 | package io.jare.cached; 12 | -------------------------------------------------------------------------------- /src/main/java/io/jare/dynamo/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Dynamo. 8 | * 9 | * @since 1.0 10 | */ 11 | package io.jare.dynamo; 12 | -------------------------------------------------------------------------------- /src/main/java/io/jare/model/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * The model. 8 | * 9 | * @since 0.1 10 | */ 11 | package io.jare.model; 12 | -------------------------------------------------------------------------------- /src/main/java/io/jare/smarts/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Smarts. 8 | * 9 | * @since 1.0 10 | */ 11 | package io.jare.smarts; 12 | -------------------------------------------------------------------------------- /src/test/java/io/jare/cached/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Cached, tests. 8 | * 9 | * @since 0.7 10 | */ 11 | package io.jare.cached; 12 | -------------------------------------------------------------------------------- /src/test/java/io/jare/dynamo/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Dynamo, tests. 8 | * 9 | * @since 1.0 10 | */ 11 | package io.jare.dynamo; 12 | -------------------------------------------------------------------------------- /src/test/java/io/jare/smarts/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /** 7 | * Smarts, tests. 8 | * 9 | * @since 1.0 10 | */ 11 | package io.jare.smarts; 12 | -------------------------------------------------------------------------------- /src/main/resources/io/jare/tk/error.html.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Internal application error 5 | 6 | 7 |
${err}
8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Check out all text files in UNIX format, with LF as end of line 2 | # Don't change this file. If you have any ideas about it, please 3 | # submit a separate issue about it and we'll discuss. 4 | 5 | * text=auto eol=lf 6 | *.java ident 7 | *.xml ident 8 | *.png binary 9 | -------------------------------------------------------------------------------- /src/main/scss/main.scss: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | // SPDX-License-Identifier: MIT 3 | 4 | .logo { 5 | height: 75px; 6 | } 7 | 8 | .flash { 9 | &.SEVERE { color: red; } 10 | 11 | &.INFO { color: green; } 12 | 13 | &.WARNING { color: orange; } 14 | } 15 | 16 | footer li { 17 | font-size: 80%; 18 | font-family: monospace; 19 | } 20 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 4 | # SPDX-License-Identifier: MIT 5 | 6 | set -e -o pipefail 7 | 8 | cd "$(dirname "$0")" 9 | cp /code/home/assets/jare/settings.xml . 10 | git add settings.xml 11 | git commit -m 'settings.xml for heroku' 12 | trap 'git reset HEAD~1 && rm settings.xml' EXIT 13 | git push heroku master -f 14 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Jare-Revision: a1b2c3e 2 | Jare-Version: 1.0 3 | Jare-Date: ${timestamp} 4 | Jare-DynamoKey: ${failsafe.dynamo.key} 5 | Jare-DynamoSecret: ${failsafe.dynamo.secret} 6 | Jare-S3Key: - 7 | Jare-S3Secret: - 8 | Jare-CloudFrontKey: - 9 | Jare-CloudFrontSecret: - 10 | Jare-GithubId: ${failsafe.github.id} 11 | Jare-GithubSecret: ${failsafe.github.secret} 12 | Jare-SecurityKey: 0123456701234567 13 | Jare-SentryDsn: test 14 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | 4 | log4j.rootLogger=WARN, CONSOLE 5 | 6 | log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 7 | log4j.appender.CONSOLE.layout=com.jcabi.log.MulticolorLayout 8 | log4j.appender.CONSOLE.layout.ConversionPattern=[%color{%p}] %c: %m%n 9 | 10 | log4j.logger.io.jare=INFO 11 | log4j.logger.com.jcabi.dynamo=INFO 12 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: reuse 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | reuse: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: fsfe/reuse-action@v5 20 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: typos 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | typos: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: crate-ci/typos@v1.32.0 20 | -------------------------------------------------------------------------------- /.github/workflows/xcop.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: xcop 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | xcop: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: g4s8/xcop-action@master 20 | -------------------------------------------------------------------------------- /.github/workflows/pdd.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: pdd 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | pdd: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: volodya-lombrozo/pdd-action@master 20 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Jare-Revision: BUILD 2 | Jare-Version: ${project.version} 3 | Jare-Date: ${timestamp} 4 | Jare-DynamoKey: ${dynamo.key} 5 | Jare-DynamoSecret: ${dynamo.secret} 6 | Jare-S3Key: ${s3.key} 7 | Jare-S3Secret: ${s3.secret} 8 | Jare-CloudFrontKey: ${cloudfront.key} 9 | Jare-CloudFrontSecret: ${cloudfront.secret} 10 | Jare-GithubId: ${github.id} 11 | Jare-GithubSecret: ${github.secret} 12 | Jare-SecurityKey: ${security.key} 13 | Jare-SentryDsn: ${sentry.dsn} 14 | -------------------------------------------------------------------------------- /.github/workflows/yamllint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: yamllint 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | yamllint: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: ibiqlik/action-yamllint@v3 20 | -------------------------------------------------------------------------------- /.github/workflows/copyrights.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: copyrights 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | copyrights: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: yegor256/copyrights-action@0.0.8 20 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: shellcheck 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | shellcheck: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: ludeeus/action-shellcheck@master 20 | -------------------------------------------------------------------------------- /.github/workflows/markdown-lint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: markdown-lint 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | markdown-lint: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: DavidAnson/markdownlint-cli2-action@v20.0.0 20 | -------------------------------------------------------------------------------- /src/main/java/io/jare/fake/FkUser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.fake; 6 | 7 | import io.jare.model.Domain; 8 | import io.jare.model.User; 9 | import java.util.Collections; 10 | 11 | /** 12 | * Fake user. 13 | * 14 | * @since 1.0 15 | */ 16 | public final class FkUser implements User { 17 | 18 | @Override 19 | public Iterable mine() { 20 | return Collections.emptyList(); 21 | } 22 | 23 | @Override 24 | public void add(final String name) { 25 | // nothing 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/jare/model/User.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.model; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * User. 11 | * 12 | * @since 1.0 13 | */ 14 | public interface User { 15 | 16 | /** 17 | * All my domains. 18 | * @return All domains 19 | */ 20 | Iterable mine(); 21 | 22 | /** 23 | * Add a domain. 24 | * @param name The name of the domain 25 | * @throws IOException If fails 26 | */ 27 | void add(String name) throws IOException; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/bashate.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: bashate 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | bashate: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: 3.11 22 | - run: pip install bashate 23 | - run: | 24 | readarray -t files < <(find . -name '*.sh') 25 | bashate -i E006,E003 "${files[@]}" 26 | -------------------------------------------------------------------------------- /src/main/java/io/jare/model/Base.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.model; 6 | 7 | /** 8 | * Base. 9 | * 10 | * @since 1.0 11 | */ 12 | public interface Base { 13 | 14 | /** 15 | * Get user by GitHub handle. 16 | * @param name GitHub name of the user 17 | * @return The user 18 | */ 19 | User user(String name); 20 | 21 | /** 22 | * Find domain by hostname. 23 | * @param name The name 24 | * @return The domain 25 | */ 26 | Iterable domain(String name); 27 | 28 | /** 29 | * All domains. 30 | * @return Full list of all domains 31 | */ 32 | Iterable all(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | 4 | log4j.rootLogger=WARN, SYSLOG, CONSOLE 5 | 6 | log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 7 | log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.CONSOLE.layout.ConversionPattern=BUILD %c: %m%n 9 | 10 | log4j.appender.SYSLOG=org.apache.log4j.net.SyslogAppender 11 | log4j.appender.SYSLOG.facility=user 12 | log4j.appender.SYSLOG.facilityPrinting=true 13 | log4j.appender.SYSLOG.header=true 14 | log4j.appender.SYSLOG.syslogHost=${syslogHost} 15 | log4j.appender.SYSLOG.layout=org.apache.log4j.PatternLayout 16 | log4j.appender.SYSLOG.layout.ConversionPattern=[%p] BUILD %c: %m%n 17 | 18 | log4j.logger.io.jare=INFO 19 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: actionlint 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | actionlint: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Download actionlint 20 | id: get_actionlint 21 | run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 22 | shell: bash 23 | - name: Check workflow files 24 | run: ${{ steps.get_actionlint.outputs.executable }} -color 25 | shell: bash 26 | -------------------------------------------------------------------------------- /src/main/java/io/jare/fake/FkDomain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.fake; 6 | 7 | import io.jare.model.Domain; 8 | import io.jare.model.Usage; 9 | import java.io.IOException; 10 | 11 | /** 12 | * Fake domain. 13 | * 14 | * @since 1.0 15 | */ 16 | public final class FkDomain implements Domain { 17 | 18 | @Override 19 | public String owner() { 20 | return "name:test:1"; 21 | } 22 | 23 | @Override 24 | public String name() { 25 | return "jare.io"; 26 | } 27 | 28 | @Override 29 | public void delete() { 30 | // nothing 31 | } 32 | 33 | @Override 34 | public Usage usage() throws IOException { 35 | return new FkUsage(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/io/jare/tk/RqUserTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import org.hamcrest.MatcherAssert; 8 | import org.hamcrest.Matchers; 9 | import org.junit.Test; 10 | import org.takes.facets.auth.RqWithAuth; 11 | 12 | /** 13 | * Test case for {@link RqUser}. 14 | * @since 0.2 15 | */ 16 | public final class RqUserTest { 17 | 18 | /** 19 | * RqUser can fetch user name. 20 | * @throws Exception If some problem inside 21 | */ 22 | @Test 23 | public void fetchesUserName() throws Exception { 24 | MatcherAssert.assertThat( 25 | new RqUser(new RqWithAuth("urn:github:yegor256")).name(), 26 | Matchers.nullValue() 27 | ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/jare/fake/FkUsage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.fake; 6 | 7 | import com.jcabi.log.Logger; 8 | import io.jare.model.Usage; 9 | import java.util.Date; 10 | import java.util.SortedMap; 11 | import java.util.TreeMap; 12 | 13 | /** 14 | * Fake usage. 15 | * 16 | * @since 0.7 17 | */ 18 | public final class FkUsage implements Usage { 19 | 20 | @Override 21 | public void add(final Date date, final long bytes) { 22 | Logger.info(this, "usage, date=%s, bytes=%d", date, bytes); 23 | } 24 | 25 | @Override 26 | public long total() { 27 | return 1L; 28 | } 29 | 30 | @Override 31 | public SortedMap history() { 32 | return new TreeMap<>(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/jare/fake/FkBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.fake; 6 | 7 | import io.jare.model.Base; 8 | import io.jare.model.Domain; 9 | import io.jare.model.User; 10 | import java.util.Collections; 11 | 12 | /** 13 | * Fake Base. 14 | * 15 | * @since 1.0 16 | */ 17 | public final class FkBase implements Base { 18 | 19 | @Override 20 | public User user(final String name) { 21 | return new FkUser(); 22 | } 23 | 24 | @Override 25 | public Iterable domain(final String name) { 26 | return Collections.singleton(new FkDomain()); 27 | } 28 | 29 | @Override 30 | public Iterable all() { 31 | return Collections.singleton(new FkDomain()); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: codecov 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | jobs: 11 | codecov: 12 | timeout-minutes: 15 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-java@v4 17 | with: 18 | distribution: 'temurin' 19 | java-version: 11 20 | - uses: actions/cache@v4 21 | with: 22 | path: ~/.m2/repository 23 | key: maven-${{ hashFiles('**/pom.xml') }} 24 | restore-keys: | 25 | maven- 26 | - run: mvn install -Pjacoco 27 | - uses: codecov/codecov-action@v5 28 | with: 29 | files: ./target/site/jacoco/jacoco.xml 30 | fail_ci_if_error: true 31 | -------------------------------------------------------------------------------- /src/test/java/io/jare/cached/CdUsageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.cached; 6 | 7 | import io.jare.fake.FkBase; 8 | import io.jare.model.Usage; 9 | import org.hamcrest.MatcherAssert; 10 | import org.hamcrest.Matchers; 11 | import org.junit.Test; 12 | 13 | /** 14 | * Test case for {@link CdUsage}. 15 | * @since 0.7 16 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 17 | */ 18 | public final class CdUsageTest { 19 | 20 | /** 21 | * CdUsage can make objects right. 22 | * @throws Exception If some problem inside 23 | */ 24 | @Test 25 | public void makesObjects() throws Exception { 26 | final Usage usage = new CdBase(new FkBase()).domain("") 27 | .iterator().next().usage(); 28 | MatcherAssert.assertThat(usage.total(), Matchers.notNullValue()); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/jare/model/Domain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.model; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * Domain. 11 | * 12 | * @since 1.0 13 | */ 14 | public interface Domain { 15 | 16 | /** 17 | * Owner of it. 18 | * @return The owner's GitHub handle 19 | * @throws IOException If fails 20 | */ 21 | String owner() throws IOException; 22 | 23 | /** 24 | * Name. 25 | * @return The name 26 | * @throws IOException If fails 27 | */ 28 | String name() throws IOException; 29 | 30 | /** 31 | * Delete it. 32 | * @throws IOException If fails 33 | */ 34 | void delete() throws IOException; 35 | 36 | /** 37 | * Usage. 38 | * @return Usage 39 | * @throws IOException If fails 40 | */ 41 | Usage usage() throws IOException; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/jare/model/Usage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.model; 6 | 7 | import java.io.IOException; 8 | import java.util.Date; 9 | import java.util.SortedMap; 10 | 11 | /** 12 | * Usage. 13 | * 14 | * @since 0.7 15 | */ 16 | public interface Usage { 17 | 18 | /** 19 | * Add more usage in bytes. 20 | * @param date When did it happen 21 | * @param bytes How many bytes 22 | * @throws IOException If fails 23 | */ 24 | void add(Date date, long bytes) throws IOException; 25 | 26 | /** 27 | * Total, over the last ten days. 28 | * @return The total in bytes 29 | * @throws IOException If fails 30 | */ 31 | long total() throws IOException; 32 | 33 | /** 34 | * History. 35 | * @return Full usage history 36 | * @throws IOException If fails 37 | */ 38 | SortedMap history() throws IOException; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/dynamodb/domains.json: -------------------------------------------------------------------------------- 1 | { 2 | "AttributeDefinitions": [ 3 | { 4 | "AttributeName": "domain", 5 | "AttributeType": "S" 6 | }, 7 | { 8 | "AttributeName": "user", 9 | "AttributeType": "S" 10 | } 11 | ], 12 | "GlobalSecondaryIndexes": [ 13 | { 14 | "IndexName": "mine", 15 | "KeySchema": [ 16 | { 17 | "AttributeName": "user", 18 | "KeyType": "HASH" 19 | }, 20 | { 21 | "AttributeName": "domain", 22 | "KeyType": "RANGE" 23 | } 24 | ], 25 | "Projection": { 26 | "ProjectionType": "ALL" 27 | }, 28 | "ProvisionedThroughput": { 29 | "ReadCapacityUnits": "1", 30 | "WriteCapacityUnits": "1" 31 | } 32 | } 33 | ], 34 | "KeySchema": [ 35 | { 36 | "AttributeName": "domain", 37 | "KeyType": "HASH" 38 | } 39 | ], 40 | "ProvisionedThroughput": { 41 | "ReadCapacityUnits": "1", 42 | "WriteCapacityUnits": "1" 43 | }, 44 | "TableName": "domains" 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/mvn.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: mvn 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | mvn: 15 | timeout-minutes: 15 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-24.04, windows-2022, macos-15] 20 | java: [11, 17] 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-java@v4 24 | with: 25 | distribution: 'temurin' 26 | java-version: ${{ matrix.java }} 27 | - uses: actions/cache@v4 28 | with: 29 | path: ~/.m2/repository 30 | key: ${{ runner.os }}-jdk-${{ matrix.java }}-maven-${{ hashFiles('**/pom.xml') }} 31 | restore-keys: | 32 | ${{ runner.os }}-jdk-${{ matrix.java }}-maven- 33 | - run: java -version 34 | - run: mvn -version 35 | - run: mvn --errors --batch-mode clean install -Pqulice 36 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | 4 | version = 1 5 | [[annotations]] 6 | path = [ 7 | ".DS_Store", 8 | ".gitattributes", 9 | ".gitignore", 10 | ".pdd", 11 | "**.json", 12 | "**.md", 13 | "**.png", 14 | "**.sigil", 15 | "**.svg", 16 | "**.txt", 17 | "**.vm", 18 | "**/.DS_Store", 19 | "**/.gitignore", 20 | "**/.pdd", 21 | "**/*.csv", 22 | "**/*.jpg", 23 | "**/*.json", 24 | "**/*.md", 25 | "**/*.pdf", 26 | "**/*.png", 27 | "**/*.svg", 28 | "**/*.txt", 29 | "**/*.vm", 30 | "**/CNAME", 31 | "**/MANIFEST.MF", 32 | "**/Procfile", 33 | "MANIFEST.MF", 34 | "Procfile", 35 | "README.md", 36 | "renovate.json", 37 | "src/main/resources/META-INF/services/javax.xml.transform.TransformerFactory", 38 | "src/main/resources/META-INF/services/javax.xml.xpath.XPathFactory", 39 | "src/test/resources/io/jare/test", 40 | ] 41 | precedence = "override" 42 | SPDX-FileCopyrightText = "Copyright (c) 2025 Yegor Bugayenko" 43 | SPDX-License-Identifier = "MIT" 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2025 Yegor Bugayenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: the above copyright notice and this 12 | permission notice shall be included in all copies or substantial 13 | portions of the Software. The software is provided "as is", without 14 | warranty of any kind, express or implied, including but not limited to 15 | the warranties of merchantability, fitness for a particular purpose 16 | and non-infringement. In no event shall the authors or copyright 17 | holders be liable for any claim, damages or other liability, whether 18 | in an action of contract, tort or otherwise, arising from, out of or 19 | in connection with the software or the use or other dealings in the 20 | software. 21 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2025 Yegor Bugayenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: the above copyright notice and this 12 | permission notice shall be included in all copies or substantial 13 | portions of the Software. The software is provided "as is", without 14 | warranty of any kind, express or implied, including but not limited to 15 | the warranties of merchantability, fitness for a particular purpose 16 | and non-infringement. In no event shall the authors or copyright 17 | holders be liable for any claim, damages or other liability, whether 18 | in an action of contract, tort or otherwise, arising from, out of or 19 | in connection with the software or the use or other dealings in the 20 | software. 21 | -------------------------------------------------------------------------------- /src/test/java/io/jare/tk/TkInvalidateITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import org.hamcrest.MatcherAssert; 8 | import org.hamcrest.Matchers; 9 | import org.junit.Ignore; 10 | import org.junit.Test; 11 | import org.takes.Response; 12 | import org.takes.rq.RqFake; 13 | 14 | /** 15 | * Integration case for {@link TkInvalidate}. 16 | * @since 1.0 17 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 18 | */ 19 | public final class TkInvalidateITCase { 20 | 21 | /** 22 | * TkInvalidate can invalidate URL. 23 | * @throws Exception If some problem inside 24 | */ 25 | @Test 26 | @Ignore 27 | public void invalidatesUrl() throws Exception { 28 | final String url = 29 | "http://www.yegor256.com/images/yegor-bugayenko-192x192.png"; 30 | final Response rsp = new TkInvalidate( 31 | "-key-", "-secret-" 32 | ).act(new RqFake("GET", String.format("/invalidate?url=%s", url))); 33 | MatcherAssert.assertThat( 34 | rsp.toString(), 35 | Matchers.containsString("InProgress") 36 | ); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/io/jare/dynamo/DyUsageITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.dynamo; 6 | 7 | import io.jare.model.Base; 8 | import io.jare.model.Domain; 9 | import io.jare.model.Usage; 10 | import io.jare.model.User; 11 | import java.util.Date; 12 | import org.hamcrest.MatcherAssert; 13 | import org.hamcrest.Matchers; 14 | import org.junit.Test; 15 | 16 | /** 17 | * Integration case for {@link DyUsage}. 18 | * @since 0.7 19 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 20 | */ 21 | public final class DyUsageITCase { 22 | 23 | /** 24 | * DyUsage can be record usage. 25 | * @throws Exception If some problem inside 26 | */ 27 | @Test 28 | public void recordsUsage() throws Exception { 29 | final Base base = new DyBase(new Dynamo()); 30 | final User user = base.user("Erik"); 31 | final String name = "yegor256.com"; 32 | user.add(name); 33 | final Domain domain = base.domain(name).iterator().next(); 34 | final Usage usage = domain.usage(); 35 | usage.add(new Date(), 1L); 36 | usage.add(new Date(), 1L); 37 | MatcherAssert.assertThat(usage.total(), Matchers.equalTo(2L)); 38 | domain.delete(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/jare/Entrance.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare; 6 | 7 | import com.jcabi.manifests.Manifests; 8 | import com.jcabi.s3.Region; 9 | import io.jare.cached.CdBase; 10 | import io.jare.dynamo.DyBase; 11 | import io.jare.model.Base; 12 | import io.jare.tk.TkApp; 13 | import io.sentry.Sentry; 14 | import java.io.IOException; 15 | import org.takes.http.Exit; 16 | import org.takes.http.FtCli; 17 | 18 | /** 19 | * Command line entry. 20 | * 21 | * @since 1.0 22 | */ 23 | public final class Entrance { 24 | 25 | /** 26 | * Ctor. 27 | */ 28 | private Entrance() { 29 | // utility class 30 | } 31 | 32 | /** 33 | * Main entry point. 34 | * @param args Arguments 35 | * @throws IOException If fails 36 | */ 37 | public static void main(final String... args) throws IOException { 38 | Sentry.init(Manifests.read("Jare-SentryDsn")); 39 | final Base base = new CdBase(new DyBase()); 40 | new Logs( 41 | base, 42 | new Region.Simple( 43 | Manifests.read("Jare-S3Key"), 44 | Manifests.read("Jare-S3Secret") 45 | ).bucket("logs.jare.io") 46 | ); 47 | new FtCli(new TkApp(base), args).start(Exit.NEVER); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/jare/cached/CdDomain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.cached; 6 | 7 | import com.jcabi.aspects.Cacheable; 8 | import io.jare.model.Domain; 9 | import io.jare.model.Usage; 10 | import java.io.IOException; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.ToString; 13 | 14 | /** 15 | * Cached Domain. 16 | * 17 | * @since 1.0 18 | */ 19 | @ToString 20 | @EqualsAndHashCode(of = "origin") 21 | final class CdDomain implements Domain { 22 | 23 | /** 24 | * Original. 25 | */ 26 | private final transient Domain origin; 27 | 28 | /** 29 | * Ctor. 30 | * @param domain Original 31 | */ 32 | CdDomain(final Domain domain) { 33 | this.origin = domain; 34 | } 35 | 36 | @Override 37 | @Cacheable(forever = true) 38 | public String owner() throws IOException { 39 | return this.origin.owner(); 40 | } 41 | 42 | @Override 43 | @Cacheable(forever = true) 44 | public String name() throws IOException { 45 | return this.origin.name(); 46 | } 47 | 48 | @Override 49 | public void delete() throws IOException { 50 | this.origin.delete(); 51 | } 52 | 53 | @Override 54 | public Usage usage() throws IOException { 55 | return new CdUsage(this.origin.usage()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/io/jare/dynamo/DyDomainITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.dynamo; 6 | 7 | import io.jare.model.Base; 8 | import io.jare.model.Domain; 9 | import io.jare.model.User; 10 | import org.hamcrest.MatcherAssert; 11 | import org.hamcrest.Matchers; 12 | import org.junit.Test; 13 | 14 | /** 15 | * Integration case for {@link DyDomain}. 16 | * @since 1.0 17 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 18 | */ 19 | public final class DyDomainITCase { 20 | 21 | /** 22 | * DyDomain can be added and removed. 23 | * @throws Exception If some problem inside 24 | */ 25 | @Test 26 | public void addsAndRemoveDomains() throws Exception { 27 | final Base base = new DyBase(new Dynamo()); 28 | final String john = "john"; 29 | final User user = base.user(john); 30 | final String name = "google.com"; 31 | user.add(name); 32 | final Domain domain = base.domain(name).iterator().next(); 33 | MatcherAssert.assertThat(domain.name(), Matchers.equalTo(name)); 34 | MatcherAssert.assertThat(domain.owner(), Matchers.equalTo(john)); 35 | domain.delete(); 36 | MatcherAssert.assertThat( 37 | base.domain(name).iterator().hasNext(), 38 | Matchers.equalTo(false) 39 | ); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/TkRefresh.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import com.jcabi.log.VerboseProcess; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import org.takes.facets.fork.FkFixed; 11 | import org.takes.facets.fork.FkHitRefresh; 12 | import org.takes.facets.fork.TkFork; 13 | import org.takes.tk.TkClasspath; 14 | import org.takes.tk.TkFiles; 15 | import org.takes.tk.TkWrap; 16 | 17 | /** 18 | * Refresh on hit. 19 | * 20 | * @since 1.0 21 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 22 | */ 23 | final class TkRefresh extends TkWrap { 24 | 25 | /** 26 | * Ctor. 27 | * @param path Path of files 28 | * @throws IOException If fails 29 | */ 30 | TkRefresh(final String path) throws IOException { 31 | super( 32 | new TkFork( 33 | new FkHitRefresh( 34 | new File(path), 35 | () -> new VerboseProcess( 36 | new ProcessBuilder( 37 | "mvn", 38 | "generate-resources" 39 | ) 40 | ).stdout(), 41 | new TkFiles("./target/classes") 42 | ), 43 | new FkFixed(new TkClasspath()) 44 | ) 45 | ); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/jare/cached/CdUsage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.cached; 6 | 7 | import com.jcabi.aspects.Cacheable; 8 | import io.jare.model.Usage; 9 | import java.io.IOException; 10 | import java.util.Date; 11 | import java.util.SortedMap; 12 | import java.util.concurrent.TimeUnit; 13 | import lombok.EqualsAndHashCode; 14 | import lombok.ToString; 15 | 16 | /** 17 | * Cached Usage. 18 | * 19 | * @since 0.7 20 | */ 21 | @ToString 22 | @EqualsAndHashCode(of = "origin") 23 | final class CdUsage implements Usage { 24 | 25 | /** 26 | * Original. 27 | */ 28 | private final transient Usage origin; 29 | 30 | /** 31 | * Ctor. 32 | * @param usage Original 33 | */ 34 | CdUsage(final Usage usage) { 35 | this.origin = usage; 36 | } 37 | 38 | @Override 39 | @Cacheable.FlushBefore 40 | public void add(final Date date, final long bytes) throws IOException { 41 | this.origin.add(date, bytes); 42 | } 43 | 44 | @Override 45 | @Cacheable(lifetime = 1, unit = TimeUnit.HOURS) 46 | public long total() throws IOException { 47 | return this.origin.total(); 48 | } 49 | 50 | @Override 51 | @Cacheable(lifetime = 1, unit = TimeUnit.HOURS) 52 | public SortedMap history() throws IOException { 53 | return this.origin.history(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/RqUser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import java.io.IOException; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | import org.takes.Request; 11 | import org.takes.facets.auth.Identity; 12 | import org.takes.facets.auth.RqAuth; 13 | import org.takes.rq.RqWrap; 14 | 15 | /** 16 | * User in request. 17 | * 18 | * @since 1.0 19 | */ 20 | public final class RqUser extends RqWrap { 21 | 22 | /** 23 | * Pattern to fetch name. 24 | */ 25 | private static final Pattern PTN = Pattern.compile( 26 | "urn:(?:github|test):(.*)" 27 | ); 28 | 29 | /** 30 | * Ctor. 31 | * @param req Request 32 | */ 33 | public RqUser(final Request req) { 34 | super(req); 35 | } 36 | 37 | /** 38 | * Get user name (GitHub handle). 39 | * @return Name 40 | * @throws IOException If fails 41 | */ 42 | public String name() throws IOException { 43 | final Identity identity = new RqAuth(this).identity(); 44 | final String urn = identity.urn(); 45 | final Matcher mtr = RqUser.PTN.matcher(urn); 46 | if (!mtr.matches()) { 47 | throw new IllegalArgumentException( 48 | String.format("URN \"%s\" is not from GitHub", urn) 49 | ); 50 | } 51 | return identity.properties().get("login"); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /.rultor.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | docker: 6 | image: yegor256/rultor-image:1.24.0 7 | readers: 8 | - "urn:github:526301" 9 | assets: 10 | settings.xml: yegor256/home#assets/jare/settings.xml 11 | id_rsa: yegor256/home#assets/heroku-key 12 | id_rsa.pub: yegor256/home#assets/heroku-key.pub 13 | install: |- 14 | pdd --source=$(pwd) --verbose --file=/dev/null 15 | release: 16 | pre: false 17 | sensitive: 18 | - settings.xml 19 | script: | 20 | mvn versions:set "-DnewVersion=${tag}" 21 | git commit -am "${tag}" 22 | cp ../settings.xml settings.xml 23 | mvn clean package -Pqulice -Pjare --errors --batch-mode 24 | git remote add dokku dokku@www.jare.io:jare 25 | mkdir ~/.ssh 26 | mv ../id_rsa ../id_rsa.pub ~/.ssh 27 | chmod -R 600 ~/.ssh/* 28 | echo -e "Host *\n StrictHostKeyChecking no\n UserKnownHostsFile=/dev/null" > ~/.ssh/config 29 | git add settings.xml 30 | git fetch 31 | git commit -m 'settings.xml' 32 | git push -f dokku $(git symbolic-ref --short HEAD):master 33 | git reset HEAD~1 34 | rm -rf settings.xml 35 | curl -f --connect-timeout 15 --retry 5 --retry-delay 30 http://www.jare.io > /dev/null 36 | # mvn clean site-deploy -Psite --batch-mode --settings ../settings.xml 37 | merge: 38 | script: |- 39 | mvn help:system clean install -Pqulice --settings ../settings.xml 40 | mvn clean site -Psite --settings ../settings.xml 41 | -------------------------------------------------------------------------------- /src/main/java/io/jare/dynamo/DyDomain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.dynamo; 6 | 7 | import com.jcabi.dynamo.Attributes; 8 | import com.jcabi.dynamo.Item; 9 | import io.jare.model.Domain; 10 | import io.jare.model.Usage; 11 | import java.io.IOException; 12 | import lombok.EqualsAndHashCode; 13 | import lombok.ToString; 14 | 15 | /** 16 | * Dynamo domain. 17 | * 18 | * @since 1.0 19 | * @checkstyle MultipleStringLiteralsCheck (500 lines) 20 | */ 21 | @ToString 22 | @EqualsAndHashCode(of = "item") 23 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") 24 | public final class DyDomain implements Domain { 25 | 26 | /** 27 | * The item. 28 | */ 29 | private final transient Item item; 30 | 31 | /** 32 | * Ctor. 33 | * @param itm Item 34 | */ 35 | public DyDomain(final Item itm) { 36 | this.item = itm; 37 | } 38 | 39 | @Override 40 | public String owner() throws IOException { 41 | return this.item.get("user").getS(); 42 | } 43 | 44 | @Override 45 | public String name() throws IOException { 46 | return this.item.get("domain").getS(); 47 | } 48 | 49 | @Override 50 | public void delete() throws IOException { 51 | this.item.frame().table().delete( 52 | new Attributes().with("domain", this.name()) 53 | ); 54 | } 55 | 56 | @Override 57 | public Usage usage() { 58 | return new DyUsage(this.item); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/jare/cached/CdBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.cached; 6 | 7 | import com.jcabi.aspects.Cacheable; 8 | import io.jare.model.Base; 9 | import io.jare.model.Domain; 10 | import io.jare.model.User; 11 | import java.util.concurrent.TimeUnit; 12 | import lombok.EqualsAndHashCode; 13 | import lombok.ToString; 14 | import org.cactoos.iterable.Mapped; 15 | 16 | /** 17 | * Cached Base. 18 | * 19 | * @since 1.0 20 | */ 21 | @ToString 22 | @EqualsAndHashCode(of = "origin") 23 | public final class CdBase implements Base { 24 | 25 | /** 26 | * Original. 27 | */ 28 | private final transient Base origin; 29 | 30 | /** 31 | * Ctor. 32 | * @param base Original 33 | */ 34 | public CdBase(final Base base) { 35 | this.origin = base; 36 | } 37 | 38 | @Override 39 | @Cacheable(forever = true) 40 | public User user(final String name) { 41 | return this.origin.user(name); 42 | } 43 | 44 | @Override 45 | @Cacheable(lifetime = 1, unit = TimeUnit.MINUTES) 46 | public Iterable domain(final String name) { 47 | return new Mapped<>( 48 | CdDomain::new, 49 | this.origin.domain(name) 50 | ); 51 | } 52 | 53 | @Override 54 | @Cacheable(unit = TimeUnit.HOURS, lifetime = 1) 55 | public Iterable all() { 56 | return new Mapped<>( 57 | CdDomain::new, 58 | this.origin.all() 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logo 2 | 3 | [![EO principles respected here](https://www.elegantobjects.org/badge.svg)](https://www.elegantobjects.org) 4 | [![DevOps By Rultor.com](https://www.rultor.com/b/yegor256/jare)](https://www.rultor.com/p/yegor256/jare) 5 | [![We recommend IntelliJ IDEA](https://www.elegantobjects.org/intellij-idea.svg)](https://www.jetbrains.com/idea/) 6 | 7 | [![mvn](https://github.com/yegor256/jare/actions/workflows/mvn.yml/badge.svg)](https://github.com/yegor256/jare/actions/workflows/mvn.yml) 8 | [![Availability at SixNines](https://www.sixnines.io/b/c292)](https://www.sixnines.io/h/c292) 9 | [![PDD status](https://www.0pdd.com/svg?name=yegor256/jare)](https://www.0pdd.com/p?name=teamed/yegor256/jare) 10 | [![Hits-of-Code](https://hitsofcode.com/github/yegor256/jare)](https://hitsofcode.com/view/github/yegor256/jare) 11 | [![codecov](https://codecov.io/gh/yegor256/jare/branch/master/graph/badge.svg)](https://codecov.io/gh/yegor256/jare) 12 | 13 | [www.jare.io](http://www.jare.io) 14 | 15 | Lightweight Content Delivery Network (CDN) 16 | 17 | More about it in this blog post: 18 | [_Jare.io, an Instant and Free CDN_](http://www.yegor256.com/2016/03/30/jare-instant-free-cdn.html). 19 | 20 | ## How to contribute 21 | 22 | Fork repository, make changes, send us a pull request. We will review 23 | your changes and apply them to the `master` branch shortly, provided 24 | they don't violate our quality standards. To avoid frustration, before 25 | sending us your pull request please run full Maven build: 26 | 27 | ``` 28 | $ mvn clean install -Pqulice 29 | ``` 30 | 31 | To avoid build errors use Maven 3.2+ and Java 8+. 32 | -------------------------------------------------------------------------------- /src/test/java/io/jare/LogsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare; 6 | 7 | import com.jcabi.s3.Bucket; 8 | import com.jcabi.s3.Ocket; 9 | import io.jare.fake.FkBase; 10 | import java.io.OutputStream; 11 | import java.util.Collections; 12 | import java.util.zip.GZIPOutputStream; 13 | import org.apache.commons.io.IOUtils; 14 | import org.junit.Test; 15 | import org.mockito.Mockito; 16 | 17 | /** 18 | * Test case for {@link Logs}. 19 | * @since 1.0 20 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 21 | */ 22 | public final class LogsTest { 23 | 24 | /** 25 | * Logs can unzip and log. 26 | * @throws Exception If some problem inside 27 | */ 28 | @Test 29 | public void unzipsAndLogs() throws Exception { 30 | final Ocket ocket = Mockito.mock(Ocket.class); 31 | Mockito.doAnswer( 32 | inv -> { 33 | final GZIPOutputStream gzip = new GZIPOutputStream( 34 | OutputStream.class.cast(inv.getArgument(0)) 35 | ); 36 | IOUtils.copyLarge( 37 | Logs.class.getResourceAsStream("test"), 38 | gzip 39 | ); 40 | gzip.close(); 41 | return null; 42 | } 43 | ).when(ocket).read(Mockito.any(OutputStream.class)); 44 | final Bucket bucket = Mockito.mock(Bucket.class); 45 | Mockito.doReturn(ocket).when(bucket).ocket(Mockito.anyString()); 46 | Mockito.doReturn(Collections.singleton("x")).when(bucket).list(""); 47 | new Logs(new FkBase(), bucket).run(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/TkAdd.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import io.jare.model.Base; 8 | import io.jare.smarts.SafeUser; 9 | import java.io.IOException; 10 | import org.takes.Request; 11 | import org.takes.Response; 12 | import org.takes.Take; 13 | import org.takes.facets.flash.RsFlash; 14 | import org.takes.facets.forward.RsForward; 15 | import org.takes.rq.form.RqFormBase; 16 | 17 | /** 18 | * Add pipe. 19 | * 20 | * @since 1.0 21 | */ 22 | final class TkAdd implements Take { 23 | 24 | /** 25 | * Base. 26 | */ 27 | private final transient Base base; 28 | 29 | /** 30 | * Ctor. 31 | * @param bse Base 32 | */ 33 | TkAdd(final Base bse) { 34 | this.base = bse; 35 | } 36 | 37 | @Override 38 | public Response act(final Request req) throws IOException { 39 | final String name = new RqFormBase(req).param("name") 40 | .iterator().next().trim(); 41 | try { 42 | new SafeUser(this.base.user(new RqUser(req).name())).add(name); 43 | } catch (final SafeUser.InvalidNameException ex) { 44 | throw TkAdd.forward(new RsFlash(ex)); 45 | } 46 | return TkAdd.forward( 47 | new RsFlash( 48 | String.format( 49 | "domain \"%s\" added", name 50 | ) 51 | ) 52 | ); 53 | } 54 | 55 | /** 56 | * Make forward. 57 | * @param rsp Response 58 | * @return Forward 59 | */ 60 | private static RsForward forward(final Response rsp) { 61 | return new RsForward(rsp, "/domains"); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/TkIndex.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import io.jare.model.Base; 8 | import io.jare.model.Domain; 9 | import java.io.IOException; 10 | import org.takes.Request; 11 | import org.takes.Response; 12 | import org.takes.Take; 13 | import org.takes.rs.xe.XeAppend; 14 | import org.takes.rs.xe.XeDirectives; 15 | import org.takes.rs.xe.XeSource; 16 | import org.takes.rs.xe.XeTransform; 17 | import org.xembly.Directives; 18 | 19 | /** 20 | * Index page, for anonymous users. 21 | * 22 | * @since 1.0 23 | */ 24 | final class TkIndex implements Take { 25 | 26 | /** 27 | * Base. 28 | */ 29 | private final transient Base base; 30 | 31 | /** 32 | * Ctor. 33 | * @param bse Base 34 | */ 35 | TkIndex(final Base bse) { 36 | this.base = bse; 37 | } 38 | 39 | @Override 40 | public Response act(final Request req) throws IOException { 41 | return new RsPage( 42 | "/xsl/index.xsl", 43 | req, 44 | new XeAppend( 45 | "domains", 46 | new XeTransform<>( 47 | this.base.all(), 48 | TkIndex::source 49 | ) 50 | ) 51 | ); 52 | } 53 | 54 | /** 55 | * Convert event to Xembly. 56 | * @param domain The event 57 | * @return Xembly 58 | * @throws IOException If fails 59 | */ 60 | private static XeSource source(final Domain domain) throws IOException { 61 | return new XeDirectives( 62 | new Directives() 63 | .add("domain") 64 | .add("name").set(domain.name()).up() 65 | .add("owner").set(domain.owner()).up() 66 | .add("usage").set(domain.usage().total()).up() 67 | .up() 68 | ); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/jare/dynamo/Dynamo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.dynamo; 6 | 7 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; 8 | import com.jcabi.dynamo.Credentials; 9 | import com.jcabi.dynamo.Region; 10 | import com.jcabi.dynamo.Table; 11 | import com.jcabi.dynamo.retry.ReRegion; 12 | import com.jcabi.log.Logger; 13 | import com.jcabi.manifests.Manifests; 14 | 15 | /** 16 | * Dynamo. 17 | * 18 | * @since 1.0 19 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 20 | */ 21 | final class Dynamo implements Region { 22 | 23 | /** 24 | * Region. 25 | */ 26 | private final transient Region region = Dynamo.connect(); 27 | 28 | @Override 29 | public AmazonDynamoDB aws() { 30 | return this.region.aws(); 31 | } 32 | 33 | @Override 34 | public Table table(final String name) { 35 | return this.region.table(name); 36 | } 37 | 38 | /** 39 | * Connect. 40 | * @return Region 41 | */ 42 | private static Region connect() { 43 | final String key = Manifests.read("Jare-DynamoKey"); 44 | final Credentials.Simple creds = new Credentials.Simple( 45 | key, Manifests.read("Jare-DynamoSecret") 46 | ); 47 | final Region region; 48 | if (key.startsWith("AAAAA")) { 49 | final int port = Integer.parseInt( 50 | System.getProperty("dynamo.port") 51 | ); 52 | region = new Region.Simple(new Credentials.Direct(creds, port)); 53 | Logger.warn(Dynamo.class, "test DynamoDB at port #%d", port); 54 | } else { 55 | region = new Region.Prefixed( 56 | new ReRegion(new Region.Simple(creds)), 57 | "jare-" 58 | ); 59 | } 60 | Logger.info(Dynamo.class, "DynamoDB connected as %s", key); 61 | return region; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/io/jare/smarts/SafeUser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.smarts; 6 | 7 | import io.jare.model.Domain; 8 | import io.jare.model.User; 9 | import java.io.IOException; 10 | import java.util.regex.Pattern; 11 | 12 | /** 13 | * Safe user. 14 | * 15 | * @since 1.0 16 | */ 17 | public final class SafeUser implements User { 18 | 19 | /** 20 | * Pattern to match. 21 | */ 22 | private static final Pattern PTN = Pattern.compile( 23 | "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(? mine() { 41 | return this.origin.mine(); 42 | } 43 | 44 | @Override 45 | public void add(final String name) throws IOException { 46 | if (!SafeUser.PTN.matcher(name).matches()) { 47 | throw new SafeUser.InvalidNameException(name); 48 | } 49 | this.origin.add(name); 50 | } 51 | 52 | /** 53 | * When name is not valid. 54 | * 55 | * @since 0.1 56 | */ 57 | public static final class InvalidNameException extends IOException { 58 | /** 59 | * Serialization marker. 60 | */ 61 | private static final long serialVersionUID = -869776873934626730L; 62 | 63 | /** 64 | * Ctor. 65 | * @param name Domain name 66 | */ 67 | public InvalidNameException(final String name) { 68 | super( 69 | String.format( 70 | "domain name \"%s\" is not valid", 71 | name 72 | ) 73 | ); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/TkDelete.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import io.jare.model.Base; 8 | import io.jare.model.Domain; 9 | import java.io.IOException; 10 | import java.util.Iterator; 11 | import org.takes.Request; 12 | import org.takes.Response; 13 | import org.takes.Take; 14 | import org.takes.facets.flash.RsFlash; 15 | import org.takes.facets.forward.RsForward; 16 | import org.takes.rq.RqHref; 17 | 18 | /** 19 | * Delete domain. 20 | * 21 | * @since 1.0 22 | */ 23 | final class TkDelete implements Take { 24 | 25 | /** 26 | * Base. 27 | */ 28 | private final transient Base base; 29 | 30 | /** 31 | * Ctor. 32 | * @param bse Base 33 | */ 34 | TkDelete(final Base bse) { 35 | this.base = bse; 36 | } 37 | 38 | @Override 39 | public Response act(final Request req) throws IOException { 40 | final String name = new RqHref.Base(req).href() 41 | .param("name").iterator().next(); 42 | final Iterator domains = this.base.domain(name).iterator(); 43 | if (!domains.hasNext()) { 44 | throw new RsForward( 45 | new RsFlash( 46 | String.format("domain \"%s\" doesn't exist", name) 47 | ) 48 | ); 49 | } 50 | final Domain domain = domains.next(); 51 | final String user = new RqUser(req).name(); 52 | if (!domain.owner().equals(user)) { 53 | throw new RsForward( 54 | new RsFlash( 55 | String.format("domain \"%s\" is not yours", name) 56 | ) 57 | ); 58 | } 59 | domain.delete(); 60 | return new RsForward( 61 | new RsFlash( 62 | String.format( 63 | "domain \"%s\" deleted", name 64 | ) 65 | ), 66 | "/domains" 67 | ); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/io/jare/dynamo/DyBaseITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.dynamo; 6 | 7 | import io.jare.model.Base; 8 | import io.jare.model.Domain; 9 | import io.jare.model.User; 10 | import org.hamcrest.MatcherAssert; 11 | import org.hamcrest.Matchers; 12 | import org.junit.Test; 13 | 14 | /** 15 | * Integration case for {@link DyBase}. 16 | * @since 1.0 17 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 18 | */ 19 | public final class DyBaseITCase { 20 | 21 | /** 22 | * DyBase can list domains. 23 | * @throws Exception If some problem inside 24 | */ 25 | @Test 26 | public void listsAllDomains() throws Exception { 27 | final Base base = new DyBase(new Dynamo()); 28 | final String erik = "erik"; 29 | final User user = base.user(erik); 30 | final String name = "www.example.com"; 31 | user.add(name); 32 | final Iterable list = base.all(); 33 | MatcherAssert.assertThat( 34 | list, 35 | Matchers.iterableWithSize(Matchers.greaterThan(0)) 36 | ); 37 | MatcherAssert.assertThat( 38 | list, 39 | Matchers.iterableWithSize(Matchers.greaterThan(0)) 40 | ); 41 | } 42 | 43 | /** 44 | * DyBase can list domain by name. 45 | * @throws Exception If some problem inside 46 | */ 47 | @Test 48 | public void listsDomainByName() throws Exception { 49 | final Base base = new DyBase(new Dynamo()); 50 | final String john = "johnny"; 51 | final User user = base.user(john); 52 | final String name = "www-1.example.com"; 53 | user.add(name); 54 | final Iterable list = base.domain(name); 55 | MatcherAssert.assertThat( 56 | list, 57 | Matchers.iterableWithSize(1) 58 | ); 59 | MatcherAssert.assertThat( 60 | list, 61 | Matchers.iterableWithSize(1) 62 | ); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/io/jare/tk/PingingTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import io.jare.fake.FkBase; 8 | import java.net.HttpURLConnection; 9 | import java.util.Arrays; 10 | import java.util.Collection; 11 | import org.hamcrest.MatcherAssert; 12 | import org.hamcrest.Matchers; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.junit.runners.Parameterized; 16 | import org.takes.Take; 17 | import org.takes.facets.hamcrest.HmRsStatus; 18 | import org.takes.rq.RqFake; 19 | 20 | /** 21 | * Test case for {@link TkApp}. 22 | * @since 1.0 23 | */ 24 | @RunWith(Parameterized.class) 25 | public final class PingingTest { 26 | 27 | /** 28 | * The URL to ping. 29 | */ 30 | private final transient String url; 31 | 32 | /** 33 | * Ctor. 34 | * @param addr The URL to test 35 | */ 36 | public PingingTest(final String addr) { 37 | this.url = addr; 38 | } 39 | 40 | /** 41 | * Params for JUnit. 42 | * @return Parameters 43 | */ 44 | @Parameterized.Parameters 45 | public static Collection params() { 46 | return Arrays.asList( 47 | new Object[][] { 48 | {"/?x=y"}, 49 | {"/robots.txt"}, 50 | {"/xsl/layout.xsl"}, 51 | {"/css/main.css"}, 52 | {"/images/logo.svg"}, 53 | {"/images/logo.png"}, 54 | } 55 | ); 56 | } 57 | 58 | /** 59 | * App can render the URL. 60 | * @throws Exception If some problem inside 61 | */ 62 | @Test 63 | public void rendersAllPossibleUrls() throws Exception { 64 | final Take take = new TkApp(new FkBase()); 65 | MatcherAssert.assertThat( 66 | this.url, 67 | take.act(new RqFake("INFO", this.url)), 68 | Matchers.not( 69 | new HmRsStatus( 70 | HttpURLConnection.HTTP_NOT_FOUND 71 | ) 72 | ) 73 | ); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/io/jare/smarts/SafeUserTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.smarts; 6 | 7 | import io.jare.fake.FkUser; 8 | import io.jare.model.User; 9 | import org.hamcrest.MatcherAssert; 10 | import org.hamcrest.Matchers; 11 | import org.junit.Assert; 12 | import org.junit.Test; 13 | 14 | /** 15 | * Test case for {@link User}. 16 | * @since 0.5 17 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 18 | */ 19 | public final class SafeUserTest { 20 | 21 | /** 22 | * User.Safe can accept normal domain names. 23 | * @throws Exception If some problem inside 24 | */ 25 | @Test 26 | public void acceptsValidDomains() throws Exception { 27 | final User user = new SafeUser(new FkUser()); 28 | final String[] domains = { 29 | "google.com", 30 | "www.google.com", 31 | "www-1.google.com", 32 | "google.ua", 33 | "www-8-9.google.ua", 34 | }; 35 | for (final String domain : domains) { 36 | user.add(domain); 37 | } 38 | } 39 | 40 | /** 41 | * User.Safe can reject invalid domain names. 42 | * @throws Exception If some problem inside 43 | */ 44 | @Test 45 | @SuppressWarnings("PMD.AvoidUsingHardCodedIP") 46 | public void rejectsInvalidDomains() throws Exception { 47 | final User user = new SafeUser(new FkUser()); 48 | final String[] domains = { 49 | "google-com", 50 | "google", 51 | "www-1 .google.com", 52 | "google.УА", 53 | "www-8=9.google.ua", 54 | "127.0.0.1", 55 | }; 56 | for (final String domain : domains) { 57 | try { 58 | user.add(domain); 59 | Assert.fail(String.format("exception expected: %s", domain)); 60 | } catch (final SafeUser.InvalidNameException ex) { 61 | MatcherAssert.assertThat( 62 | ex.getLocalizedMessage(), 63 | Matchers.containsString(domain) 64 | ); 65 | } 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/io/jare/tk/DestinationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import java.net.URI; 8 | import org.hamcrest.MatcherAssert; 9 | import org.hamcrest.Matchers; 10 | import org.junit.Test; 11 | 12 | /** 13 | * Test case for {@link Destination}. 14 | * @since 0.4 15 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 16 | */ 17 | public final class DestinationTest { 18 | 19 | /** 20 | * Destination can build a path. 21 | * @throws Exception If some problem inside 22 | */ 23 | @Test 24 | public void buildsPath() throws Exception { 25 | MatcherAssert.assertThat( 26 | new Destination( 27 | new URI( 28 | "http://www.google.com/a/b/%D0%B4%D0%B0?z=%D0%B0#%D0%B0" 29 | ) 30 | ).path(), 31 | Matchers.equalTo("/a/b/%D0%B4%D0%B0?z=%D0%B0#%D0%B0") 32 | ); 33 | } 34 | 35 | /** 36 | * Destination can build an empty path. 37 | * @throws Exception If some problem inside 38 | */ 39 | @Test 40 | public void buildsEmptyPath() throws Exception { 41 | MatcherAssert.assertThat( 42 | new Destination(new URI("http://www.google.com")).path(), 43 | Matchers.equalTo("/") 44 | ); 45 | } 46 | 47 | /** 48 | * Destination can build path with params. 49 | * @throws Exception If some problem inside 50 | */ 51 | @Test 52 | public void buildsPathWithParams() throws Exception { 53 | MatcherAssert.assertThat( 54 | new Destination( 55 | new URI("http://www.google.com?%D0%B0=1") 56 | ).path(), 57 | Matchers.equalTo("/?%D0%B0=1") 58 | ); 59 | } 60 | 61 | /** 62 | * Destination can build path with fragment. 63 | * @throws Exception If some problem inside 64 | */ 65 | @Test 66 | public void buildsPathWithFragment() throws Exception { 67 | MatcherAssert.assertThat( 68 | new Destination( 69 | new URI("http://www.google.com/%D0%B0?%D0%B0=1#t") 70 | ).path(), 71 | Matchers.equalTo("/%D0%B0?%D0%B0=1#t") 72 | ); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/Destination.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import java.net.HttpURLConnection; 8 | import java.net.URI; 9 | import org.takes.HttpException; 10 | 11 | /** 12 | * Destination for relay. 13 | * 14 | * @since 0.4 15 | */ 16 | final class Destination { 17 | 18 | /** 19 | * Destination URI. 20 | */ 21 | private final transient URI uri; 22 | 23 | /** 24 | * Ctor. 25 | * @param dst Destination URI (full) 26 | */ 27 | Destination(final URI dst) { 28 | this.uri = dst; 29 | } 30 | 31 | /** 32 | * Build destination path. 33 | * @return Destination path 34 | * @throws HttpException If fails 35 | */ 36 | public String path() throws HttpException { 37 | if (!this.uri.isAbsolute()) { 38 | throw new HttpException( 39 | HttpURLConnection.HTTP_BAD_REQUEST, 40 | String.format("URI \"%s\" is not absolute", this.uri) 41 | ); 42 | } 43 | final String protocol = this.uri.getScheme(); 44 | if (!"https".equals(protocol) && !"http".equals(protocol)) { 45 | throw new HttpException( 46 | HttpURLConnection.HTTP_BAD_REQUEST, 47 | String.format( 48 | "Protocol must be either HTTP or HTTPS at \"%s\"", 49 | this.uri 50 | ) 51 | ); 52 | } 53 | if (this.uri.getHost() == null) { 54 | throw new HttpException( 55 | HttpURLConnection.HTTP_BAD_REQUEST, 56 | String.format("URI \"%s\" doesn't have a host", this.uri) 57 | ); 58 | } 59 | final StringBuilder path = new StringBuilder(100); 60 | if (this.uri.getRawPath().isEmpty()) { 61 | path.append('/'); 62 | } else { 63 | path.append(this.uri.getRawPath()); 64 | } 65 | if (this.uri.getRawQuery() != null) { 66 | path.append('?').append(this.uri.getRawQuery()); 67 | } 68 | if (this.uri.getRawFragment() != null) { 69 | path.append('#').append(this.uri.getRawFragment()); 70 | } 71 | return path.toString(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/io/jare/tk/TkIndexTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import com.jcabi.matchers.XhtmlMatchers; 8 | import io.jare.fake.FkBase; 9 | import org.hamcrest.MatcherAssert; 10 | import org.junit.Test; 11 | import org.takes.Take; 12 | import org.takes.rq.RqFake; 13 | import org.takes.rq.RqWithHeader; 14 | import org.takes.rs.RsPrint; 15 | 16 | /** 17 | * Test case for {@link TkIndex}. 18 | * @since 1.0 19 | * @checkstyle MultipleStringLiteralsCheck (500 lines) 20 | */ 21 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") 22 | public final class TkIndexTest { 23 | 24 | /** 25 | * TkHome can render home page. 26 | * @throws Exception If some problem inside 27 | */ 28 | @Test 29 | public void rendersHomePage() throws Exception { 30 | final Take take = new TkAppAuth(new TkIndex(new FkBase())); 31 | MatcherAssert.assertThat( 32 | XhtmlMatchers.xhtml( 33 | new RsPrint( 34 | take.act( 35 | new RqWithHeader( 36 | new RqFake("GET", "/"), 37 | "Accept", 38 | "text/xml" 39 | ) 40 | ) 41 | ).printBody() 42 | ), 43 | XhtmlMatchers.hasXPaths( 44 | "/page/millis", 45 | "/page/identity/urn", 46 | "/page/version", 47 | "/page/links/link[@rel='home']", 48 | "/page/links/link[@rel='self']", 49 | "/page/links/link[@rel='takes:logout']", 50 | "/page/domains/domain[name and owner and usage]" 51 | ) 52 | ); 53 | } 54 | 55 | /** 56 | * TkIndex can render home page in HTML. 57 | * @throws Exception If some problem inside 58 | */ 59 | @Test 60 | public void rendersHomePageInHtml() throws Exception { 61 | final Take take = new TkAppAuth(new TkIndex(new FkBase())); 62 | MatcherAssert.assertThat( 63 | XhtmlMatchers.xhtml( 64 | new RsPrint( 65 | take.act(new RqFake("GET", "/")) 66 | ).printBody() 67 | ), 68 | XhtmlMatchers.hasXPaths( 69 | "/xhtml:html", 70 | "/xhtml:html/xhtml:body" 71 | ) 72 | ); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/resources/xsl/domains.xsl: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <xsl:text>Domains</xsl:text> 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 |

23 | 24 | There is just one domain: 25 | 26 | 27 | There are 28 | 29 | domains registered: 30 | 31 |

32 | 33 |
34 | 35 | 36 | 37 |
38 |
39 | 40 |

41 | There are no registered domains yet. 42 |

43 |
44 |
45 | 46 |
    47 | 48 |
49 |
50 | 51 |
  • 52 | 53 | : 54 | 55 | Kb 56 | 57 | delete 58 | 59 |
  • 60 |
    61 |
    62 | -------------------------------------------------------------------------------- /src/test/java/io/jare/dynamo/DyUserITCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.dynamo; 6 | 7 | import io.jare.model.Base; 8 | import io.jare.model.Domain; 9 | import io.jare.model.User; 10 | import java.io.IOException; 11 | import org.hamcrest.MatcherAssert; 12 | import org.hamcrest.Matchers; 13 | import org.junit.Test; 14 | 15 | /** 16 | * Integration case for {@link DyUser}. 17 | * @since 1.0 18 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 19 | */ 20 | public final class DyUserITCase { 21 | 22 | /** 23 | * DyUser can add and remove domains. 24 | * @throws Exception If some problem inside 25 | */ 26 | @Test 27 | public void addsAndRemoveDomains() throws Exception { 28 | final Base base = new DyBase(new Dynamo()); 29 | final User user = base.user("jeffrey"); 30 | final String name = "google.com"; 31 | user.add(name); 32 | final Domain domain = base.domain(name).iterator().next(); 33 | MatcherAssert.assertThat( 34 | domain.name(), 35 | Matchers.equalTo(name) 36 | ); 37 | domain.delete(); 38 | MatcherAssert.assertThat( 39 | base.domain(name).iterator().hasNext(), 40 | Matchers.equalTo(false) 41 | ); 42 | } 43 | 44 | /** 45 | * DyUser can list domains. 46 | * @throws Exception If some problem inside 47 | */ 48 | @Test 49 | public void listsMineDomains() throws Exception { 50 | final Base base = new DyBase(new Dynamo()); 51 | final User user = base.user("willy"); 52 | for (int idx = 0; idx < 10; ++idx) { 53 | user.add(String.format("facebook-%d.com", idx)); 54 | } 55 | final Iterable list = user.mine(); 56 | MatcherAssert.assertThat( 57 | list, 58 | Matchers.iterableWithSize(10) 59 | ); 60 | MatcherAssert.assertThat( 61 | list, 62 | Matchers.iterableWithSize(10) 63 | ); 64 | } 65 | 66 | /** 67 | * DyUser can reject if domain is occupied. 68 | * @throws Exception If some problem inside 69 | */ 70 | @Test(expected = IOException.class) 71 | public void rejectsIfOccupied() throws Exception { 72 | final Base base = new DyBase(new Dynamo()); 73 | base.user("melissa").add("yahoo.com"); 74 | final User alex = base.user("alex"); 75 | alex.add("Yahoo.com"); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/TkDomains.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import io.jare.model.Base; 8 | import io.jare.model.Domain; 9 | import io.jare.model.Usage; 10 | import java.io.IOException; 11 | import org.takes.Request; 12 | import org.takes.Response; 13 | import org.takes.Take; 14 | import org.takes.misc.Href; 15 | import org.takes.rs.xe.XeAppend; 16 | import org.takes.rs.xe.XeDirectives; 17 | import org.takes.rs.xe.XeLink; 18 | import org.takes.rs.xe.XeSource; 19 | import org.takes.rs.xe.XeTransform; 20 | import org.xembly.Directives; 21 | 22 | /** 23 | * Index page, for authenticated user. 24 | * 25 | * @since 1.0 26 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 27 | * @checkstyle MultipleStringLiteralsCheck (500 lines) 28 | */ 29 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") 30 | final class TkDomains implements Take { 31 | 32 | /** 33 | * Base. 34 | */ 35 | private final transient Base base; 36 | 37 | /** 38 | * Ctor. 39 | * @param bse Base 40 | */ 41 | TkDomains(final Base bse) { 42 | this.base = bse; 43 | } 44 | 45 | @Override 46 | public Response act(final Request req) throws IOException { 47 | return new RsPage( 48 | "/xsl/domains.xsl", 49 | req, 50 | new XeAppend( 51 | "domains", 52 | new XeTransform<>( 53 | this.base.user(new RqUser(req).name()).mine(), 54 | TkDomains::source 55 | ) 56 | ), 57 | new XeLink("add", "/add"), 58 | new XeLink("invalidate", "/invalidate") 59 | ); 60 | } 61 | 62 | /** 63 | * Convert event to Xembly. 64 | * @param domain The event 65 | * @return Xembly 66 | * @throws IOException If fails 67 | */ 68 | private static XeSource source(final Domain domain) throws IOException { 69 | final String name = domain.name(); 70 | final Usage usage = domain.usage(); 71 | return new XeDirectives( 72 | new Directives() 73 | .add("domain") 74 | .add("name").set(name).up() 75 | .add("usage").set(usage.total()).up() 76 | .append( 77 | new XeLink( 78 | "delete", 79 | new Href("/delete").with("name", name) 80 | ).toXembly() 81 | ) 82 | .up() 83 | ); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/io/jare/dynamo/DyBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.dynamo; 6 | 7 | import com.amazonaws.services.dynamodbv2.model.Select; 8 | import com.jcabi.dynamo.Conditions; 9 | import com.jcabi.dynamo.QueryValve; 10 | import com.jcabi.dynamo.Region; 11 | import com.jcabi.dynamo.ScanValve; 12 | import com.jcabi.dynamo.Table; 13 | import io.jare.model.Base; 14 | import io.jare.model.Domain; 15 | import io.jare.model.User; 16 | import java.util.Locale; 17 | import java.util.stream.Collectors; 18 | import lombok.EqualsAndHashCode; 19 | import lombok.ToString; 20 | 21 | /** 22 | * Dynamo Base. 23 | * 24 | * @since 1.0 25 | * @checkstyle MultipleStringLiteralsCheck (500 lines) 26 | */ 27 | @ToString 28 | @EqualsAndHashCode(of = "region") 29 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") 30 | public final class DyBase implements Base { 31 | 32 | /** 33 | * The region to work with. 34 | */ 35 | private final transient Region region; 36 | 37 | /** 38 | * Ctor. 39 | */ 40 | public DyBase() { 41 | this(new Dynamo()); 42 | } 43 | 44 | /** 45 | * Ctor. 46 | * @param reg Region 47 | */ 48 | public DyBase(final Region reg) { 49 | this.region = reg; 50 | } 51 | 52 | @Override 53 | public User user(final String name) { 54 | return new DyUser(this.region, name); 55 | } 56 | 57 | @Override 58 | public Iterable domain(final String name) { 59 | return this.table() 60 | .frame() 61 | .through( 62 | new QueryValve() 63 | .withSelect(Select.ALL_ATTRIBUTES) 64 | .withLimit(1) 65 | .withConsistentRead(true) 66 | ) 67 | .where( 68 | "domain", 69 | Conditions.equalTo(name.toLowerCase(Locale.ENGLISH)) 70 | ) 71 | .stream() 72 | .map(DyDomain::new) 73 | .map(Domain.class::cast) 74 | .collect(Collectors.toList()); 75 | } 76 | 77 | @Override 78 | public Iterable all() { 79 | return this.table() 80 | .frame() 81 | .through( 82 | new ScanValve() 83 | .withAttributeToGet("user", "domain", "total", "usage") 84 | .withLimit(1000) 85 | ) 86 | .stream() 87 | .map(DyDomain::new) 88 | .map(Domain.class::cast) 89 | .collect(Collectors.toList()); 90 | } 91 | 92 | /** 93 | * Table to work with. 94 | * @return Table 95 | */ 96 | private Table table() { 97 | return this.region.table("domains"); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/TkAppAuth.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import com.jcabi.manifests.Manifests; 8 | import java.util.regex.Pattern; 9 | import org.takes.Take; 10 | import org.takes.facets.auth.PsByFlag; 11 | import org.takes.facets.auth.PsChain; 12 | import org.takes.facets.auth.PsCookie; 13 | import org.takes.facets.auth.PsFake; 14 | import org.takes.facets.auth.PsLogout; 15 | import org.takes.facets.auth.TkAuth; 16 | import org.takes.facets.auth.codecs.CcAes; 17 | import org.takes.facets.auth.codecs.CcCompact; 18 | import org.takes.facets.auth.codecs.CcHex; 19 | import org.takes.facets.auth.codecs.CcSafe; 20 | import org.takes.facets.auth.codecs.CcSalted; 21 | import org.takes.facets.auth.social.PsGithub; 22 | import org.takes.facets.fork.FkFixed; 23 | import org.takes.facets.fork.FkParams; 24 | import org.takes.facets.fork.TkFork; 25 | import org.takes.tk.TkRedirect; 26 | import org.takes.tk.TkWrap; 27 | 28 | /** 29 | * Authenticated app. 30 | * 31 | * @since 1.0 32 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 33 | */ 34 | final class TkAppAuth extends TkWrap { 35 | 36 | /** 37 | * Ctor. 38 | * @param take Take 39 | */ 40 | TkAppAuth(final Take take) { 41 | super(TkAppAuth.make(take)); 42 | } 43 | 44 | /** 45 | * Authenticated. 46 | * @param take Takes 47 | * @return Authenticated takes 48 | */ 49 | private static Take make(final Take take) { 50 | return new TkAuth( 51 | new TkFork( 52 | new FkParams( 53 | PsByFlag.class.getSimpleName(), 54 | Pattern.compile(".+"), 55 | new TkRedirect() 56 | ), 57 | new FkFixed(take) 58 | ), 59 | new PsChain( 60 | new PsFake( 61 | Manifests.read("Jare-DynamoKey").startsWith("AAAA") 62 | ), 63 | new PsByFlag( 64 | new PsByFlag.Pair( 65 | PsGithub.class.getSimpleName(), 66 | new PsGithub( 67 | Manifests.read("Jare-GithubId"), 68 | Manifests.read("Jare-GithubSecret") 69 | ) 70 | ), 71 | new PsByFlag.Pair( 72 | PsLogout.class.getSimpleName(), 73 | new PsLogout() 74 | ) 75 | ), 76 | new PsCookie( 77 | new CcSafe( 78 | new CcHex( 79 | new CcAes( 80 | new CcSalted(new CcCompact()), 81 | Manifests.read("Jare-SecurityKey") 82 | ) 83 | ) 84 | ) 85 | ) 86 | ) 87 | ); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/TkInvalidate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 8 | import com.amazonaws.auth.BasicAWSCredentials; 9 | import com.amazonaws.services.cloudfront.AmazonCloudFront; 10 | import com.amazonaws.services.cloudfront.AmazonCloudFrontClientBuilder; 11 | import com.amazonaws.services.cloudfront.model.CreateInvalidationRequest; 12 | import com.amazonaws.services.cloudfront.model.CreateInvalidationResult; 13 | import com.amazonaws.services.cloudfront.model.InvalidationBatch; 14 | import com.amazonaws.services.cloudfront.model.Paths; 15 | import java.io.IOException; 16 | import java.net.URLEncoder; 17 | import java.util.UUID; 18 | import org.takes.Request; 19 | import org.takes.Response; 20 | import org.takes.Take; 21 | import org.takes.facets.flash.RsFlash; 22 | import org.takes.facets.forward.RsForward; 23 | import org.takes.rq.RqHref; 24 | 25 | /** 26 | * Invalidate an URL. 27 | * 28 | * @since 1.0 29 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 30 | */ 31 | final class TkInvalidate implements Take { 32 | 33 | /** 34 | * AWS key. 35 | */ 36 | private final transient String key; 37 | 38 | /** 39 | * AWS secret. 40 | */ 41 | private final transient String secret; 42 | 43 | /** 44 | * Ctor. 45 | * @param akey AWS key 46 | * @param scrt AWS secret 47 | */ 48 | TkInvalidate(final String akey, final String scrt) { 49 | this.key = akey; 50 | this.secret = scrt; 51 | } 52 | 53 | @Override 54 | public Response act(final Request req) throws IOException { 55 | final String url = new RqHref.Base(req).href() 56 | .param("url").iterator().next(); 57 | final String path = String.format( 58 | "/?u=%s", 59 | URLEncoder.encode( 60 | url, 61 | "UTF-8" 62 | ) 63 | ); 64 | final AmazonCloudFront aws = AmazonCloudFrontClientBuilder.standard() 65 | .withCredentials( 66 | new AWSStaticCredentialsProvider( 67 | new BasicAWSCredentials(this.key, this.secret) 68 | ) 69 | ) 70 | .build(); 71 | final CreateInvalidationResult result = aws.createInvalidation( 72 | new CreateInvalidationRequest( 73 | "E2QC66VZY6F0QA", 74 | new InvalidationBatch( 75 | new Paths().withItems(path).withQuantity(1), 76 | UUID.randomUUID().toString() 77 | ) 78 | ) 79 | ); 80 | return new RsForward( 81 | new RsFlash( 82 | String.format( 83 | "URL \"%s\" was invalidated (ID=\"%s\", Status=\"%s\")", 84 | url, 85 | result.getInvalidation().getId(), 86 | result.getInvalidation().getStatus() 87 | ) 88 | ), 89 | "/domains" 90 | ); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/io/jare/dynamo/DyUser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.dynamo; 6 | 7 | import com.amazonaws.services.dynamodbv2.model.Select; 8 | import com.jcabi.dynamo.Attributes; 9 | import com.jcabi.dynamo.Conditions; 10 | import com.jcabi.dynamo.QueryValve; 11 | import com.jcabi.dynamo.Region; 12 | import com.jcabi.dynamo.Table; 13 | import io.jare.model.Domain; 14 | import io.jare.model.User; 15 | import java.io.IOException; 16 | import java.util.Iterator; 17 | import java.util.Locale; 18 | import java.util.stream.Collectors; 19 | import lombok.EqualsAndHashCode; 20 | import lombok.ToString; 21 | 22 | /** 23 | * Dynamo user. 24 | * 25 | * @since 1.0 26 | * @checkstyle MultipleStringLiteralsCheck (500 lines) 27 | */ 28 | @ToString 29 | @EqualsAndHashCode(of = { "region", "handle" }) 30 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") 31 | public final class DyUser implements User { 32 | 33 | /** 34 | * The region to work with. 35 | */ 36 | private final transient Region region; 37 | 38 | /** 39 | * The name of him. 40 | */ 41 | private final transient String handle; 42 | 43 | /** 44 | * Ctor. 45 | * @param reg Region 46 | * @param name Name of him 47 | */ 48 | public DyUser(final Region reg, final String name) { 49 | this.region = reg; 50 | this.handle = name.toLowerCase(Locale.ENGLISH); 51 | } 52 | 53 | @Override 54 | public Iterable mine() { 55 | return this.table() 56 | .frame() 57 | .through( 58 | new QueryValve() 59 | .withSelect(Select.ALL_ATTRIBUTES) 60 | .withLimit(100) 61 | .withConsistentRead(false) 62 | .withIndexName("mine") 63 | ) 64 | .where("user", Conditions.equalTo(this.handle)) 65 | .stream() 66 | .map(DyDomain::new) 67 | .map(Domain.class::cast) 68 | .collect(Collectors.toList()); 69 | } 70 | 71 | @Override 72 | public void add(final String name) throws IOException { 73 | synchronized (this.region) { 74 | final Iterator before = new DyBase(this.region) 75 | .domain(name).iterator(); 76 | if (before.hasNext()) { 77 | final Domain domain = before.next(); 78 | if (!domain.owner().equals(this.handle)) { 79 | throw new IOException( 80 | String.format( 81 | "Domain \"%s\" is occupied by @%s", 82 | domain.name(), domain.owner() 83 | ) 84 | ); 85 | } 86 | } 87 | this.table().put( 88 | new Attributes() 89 | .with("user", this.handle) 90 | .with("domain", name.toLowerCase(Locale.ENGLISH)) 91 | ); 92 | } 93 | } 94 | 95 | /** 96 | * Table to work with. 97 | * @return Table 98 | */ 99 | private Table table() { 100 | return this.region.table("domains"); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/io/jare/dynamo/DyUsageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.dynamo; 6 | 7 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 8 | import com.jcabi.dynamo.Attributes; 9 | import com.jcabi.dynamo.Item; 10 | import com.jcabi.dynamo.Region; 11 | import com.jcabi.dynamo.Table; 12 | import com.jcabi.dynamo.mock.H2Data; 13 | import com.jcabi.dynamo.mock.MkRegion; 14 | import io.jare.model.Usage; 15 | import java.util.Date; 16 | import org.apache.commons.lang3.time.DateUtils; 17 | import org.hamcrest.MatcherAssert; 18 | import org.hamcrest.Matchers; 19 | import org.junit.Test; 20 | 21 | /** 22 | * Test case for {@link DyUsage}. 23 | * @since 0.7 24 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 25 | * @checkstyle MultipleStringLiteralsCheck (500 lines) 26 | */ 27 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") 28 | public final class DyUsageTest { 29 | 30 | /** 31 | * DyUsage can be record usage. 32 | * @throws Exception If some problem inside 33 | */ 34 | @Test 35 | public void recordsUsage() throws Exception { 36 | final Usage usage = new DyUsage(DyUsageTest.item()); 37 | usage.add(new Date(), 1L); 38 | usage.add(new Date(), 1L); 39 | MatcherAssert.assertThat(usage.total(), Matchers.equalTo(2L)); 40 | } 41 | 42 | /** 43 | * DyUsage can be ignore old data. 44 | * @throws Exception If some problem inside 45 | */ 46 | @Test 47 | public void ignoresOldData() throws Exception { 48 | final Usage usage = new DyUsage(DyUsageTest.item()); 49 | usage.add(new Date(), 1L); 50 | usage.add(DateUtils.addDays(new Date(), -50), 1L); 51 | MatcherAssert.assertThat(usage.total(), Matchers.equalTo(1L)); 52 | } 53 | 54 | /** 55 | * DyUsage can be print history. 56 | * @throws Exception If some problem inside 57 | */ 58 | @Test 59 | public void printsHistory() throws Exception { 60 | final Usage usage = new DyUsage(DyUsageTest.item()); 61 | usage.add(new Date(), 1L); 62 | usage.add(new Date(), 1L); 63 | MatcherAssert.assertThat( 64 | usage.history(), 65 | Matchers.hasEntry( 66 | Matchers.any(Date.class), 67 | Matchers.equalTo(2L) 68 | ) 69 | ); 70 | } 71 | 72 | /** 73 | * The item to work with. 74 | * @return Item to work with 75 | * @throws Exception If some problem inside 76 | */ 77 | private static Item item() throws Exception { 78 | final Region region = new MkRegion( 79 | new H2Data().with( 80 | "domains", 81 | new String[] {"domain"}, 82 | "owner", "usage", "total" 83 | ) 84 | ); 85 | final Table table = region.table("domains"); 86 | table.put( 87 | new Attributes() 88 | .with("domain", "yegor256.com") 89 | .with("owner", new AttributeValue("yegor256")) 90 | .with("usage", new AttributeValue("")) 91 | .with("total", new AttributeValue().withN("0")) 92 | ); 93 | return table.frame() 94 | .where("domain", "yegor256.com") 95 | .iterator().next(); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/TkAppFallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import com.jcabi.manifests.Manifests; 8 | import io.sentry.Sentry; 9 | import java.io.IOException; 10 | import java.net.HttpURLConnection; 11 | import org.apache.commons.lang3.exception.ExceptionUtils; 12 | import org.takes.Response; 13 | import org.takes.Take; 14 | import org.takes.facets.fallback.FbChain; 15 | import org.takes.facets.fallback.FbStatus; 16 | import org.takes.facets.fallback.RqFallback; 17 | import org.takes.facets.fallback.TkFallback; 18 | import org.takes.misc.Opt; 19 | import org.takes.rs.RsText; 20 | import org.takes.rs.RsVelocity; 21 | import org.takes.rs.RsWithStatus; 22 | import org.takes.rs.RsWithType; 23 | import org.takes.tk.TkWrap; 24 | 25 | /** 26 | * App with fallback. 27 | * 28 | * @since 1.0 29 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 30 | */ 31 | final class TkAppFallback extends TkWrap { 32 | 33 | /** 34 | * Revision. 35 | */ 36 | private static final String REV = Manifests.read("Jare-Revision"); 37 | 38 | /** 39 | * Ctor. 40 | * @param take Take 41 | */ 42 | TkAppFallback(final Take take) { 43 | super(TkAppFallback.make(take)); 44 | } 45 | 46 | /** 47 | * Authenticated. 48 | * @param take Takes 49 | * @return Authenticated takes 50 | */ 51 | private static Take make(final Take take) { 52 | return new TkFallback( 53 | take, 54 | new FbChain( 55 | new FbStatus( 56 | HttpURLConnection.HTTP_NOT_FOUND, 57 | new RsWithStatus( 58 | new RsText("Page not found"), 59 | HttpURLConnection.HTTP_NOT_FOUND 60 | ) 61 | ), 62 | new FbStatus( 63 | HttpURLConnection.HTTP_BAD_REQUEST, 64 | new RsWithStatus( 65 | new RsText("Bad request"), 66 | HttpURLConnection.HTTP_BAD_REQUEST 67 | ) 68 | ), 69 | req -> { 70 | Sentry.captureException(req.throwable()); 71 | return new Opt.Empty<>(); 72 | }, 73 | req -> new Opt.Single<>(TkAppFallback.fatal(req)) 74 | ) 75 | ); 76 | } 77 | 78 | /** 79 | * Make fatal error page. 80 | * @param req Request 81 | * @return Response 82 | * @throws IOException If fails 83 | */ 84 | private static Response fatal(final RqFallback req) throws IOException { 85 | return new RsWithStatus( 86 | new RsWithType( 87 | new RsVelocity( 88 | TkAppFallback.class.getResource("error.html.vm"), 89 | new RsVelocity.Pair( 90 | "err", 91 | ExceptionUtils.getStackTrace(req.throwable()) 92 | ), 93 | new RsVelocity.Pair("rev", TkAppFallback.REV) 94 | ), 95 | "text/html" 96 | ), 97 | HttpURLConnection.HTTP_INTERNAL_ERROR 98 | ); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/resources/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/test/java/io/jare/tk/TkAppTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import com.jcabi.http.request.JdkRequest; 8 | import com.jcabi.http.response.RestResponse; 9 | import com.jcabi.http.response.XmlResponse; 10 | import com.jcabi.http.wire.VerboseWire; 11 | import com.jcabi.matchers.XhtmlMatchers; 12 | import io.jare.fake.FkBase; 13 | import java.net.HttpURLConnection; 14 | import org.hamcrest.MatcherAssert; 15 | import org.hamcrest.Matchers; 16 | import org.junit.Test; 17 | import org.takes.Take; 18 | import org.takes.http.FtRemote; 19 | import org.takes.rq.RqFake; 20 | import org.takes.rq.RqWithHeader; 21 | import org.takes.rs.RsPrint; 22 | 23 | /** 24 | * Test case for {@link TkApp}. 25 | * @since 1.0 26 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 27 | */ 28 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") 29 | public final class TkAppTest { 30 | 31 | /** 32 | * App can render front page. 33 | * @throws Exception If some problem inside 34 | */ 35 | @Test 36 | public void rendersHomePage() throws Exception { 37 | final Take take = new TkApp(new FkBase()); 38 | MatcherAssert.assertThat( 39 | XhtmlMatchers.xhtml( 40 | new RsPrint( 41 | take.act( 42 | new RqWithHeader( 43 | new RqFake("GET", "/"), 44 | // @checkstyle MultipleStringLiteralsCheck (1 line) 45 | "Accept", 46 | "text/xml" 47 | ) 48 | ) 49 | ).printBody() 50 | ), 51 | XhtmlMatchers.hasXPaths( 52 | "/page/@date", 53 | "/page/@sla", 54 | "/page/millis", 55 | "/page/links/link[@rel='takes:logout']" 56 | ) 57 | ); 58 | } 59 | 60 | /** 61 | * App can render front page. 62 | * @throws Exception If some problem inside 63 | */ 64 | @Test 65 | public void rendersHomePageViaHttp() throws Exception { 66 | final Take app = new TkApp(new FkBase()); 67 | new FtRemote(app).exec( 68 | home -> { 69 | new JdkRequest(home) 70 | .header("Accept", "text/html") 71 | .fetch() 72 | .as(RestResponse.class) 73 | .assertStatus(HttpURLConnection.HTTP_OK) 74 | .as(XmlResponse.class) 75 | .assertXPath("/xhtml:html"); 76 | new JdkRequest(home) 77 | .through(VerboseWire.class) 78 | .header("Accept", "application/xml") 79 | .fetch() 80 | .as(RestResponse.class) 81 | .assertStatus(HttpURLConnection.HTTP_OK) 82 | .as(XmlResponse.class) 83 | .assertXPath("/page/version"); 84 | } 85 | ); 86 | } 87 | 88 | /** 89 | * App can render not found. 90 | * @throws Exception If some problem inside 91 | */ 92 | @Test 93 | public void rendersNotFoundPage() throws Exception { 94 | final Take take = new TkApp(new FkBase()); 95 | MatcherAssert.assertThat( 96 | new RsPrint( 97 | take.act(new RqFake("HEAD", "/not-found")) 98 | ).printBody(), 99 | Matchers.equalTo("Page not found") 100 | ); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/io/jare/tk/TkRelayTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import io.jare.fake.FkBase; 8 | import java.io.UnsupportedEncodingException; 9 | import java.net.URI; 10 | import java.net.URLEncoder; 11 | import java.util.Arrays; 12 | import org.hamcrest.MatcherAssert; 13 | import org.hamcrest.Matchers; 14 | import org.junit.Test; 15 | import org.takes.HttpException; 16 | import org.takes.Request; 17 | import org.takes.Take; 18 | import org.takes.facets.fork.FkRegex; 19 | import org.takes.facets.fork.TkFork; 20 | import org.takes.http.FtRemote; 21 | import org.takes.rq.RqFake; 22 | import org.takes.rq.RqHref; 23 | import org.takes.rs.RsPrint; 24 | import org.takes.rs.RsText; 25 | import org.takes.tk.TkText; 26 | import org.takes.tk.TkWithHeaders; 27 | 28 | /** 29 | * Test case for {@link TkRelay}. 30 | * @since 1.0 31 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 32 | */ 33 | public final class TkRelayTest { 34 | 35 | /** 36 | * TkRelay can send the request through. 37 | * @throws Exception If some problem inside 38 | */ 39 | @Test 40 | public void sendsRequestThroughToHome() throws Exception { 41 | final Take target = new TkFork( 42 | new FkRegex( 43 | "/alpha/.*", 44 | (Take) req -> new RsText( 45 | new RqHref.Base(req).href().toString() 46 | ) 47 | ) 48 | ); 49 | new FtRemote(target).exec( 50 | home -> MatcherAssert.assertThat( 51 | new RsPrint( 52 | new TkRelay(new FkBase()).act( 53 | TkRelayTest.fake( 54 | home, "/alpha/%D0%B4%D0%B0?abc=cde" 55 | ) 56 | ) 57 | ).printBody(), 58 | Matchers.equalTo( 59 | String.format( 60 | "%s/alpha/%%D0%%B4%%D0%%B0?abc=cde", 61 | home 62 | ) 63 | ) 64 | ) 65 | ); 66 | } 67 | 68 | /** 69 | * TkRelay can fail if URL is not valid (space is not allowed). 70 | * @throws Exception If some problem inside 71 | */ 72 | @Test(expected = HttpException.class) 73 | public void catchesInvalidUrls() throws Exception { 74 | new TkRelay(new FkBase()).act( 75 | new RqFake( 76 | Arrays.asList( 77 | "GET /?u=http://www.yegor256.com/a+b", 78 | "Host: 127.0.0.1" 79 | ), 80 | "" 81 | ) 82 | ); 83 | } 84 | 85 | /** 86 | * TkRelay can set cache headers to "forever". 87 | * @throws Exception If some problem inside 88 | */ 89 | @Test 90 | public void setsCachingHeaders() throws Exception { 91 | final Take target = new TkWithHeaders( 92 | new TkText("cacheable forever"), 93 | "age: 600", 94 | "cache-control: max-age=600", 95 | "expires: Thu, 08 Dec 2016 22:51:37 GMT" 96 | ); 97 | new FtRemote(target).exec( 98 | home -> MatcherAssert.assertThat( 99 | new RsPrint( 100 | new TkRelay(new FkBase()).act( 101 | TkRelayTest.fake(home, "/&whatever") 102 | ) 103 | ).print(), 104 | Matchers.allOf( 105 | Matchers.containsString("Age: 31536000"), 106 | Matchers.containsString("Cache-Control: max-age=31536000"), 107 | Matchers.containsString("Expires: Sun, 19 Jul 2020 18:06:32 GMT"), 108 | Matchers.not(Matchers.containsString("Cache-Control: max-age=600")) 109 | ) 110 | ) 111 | ); 112 | } 113 | 114 | /** 115 | * Fake request. 116 | * @param home Base URI 117 | * @param path Path 118 | * @return Request 119 | * @throws UnsupportedEncodingException If fails 120 | */ 121 | private static Request fake(final URI home, final String path) 122 | throws UnsupportedEncodingException { 123 | return new RqFake( 124 | Arrays.asList( 125 | String.format( 126 | "GET /?u=%s", 127 | URLEncoder.encode( 128 | home.resolve(path).toString(), 129 | "UTF-8" 130 | ) 131 | ), 132 | "Host: localhost" 133 | ), 134 | "" 135 | ); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/RsPage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import com.jcabi.manifests.Manifests; 8 | import java.io.IOException; 9 | import java.util.Arrays; 10 | import lombok.EqualsAndHashCode; 11 | import org.takes.Request; 12 | import org.takes.Response; 13 | import org.takes.facets.auth.Identity; 14 | import org.takes.facets.auth.RqAuth; 15 | import org.takes.facets.auth.XeIdentity; 16 | import org.takes.facets.auth.XeLogoutLink; 17 | import org.takes.facets.auth.social.XeGithubLink; 18 | import org.takes.facets.flash.XeFlash; 19 | import org.takes.facets.fork.FkTypes; 20 | import org.takes.facets.fork.RsFork; 21 | import org.takes.rs.RsPrettyXml; 22 | import org.takes.rs.RsWithType; 23 | import org.takes.rs.RsWrap; 24 | import org.takes.rs.RsXslt; 25 | import org.takes.rs.xe.RsXembly; 26 | import org.takes.rs.xe.XeAppend; 27 | import org.takes.rs.xe.XeChain; 28 | import org.takes.rs.xe.XeDate; 29 | import org.takes.rs.xe.XeLink; 30 | import org.takes.rs.xe.XeLinkHome; 31 | import org.takes.rs.xe.XeLinkSelf; 32 | import org.takes.rs.xe.XeLocalhost; 33 | import org.takes.rs.xe.XeMillis; 34 | import org.takes.rs.xe.XeSla; 35 | import org.takes.rs.xe.XeSource; 36 | import org.takes.rs.xe.XeStylesheet; 37 | import org.takes.rs.xe.XeWhen; 38 | 39 | /** 40 | * Index resource, front page of the website. 41 | * 42 | * @since 1.0 43 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 44 | */ 45 | @EqualsAndHashCode(callSuper = true) 46 | @SuppressWarnings("PMD.ExcessiveImports") 47 | public final class RsPage extends RsWrap { 48 | 49 | /** 50 | * Ctor. 51 | * @param xsl XSL 52 | * @param req Request 53 | * @param src Source 54 | * @throws IOException If fails 55 | */ 56 | public RsPage(final String xsl, final Request req, final XeSource... src) 57 | throws IOException { 58 | this(xsl, req, Arrays.asList(src)); 59 | } 60 | 61 | /** 62 | * Ctor. 63 | * @param xsl XSL 64 | * @param req Request 65 | * @param src Source 66 | * @throws IOException If fails 67 | */ 68 | public RsPage(final String xsl, final Request req, 69 | final Iterable src) 70 | throws IOException { 71 | super(RsPage.make(xsl, req, src)); 72 | } 73 | 74 | /** 75 | * Make it. 76 | * @param xsl XSL 77 | * @param req Request 78 | * @param src Source 79 | * @return Response 80 | * @throws IOException If fails 81 | */ 82 | private static Response make(final String xsl, final Request req, 83 | final Iterable src) throws IOException { 84 | final Response raw = new RsXembly( 85 | new XeStylesheet(xsl), 86 | new XeAppend( 87 | "page", 88 | new XeMillis(false), 89 | new XeChain(src), 90 | new XeLinkHome(req), 91 | new XeLinkSelf(req), 92 | new XeMillis(true), 93 | new XeDate(), 94 | new XeSla(), 95 | new XeLocalhost(), 96 | new XeFlash(req), 97 | new XeWhen( 98 | new RqAuth(req).identity().equals(Identity.ANONYMOUS), 99 | new XeChain( 100 | new XeGithubLink(req, Manifests.read("Jare-GithubId")) 101 | ) 102 | ), 103 | new XeWhen( 104 | !new RqAuth(req).identity().equals(Identity.ANONYMOUS), 105 | new XeChain( 106 | new XeIdentity(req), 107 | new XeLogoutLink(req), 108 | new XeLink("domains", "/domains") 109 | ) 110 | ), 111 | new XeAppend( 112 | "version", 113 | new XeAppend("name", Manifests.read("Jare-Version")), 114 | new XeAppend("revision", Manifests.read("Jare-Revision")), 115 | new XeAppend("date", Manifests.read("Jare-Date")), 116 | new XeAppend("heroku", RsPage.heroku()) 117 | ) 118 | ) 119 | ); 120 | return new RsFork( 121 | req, 122 | new FkTypes( 123 | "application/xml,text/xml", 124 | new RsPrettyXml(new RsWithType(raw, "text/xml")) 125 | ), 126 | new FkTypes( 127 | "*/*", 128 | new RsXslt(new RsWithType(raw, "text/html")) 129 | ) 130 | ); 131 | } 132 | 133 | /** 134 | * Current Heroku release version. 135 | * @return The version 136 | */ 137 | private static String heroku() { 138 | String ver = System.getenv("HEROKU_RELEASE_VERSION"); 139 | if (ver == null) { 140 | ver = "?"; 141 | } 142 | return ver; 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/TkRelay.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import io.jare.model.Base; 8 | import io.jare.model.Domain; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.net.HttpURLConnection; 12 | import java.net.URI; 13 | import java.util.Collections; 14 | import java.util.Iterator; 15 | import java.util.Locale; 16 | import java.util.regex.Pattern; 17 | import org.cactoos.iterable.Joined; 18 | import org.cactoos.iterable.Skipped; 19 | import org.takes.HttpException; 20 | import org.takes.Request; 21 | import org.takes.Response; 22 | import org.takes.Take; 23 | import org.takes.rq.RqHref; 24 | import org.takes.rs.RsWithHeaders; 25 | import org.takes.rs.RsWithoutHeader; 26 | import org.takes.tk.TkProxy; 27 | 28 | /** 29 | * Relay. 30 | * 31 | * @since 1.0 32 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 33 | */ 34 | final class TkRelay implements Take { 35 | 36 | /** 37 | * Validation pattern for destination URLs. 38 | * @link https://tools.ietf.org/html/rfc3986 39 | */ 40 | private static final Pattern PTN = Pattern.compile( 41 | "[A-Za-z0-9-%._~:/\\?#@!\\$&'\\(\\)\\*\\+,;=`\\[\\]]+" 42 | ); 43 | 44 | /** 45 | * Base. 46 | */ 47 | private final transient Base base; 48 | 49 | /** 50 | * Ctor. 51 | * @param bse Base 52 | */ 53 | TkRelay(final Base bse) { 54 | this.base = bse; 55 | } 56 | 57 | @Override 58 | public Response act(final Request req) throws Exception { 59 | final Iterator param = new RqHref.Base(req).href() 60 | .param("u").iterator(); 61 | if (!param.hasNext()) { 62 | throw new HttpException( 63 | HttpURLConnection.HTTP_BAD_REQUEST, 64 | String.format( 65 | "Parameter \"u\" is mandatory in %s", 66 | new RqHref.Base(req).href() 67 | ) 68 | ); 69 | } 70 | final String target = param.next().trim(); 71 | if (!TkRelay.PTN.matcher(target).matches()) { 72 | throw new HttpException( 73 | HttpURLConnection.HTTP_BAD_REQUEST, 74 | String.format( 75 | "Target URL \"%s\" is not compliant with RFC3986", 76 | target 77 | ) 78 | ); 79 | } 80 | final URI uri = URI.create(target); 81 | final String host = uri.getHost().toLowerCase(Locale.ENGLISH); 82 | final Iterator domains = this.base.domain(host).iterator(); 83 | if (!domains.hasNext()) { 84 | throw new HttpException( 85 | HttpURLConnection.HTTP_BAD_REQUEST, 86 | String.format( 87 | // @checkstyle LineLength (1 line) 88 | "Domain \"%s\" is not registered, check your account at www.jare.io", 89 | host 90 | ) 91 | ); 92 | } 93 | final Domain domain = domains.next(); 94 | return TkRelay.cached( 95 | new RsWithHeaders( 96 | new TkProxy(uri.toString()).act( 97 | TkRelay.request( 98 | req, 99 | new Destination(uri).path() 100 | ) 101 | ), 102 | String.format("X-Jare-Target: %s", uri), 103 | String.format("X-Jare-Usage: %d", domain.usage().total()) 104 | ) 105 | ); 106 | } 107 | 108 | /** 109 | * Response that is cached forever. 110 | * @param rsp Response 111 | * @return New response 112 | */ 113 | private static Response cached(final Response rsp) { 114 | return new RsWithHeaders( 115 | new RsWithoutHeader( 116 | new RsWithoutHeader( 117 | new RsWithoutHeader(rsp, "Age"), 118 | "Expires" 119 | ), 120 | "Cache-Control" 121 | ), 122 | "Age: 31536000", 123 | "Cache-Control: max-age=31536000", 124 | "Expires: Sun, 19 Jul 2020 18:06:32 GMT" 125 | ); 126 | } 127 | 128 | /** 129 | * The request to send. 130 | * @param req Original request 131 | * @param path Destination path 132 | * @return Request 133 | */ 134 | private static Request request(final Request req, final String path) { 135 | return new Request() { 136 | @Override 137 | public Iterable head() throws IOException { 138 | return new Joined( 139 | Collections.singleton( 140 | String.format( 141 | "GET %s HTTP/1.1", 142 | path 143 | ) 144 | ), 145 | new Skipped<>(1, req.head()) 146 | ); 147 | } 148 | 149 | @Override 150 | public InputStream body() throws IOException { 151 | return req.body(); 152 | } 153 | }; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/io/jare/dynamo/DyUsage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.dynamo; 6 | 7 | import com.amazonaws.services.dynamodbv2.model.AttributeAction; 8 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 9 | import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; 10 | import com.jcabi.dynamo.Item; 11 | import com.jcabi.xml.XML; 12 | import com.jcabi.xml.XMLDocument; 13 | import io.jare.model.Usage; 14 | import java.io.IOException; 15 | import java.text.DateFormat; 16 | import java.text.ParseException; 17 | import java.text.SimpleDateFormat; 18 | import java.util.Date; 19 | import java.util.List; 20 | import java.util.Locale; 21 | import java.util.SortedMap; 22 | import java.util.TimeZone; 23 | import java.util.TreeMap; 24 | import lombok.EqualsAndHashCode; 25 | import lombok.ToString; 26 | import org.apache.commons.lang3.time.DateUtils; 27 | import org.w3c.dom.Node; 28 | import org.xembly.Directives; 29 | import org.xembly.Xembler; 30 | 31 | /** 32 | * Dynamo usage. 33 | * 34 | * @since 0.7 35 | * @checkstyle MultipleStringLiteralsCheck (500 lines) 36 | */ 37 | @ToString 38 | @EqualsAndHashCode(of = "item") 39 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") 40 | public final class DyUsage implements Usage { 41 | 42 | /** 43 | * The item. 44 | */ 45 | private final transient Item item; 46 | 47 | /** 48 | * Ctor. 49 | * @param itm Item 50 | */ 51 | public DyUsage(final Item itm) { 52 | this.item = itm; 53 | } 54 | 55 | @Override 56 | public void add(final Date date, final long bytes) throws IOException { 57 | final int day = DyUsage.asNumber(date); 58 | final XML xml = this.xml(); 59 | final String xpath = String.format("/usage/day[@id='%d']/text()", day); 60 | final List items = xml.xpath(xpath); 61 | final long before; 62 | if (items.isEmpty()) { 63 | before = 0L; 64 | } else { 65 | before = Long.parseLong(items.get(0)); 66 | } 67 | final Node node = xml.node(); 68 | new Xembler( 69 | new Directives() 70 | .xpath(xpath).up().remove() 71 | .xpath("/usage").add("day").attr("id", day).set(before + bytes) 72 | ).applyQuietly(node); 73 | new Xembler( 74 | new Directives().xpath( 75 | String.format( 76 | "/usage/day[number(@id) < %d]", 77 | DyUsage.asNumber(DateUtils.addDays(new Date(), -30)) 78 | ) 79 | ).remove() 80 | ).applyQuietly(node); 81 | final XML after = new XMLDocument(node); 82 | this.save(after.toString()); 83 | this.save(Long.parseLong(after.xpath("sum(/usage/day)").get(0))); 84 | } 85 | 86 | @Override 87 | public long total() throws IOException { 88 | if (!this.item.has("total")) { 89 | this.save(0L); 90 | } 91 | return Long.parseLong(this.item.get("total").getN()); 92 | } 93 | 94 | @Override 95 | public SortedMap history() throws IOException { 96 | final SortedMap map = new TreeMap<>(); 97 | for (final XML day : this.xml().nodes("/usage/day")) { 98 | map.put( 99 | DyUsage.asDate(Integer.parseInt(day.xpath("@id").get(0))), 100 | Long.parseLong(day.xpath("text()").get(0)) 101 | ); 102 | } 103 | return map; 104 | } 105 | 106 | /** 107 | * Load XML. 108 | * @return The XML with usage 109 | * @throws IOException If fails 110 | */ 111 | private XML xml() throws IOException { 112 | if (!this.item.has("usage")) { 113 | this.save(""); 114 | } 115 | return new XMLDocument(this.item.get("usage").getS()); 116 | } 117 | 118 | /** 119 | * Save XML. 120 | * @param xml The XML to save 121 | * @throws IOException If fails 122 | */ 123 | private void save(final String xml) throws IOException { 124 | this.item.put( 125 | "usage", 126 | new AttributeValueUpdate() 127 | .withValue(new AttributeValue().withS(xml)) 128 | .withAction(AttributeAction.PUT) 129 | ); 130 | } 131 | 132 | /** 133 | * Save total. 134 | * @param total Total usage 135 | * @throws IOException If fails 136 | */ 137 | private void save(final long total) throws IOException { 138 | this.item.put( 139 | "total", 140 | new AttributeValueUpdate() 141 | .withValue(new AttributeValue().withN(Long.toString(total))) 142 | .withAction(AttributeAction.PUT) 143 | ); 144 | } 145 | 146 | /** 147 | * Convert date to number. 148 | * @param date The date 149 | * @return A number 150 | */ 151 | private static int asNumber(final Date date) { 152 | final TimeZone zone = TimeZone.getTimeZone("UTC"); 153 | final DateFormat fmt = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH); 154 | fmt.setTimeZone(zone); 155 | return Integer.parseInt(fmt.format(date)); 156 | } 157 | 158 | /** 159 | * Convert number to date. 160 | * @param num The number 161 | * @return A date 162 | */ 163 | private static Date asDate(final int num) { 164 | final TimeZone zone = TimeZone.getTimeZone("UTC"); 165 | final DateFormat fmt = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH); 166 | fmt.setTimeZone(zone); 167 | try { 168 | return fmt.parse(Integer.toString(num)); 169 | } catch (final ParseException ex) { 170 | throw new IllegalStateException(ex); 171 | } 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /src/main/resources/xsl/layout.xsl: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 |
    20 | 29 | 58 | 59 |
    60 |
    61 | 62 |
    63 |
    64 | 81 | 90 | 99 |
    100 |
    101 | 109 | 110 | 111 |
    112 | 113 |

    114 | 115 | color: 116 | 117 | 118 | #348C62 119 | 120 | 121 | orange 122 | 123 | 124 | red 125 | 126 | 127 | inherit 128 | 129 | 130 | 131 | 132 |

    133 |
    134 | 135 | 136 | 137 | 138 | ? 139 | 140 | 141 | 142 | min 143 | 144 | 145 | 146 | s 147 | 148 | 149 | 150 | ms 151 | 152 | 153 | 154 |
    155 | -------------------------------------------------------------------------------- /src/main/java/io/jare/Logs.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare; 6 | 7 | import com.jcabi.aspects.ScheduleWithFixedDelay; 8 | import com.jcabi.log.Logger; 9 | import com.jcabi.s3.Bucket; 10 | import com.jcabi.s3.Ocket; 11 | import io.jare.model.Base; 12 | import io.jare.model.Domain; 13 | import java.io.BufferedReader; 14 | import java.io.IOException; 15 | import java.io.InputStreamReader; 16 | import java.net.URI; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.text.DateFormat; 20 | import java.text.ParseException; 21 | import java.text.SimpleDateFormat; 22 | import java.util.Date; 23 | import java.util.HashMap; 24 | import java.util.Iterator; 25 | import java.util.Locale; 26 | import java.util.Map; 27 | import java.util.TimeZone; 28 | import java.util.concurrent.TimeUnit; 29 | import java.util.regex.Matcher; 30 | import java.util.regex.Pattern; 31 | import java.util.zip.GZIPInputStream; 32 | import org.apache.commons.lang3.StringUtils; 33 | 34 | /** 35 | * Logs in S3. 36 | * 37 | * @since 0.7 38 | */ 39 | @ScheduleWithFixedDelay(delay = 1, unit = TimeUnit.MINUTES) 40 | final class Logs implements Runnable { 41 | 42 | /** 43 | * Parser of one line. 44 | */ 45 | private static final Pattern PTN = Pattern.compile( 46 | StringUtils.join( 47 | "(\\d{4}-\\d{2}-\\d{2})\t", 48 | "\\d{2}:\\d{2}:\\d{2}\t", 49 | "[A-Z0-9]+\t", 50 | "(\\d+)\t", 51 | "\\d+\\.\\d+\\.\\d+\\.\\d+\t", 52 | "GET\t", 53 | "[a-z0-9]+.cloudfront.net\t", 54 | "/\t", 55 | "\\d{3}\t", 56 | "(https?://[^\t]+)\t" 57 | ) 58 | ); 59 | 60 | /** 61 | * Base. 62 | */ 63 | private final transient Base base; 64 | 65 | /** 66 | * Bucket. 67 | */ 68 | private final transient Bucket bucket; 69 | 70 | /** 71 | * Ctor. 72 | * @param bse Base 73 | * @param bkt Bucket 74 | */ 75 | Logs(final Base bse, final Bucket bkt) { 76 | this.base = bse; 77 | this.bucket = bkt; 78 | } 79 | 80 | @Override 81 | public void run() { 82 | try { 83 | final Iterator ockets = this.bucket.list("").iterator(); 84 | if (ockets.hasNext()) { 85 | final String name = ockets.next(); 86 | final long bytes = this.process(name); 87 | this.bucket.remove(name); 88 | Logger.info(this, "ocket %s processed: %d bytes", name, bytes); 89 | } 90 | } catch (final IOException ex) { 91 | throw new IllegalStateException(ex); 92 | } 93 | } 94 | 95 | /** 96 | * Process one ocket. 97 | * @param name The name of the ocket 98 | * @return Bytes in total 99 | * @throws IOException If fails 100 | */ 101 | private long process(final String name) throws IOException { 102 | final Ocket ocket = this.bucket.ocket(name); 103 | final Path path = Files.createTempFile("jare", ".gz"); 104 | ocket.read(Files.newOutputStream(path)); 105 | final BufferedReader input = new BufferedReader( 106 | new InputStreamReader( 107 | new GZIPInputStream( 108 | Files.newInputStream(path) 109 | ) 110 | ) 111 | ); 112 | final Map> map = new HashMap<>(0); 113 | try { 114 | while (true) { 115 | final String line = input.readLine(); 116 | if (line == null) { 117 | break; 118 | } 119 | Logs.parse(map, line); 120 | } 121 | } finally { 122 | input.close(); 123 | } 124 | long total = 0L; 125 | for (final Map.Entry> entry : map.entrySet()) { 126 | for (final Map.Entry usg 127 | : entry.getValue().entrySet()) { 128 | final Iterator domains = 129 | this.base.domain(entry.getKey()).iterator(); 130 | if (domains.hasNext()) { 131 | domains.next().usage().add( 132 | usg.getKey(), 133 | usg.getValue() 134 | ); 135 | } 136 | total += usg.getValue(); 137 | } 138 | } 139 | return total; 140 | } 141 | 142 | /** 143 | * Parse one line. 144 | * @param map Map to populate 145 | * @param line The line 146 | */ 147 | private static void parse(final Map> map, 148 | final CharSequence line) { 149 | final Matcher matcher = Logs.PTN.matcher(line); 150 | if (matcher.find()) { 151 | final String domain = URI.create(matcher.group(3)).getHost(); 152 | if (!map.containsKey(domain)) { 153 | map.put(domain, new HashMap<>(0)); 154 | } 155 | final Map target = map.get(domain); 156 | final Date date = Logs.asDate(matcher.group(1)); 157 | if (!target.containsKey(date)) { 158 | target.put(date, 0L); 159 | } 160 | target.put( 161 | date, 162 | target.get(date) + Long.parseLong(matcher.group(2)) 163 | ); 164 | } 165 | } 166 | 167 | /** 168 | * Convert text to date. 169 | * @param txt Text 170 | * @return A date 171 | */ 172 | private static Date asDate(final String txt) { 173 | final TimeZone zone = TimeZone.getTimeZone("UTC"); 174 | final DateFormat fmt = new SimpleDateFormat( 175 | "yyyy-MM-dd", Locale.ENGLISH 176 | ); 177 | fmt.setTimeZone(zone); 178 | try { 179 | return fmt.parse(txt); 180 | } catch (final ParseException ex) { 181 | throw new IllegalStateException(ex); 182 | } 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/main/resources/xsl/index.xsl: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <xsl:text>Jare</xsl:text> 13 | 14 | 15 | 16 | 17 |

    18 | If you know what 19 | 20 | CDN 21 | 22 | ( 23 | 24 | Content Delivery Network 25 | 26 | ) is, 27 | but don't want to spend time and money 28 | to employ a full-scale solution like 29 | Akamai or CloudFront, 30 | this system is the right choice for you. 31 |

    32 |

    33 | Say, you have something like this in your HTML: 34 |

    35 |
     36 |       <img src="http://google.com/logo.gif"/>
     37 |     
    38 |

    39 | Just change the URL and this 40 | 41 | logo.gif 42 | 43 | will be delivered through 44 | 45 | AWS CloudFront 46 | 47 | delivery servers, 48 | for free: 49 |

    50 |
     51 |       <img src="http://cf.jare.io/?u=http://google.com/logo.gif"/>
     52 |     
    53 |

    54 | SSL is supported, you can use either 55 | 56 | http://cf.jare.io 57 | 58 | or 59 | 60 | https://cf.jare.io 61 | 62 | . 63 |

    64 |

    65 | Well, there is one more thing you should do. 66 | You should login using your GitHub account and 67 | register your domain with us ( 68 | 69 | google.com 70 | 71 | in this example). 72 | We want to know who you are, mostly in order to track usage. 73 | By the way, if you want to know how it all works, read this blog post: 74 | 75 | Jare.io, an Instant and Free CDN 76 | 77 | by 78 | 79 | @yegor256 80 | 81 | . 82 |

    83 |

    84 | Keep in mind that we 85 | 86 | explicitly set 87 | 88 | all HTTP caching headers to "cache forever" values. 89 | Thus, the only way to modify resources is to change their URLs/names. 90 |

    91 | 92 |

    93 | There are 94 | 95 | 96 | domains 97 | 98 | registered now. 99 | This is the list of 100 | 101 | most active of them. 102 | The amount of Mb is calculated over the last month. 103 | If your traffic is bigger than 50Gb/mo, you are not welcome here: 104 | please, use some other CDN. 105 |

    106 | 107 |

    108 | Total traffic over the last month: 109 | 110 | 111 | Gb 112 | 113 | . 114 | AWS charges all of us together 115 | 116 | approximately 117 | 118 | $ 119 | 120 | monthly. 121 |

    122 |

    123 | It's an open source system. 124 | If you want to contribute, 125 | 126 | please do 127 | 128 | . 129 |

    130 |
    131 | 132 |

    133 | There are no registered domains yet. 134 |

    135 |
    136 |
    137 | 138 |
      139 | 140 | 141 | 142 | 143 | 144 | 145 |
    146 |
    147 | 148 |
  • 149 | 150 | by 151 | 152 | @ 153 | 154 | 155 | : 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | Mb 166 |
  • 167 |
    168 |
    169 | -------------------------------------------------------------------------------- /nginx.conf.sigil: -------------------------------------------------------------------------------- 1 | {{ range $port_map := .PROXY_PORT_MAP | split " " }} 2 | {{ $port_map_list := $port_map | split ":" }} 3 | {{ $scheme := index $port_map_list 0 }} 4 | {{ $listen_port := index $port_map_list 1 }} 5 | {{ $upstream_port := index $port_map_list 2 }} 6 | 7 | {{ if eq $scheme "http" }} 8 | server { 9 | listen [::]:{{ $listen_port }}; 10 | listen {{ $listen_port }}; 11 | {{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }} 12 | access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log; 13 | error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log; 14 | location / { 15 | 16 | gzip on; 17 | gzip_min_length 1100; 18 | gzip_buffers 4 32k; 19 | gzip_types text/css text/javascript text/xml text/plain text/x-component application/javascript application/x-javascript application/json application/xml application/rss+xml font/truetype application/x-font-ttf font/opentype application/vnd.ms-fontobject image/svg+xml; 20 | gzip_vary on; 21 | gzip_comp_level 6; 22 | 23 | proxy_pass http://{{ $.APP }}-{{ $upstream_port }}; 24 | proxy_http_version 1.1; 25 | proxy_set_header Upgrade $http_upgrade; 26 | proxy_set_header Connection $http_connection; 27 | proxy_set_header Host $http_host; 28 | proxy_set_header X-Forwarded-Proto $scheme; 29 | proxy_set_header X-Forwarded-For $remote_addr; 30 | proxy_set_header X-Forwarded-Port $server_port; 31 | proxy_set_header X-Request-Start $msec; 32 | } 33 | include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf; 34 | 35 | error_page 400 401 402 403 405 406 407 408 409 410 411 412 413 414 415 416 417 418 420 422 423 424 426 428 429 431 444 449 450 451 /400-error.html; 36 | location /400-error.html { 37 | root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors; 38 | internal; 39 | } 40 | 41 | error_page 404 /404-error.html; 42 | location /404-error.html { 43 | root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors; 44 | internal; 45 | } 46 | 47 | error_page 500 501 502 503 504 505 506 507 508 509 510 511 /500-error.html; 48 | location /500-error.html { 49 | root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors; 50 | internal; 51 | } 52 | } 53 | {{ else if eq $scheme "https"}} 54 | server { 55 | listen [::]:{{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }}; 56 | listen {{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }}; 57 | {{ if $.SSL_SERVER_NAME }}server_name {{ $.SSL_SERVER_NAME }}; {{ end }} 58 | {{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }} 59 | access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log; 60 | error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log; 61 | 62 | ssl_certificate {{ $.APP_SSL_PATH }}/server.crt; 63 | ssl_certificate_key {{ $.APP_SSL_PATH }}/server.key; 64 | ssl_protocols TLSv1.2 {{ if eq $.TLS13_SUPPORTED "true" }}TLSv1.3{{ end }}; 65 | ssl_prefer_server_ciphers off; 66 | 67 | keepalive_timeout 70; 68 | {{ if and (eq $.SPDY_SUPPORTED "true") (ne $.HTTP2_SUPPORTED "true") }}add_header Alternate-Protocol {{ $.PROXY_SSL_PORT }}:npn-spdy/2;{{ end }} 69 | 70 | location / { 71 | 72 | gzip on; 73 | gzip_min_length 1100; 74 | gzip_buffers 4 32k; 75 | gzip_types text/css text/javascript text/xml text/plain text/x-component application/javascript application/x-javascript application/json application/xml application/rss+xml font/truetype application/x-font-ttf font/opentype application/vnd.ms-fontobject image/svg+xml; 76 | gzip_vary on; 77 | gzip_comp_level 6; 78 | 79 | proxy_pass http://{{ $.APP }}-{{ $upstream_port }}; 80 | {{ if eq $.HTTP2_PUSH_SUPPORTED "true" }}http2_push_preload on; {{ end }} 81 | proxy_http_version 1.1; 82 | proxy_set_header Upgrade $http_upgrade; 83 | proxy_set_header Connection $http_connection; 84 | proxy_set_header Host $http_host; 85 | proxy_set_header X-Forwarded-Proto $scheme; 86 | proxy_set_header X-Forwarded-For $remote_addr; 87 | proxy_set_header X-Forwarded-Port $server_port; 88 | proxy_set_header X-Request-Start $msec; 89 | } 90 | include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf; 91 | 92 | error_page 400 401 402 403 405 406 407 408 409 410 411 412 413 414 415 416 417 418 420 422 423 424 426 428 429 431 444 449 450 451 /400-error.html; 93 | location /400-error.html { 94 | root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors; 95 | internal; 96 | } 97 | 98 | error_page 404 /404-error.html; 99 | location /404-error.html { 100 | root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors; 101 | internal; 102 | } 103 | 104 | error_page 500 501 503 504 505 506 507 508 509 510 511 /500-error.html; 105 | location /500-error.html { 106 | root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors; 107 | internal; 108 | } 109 | 110 | error_page 502 /502-error.html; 111 | location /502-error.html { 112 | root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors; 113 | internal; 114 | } 115 | } 116 | {{ else if eq $scheme "grpc"}} 117 | {{ if eq $.GRPC_SUPPORTED "true"}}{{ if eq $.HTTP2_SUPPORTED "true"}} 118 | server { 119 | listen [::]:{{ $listen_port }} http2; 120 | listen {{ $listen_port }} http2; 121 | {{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }} 122 | access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log; 123 | error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log; 124 | location / { 125 | grpc_pass grpc://{{ $.APP }}-{{ $upstream_port }}; 126 | } 127 | include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf; 128 | } 129 | {{ end }}{{ end }} 130 | {{ else if eq $scheme "grpcs"}} 131 | {{ if eq $.GRPC_SUPPORTED "true"}}{{ if eq $.HTTP2_SUPPORTED "true"}} 132 | server { 133 | listen [::]:{{ $listen_port }} ssl http2; 134 | listen {{ $listen_port }} ssl http2; 135 | {{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }} 136 | access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log; 137 | error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log; 138 | 139 | ssl_certificate {{ $.APP_SSL_PATH }}/server.crt; 140 | ssl_certificate_key {{ $.APP_SSL_PATH }}/server.key; 141 | ssl_protocols TLSv1.2 {{ if eq $.TLS13_SUPPORTED "true" }}TLSv1.3{{ end }}; 142 | ssl_prefer_server_ciphers off; 143 | 144 | location / { 145 | grpc_pass grpc://{{ $.APP }}-{{ $upstream_port }}; 146 | } 147 | include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf; 148 | } 149 | {{ end }}{{ end }} 150 | {{ end }} 151 | {{ end }} 152 | 153 | {{ if $.DOKKU_APP_LISTENERS }} 154 | {{ range $upstream_port := $.PROXY_UPSTREAM_PORTS | split " " }} 155 | upstream {{ $.APP }}-{{ $upstream_port }} { 156 | {{ range $listeners := $.DOKKU_APP_LISTENERS | split " " }} 157 | {{ $listener_list := $listeners | split ":" }} 158 | {{ $listener_ip := index $listener_list 0 }} 159 | server {{ $listener_ip }}:{{ $upstream_port }};{{ end }} 160 | } 161 | {{ end }}{{ end }} 162 | -------------------------------------------------------------------------------- /src/main/java/io/jare/tk/TkApp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Yegor Bugayenko 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | package io.jare.tk; 6 | 7 | import com.jcabi.manifests.Manifests; 8 | import io.jare.model.Base; 9 | import java.io.IOException; 10 | import org.apache.commons.lang3.exception.ExceptionUtils; 11 | import org.takes.facets.auth.TkSecure; 12 | import org.takes.facets.fallback.TkFallback; 13 | import org.takes.facets.flash.TkFlash; 14 | import org.takes.facets.fork.FkAuthenticated; 15 | import org.takes.facets.fork.FkFixed; 16 | import org.takes.facets.fork.FkHost; 17 | import org.takes.facets.fork.FkRegex; 18 | import org.takes.facets.fork.TkFork; 19 | import org.takes.facets.fork.TkMethods; 20 | import org.takes.facets.forward.TkForward; 21 | import org.takes.misc.Opt; 22 | import org.takes.rs.RsWithBody; 23 | import org.takes.rs.RsWithStatus; 24 | import org.takes.rs.RsWithType; 25 | import org.takes.tk.TkGzip; 26 | import org.takes.tk.TkMeasured; 27 | import org.takes.tk.TkVersioned; 28 | import org.takes.tk.TkWithHeaders; 29 | import org.takes.tk.TkWithType; 30 | import org.takes.tk.TkWrap; 31 | 32 | /** 33 | * App. 34 | * 35 | * @since 1.0 36 | * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) 37 | * @checkstyle MultipleStringLiteralsCheck (500 lines) 38 | * @checkstyle ClassFanOutComplexityCheck (500 lines) 39 | * @checkstyle LineLength (500 lines) 40 | */ 41 | @SuppressWarnings( 42 | { 43 | "PMD.ExcessiveImports", "PMD.ExcessiveMethodLength", 44 | "PMD.AvoidDuplicateLiterals" 45 | } 46 | ) 47 | public final class TkApp extends TkWrap { 48 | 49 | /** 50 | * Ctor. 51 | * @param base Base 52 | * @throws IOException If fails 53 | */ 54 | public TkApp(final Base base) throws IOException { 55 | super( 56 | new TkWithHeaders( 57 | new TkVersioned( 58 | new TkMeasured( 59 | new TkFlash( 60 | new TkAppFallback( 61 | new TkAppAuth( 62 | new TkForward( 63 | new TkFork( 64 | new FkHost( 65 | "relay.jare.io", 66 | new TkFallback( 67 | new TkRelay(base), 68 | req -> new Opt.Single<>( 69 | new RsWithType( 70 | new RsWithBody( 71 | new RsWithStatus(req.code()), 72 | String.format( 73 | "Please, submit this stacktrace to GitHub and we'll try to help: https://github.com/yegor256/jare/issues\n\n%s", 74 | ExceptionUtils.getStackTrace( 75 | req.throwable() 76 | ) 77 | ) 78 | ), 79 | "text/plain" 80 | ) 81 | ) 82 | ) 83 | ), 84 | new FkFixed( 85 | new TkGzip( 86 | new TkFork( 87 | new FkRegex("/robots.txt", ""), 88 | new FkRegex( 89 | "/xsl/[a-z\\-]+\\.xsl", 90 | new TkWithType( 91 | new TkRefresh("./src/main/xsl"), 92 | "text/xsl" 93 | ) 94 | ), 95 | new FkRegex( 96 | "/css/[a-z]+\\.css", 97 | new TkWithType( 98 | new TkRefresh("./src/main/scss"), 99 | "text/css" 100 | ) 101 | ), 102 | new FkRegex( 103 | "/images/[a-z]+\\.svg", 104 | new TkWithType( 105 | new TkRefresh("./src/main/resources"), 106 | "image/svg+xml" 107 | ) 108 | ), 109 | new FkRegex( 110 | "/images/[a-z]+\\.png", 111 | new TkWithType( 112 | new TkRefresh("./src/main/resources"), 113 | "image/png" 114 | ) 115 | ), 116 | new FkRegex("/", new TkIndex(base)), 117 | new FkRegex( 118 | "/invalidate", 119 | new TkInvalidate( 120 | Manifests.read("Jare-CloudFrontKey"), 121 | Manifests.read("Jare-CloudFrontSecret") 122 | ) 123 | ), 124 | new FkAuthenticated( 125 | new TkSecure( 126 | new TkFork( 127 | new FkRegex("/domains", new TkDomains(base)), 128 | new FkRegex( 129 | "/add", 130 | new TkMethods(new TkAdd(base), "POST") 131 | ), 132 | new FkRegex("/delete", new TkDelete(base)) 133 | ) 134 | ) 135 | ) 136 | ) 137 | ) 138 | ) 139 | ) 140 | ) 141 | ) 142 | ) 143 | ) 144 | ) 145 | ), 146 | String.format( 147 | "X-Jare-Revision: %s", 148 | Manifests.read("Jare-Revision") 149 | ), 150 | "Vary: Cookie" 151 | ) 152 | ); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/test/resources/io/jare/test: -------------------------------------------------------------------------------- 1 | #Version: 1.0 2 | #Fields: date time x-edge-location sc-bytes c-ip cs-method cs(Host) cs-uri-stem sc-status cs(Referer) cs(User-Agent) cs-uri-query cs(Cookie) x-edge-result-type x-edge-request-id x-host-header cs-protocol cs-bytes time-taken x-forwarded-for ssl-protocol ssl-cipher x-edge-response-result-type cs-protocol-version 3 | 2016-11-19 10:38:41 TPE50 282004 101.138.97.108 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/289419 Mozilla/5.0%2520(Linux;%2520Android%25206.0.1;%2520HTC_A9u%2520Build/MMB29M;%2520wv)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Chrome/49.0.2623.105%2520Mobile%2520Safari/537.36%2520%5BFB_IAB/FB4A;FBAV/103.0.0.20.72;%5D u=http://fun01.cc/ups/24212/post/600x314/580dba45d0528424.png - RefreshHit GaWfXCYbXPmOSeLrZUy1Cz0Ng_4JWQVoCGRbEakjDitnwEsYT6q8mw== cf.jare.io http 506 0.467 - - - RefreshHit HTTP/1.1 4 | 2016-11-19 10:38:47 TPE50 23614 203.203.60.44 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297026 Mozilla/5.0%2520(Linux;%2520U;%2520Android%25204.1.2;%2520zh-tw;%2520GT-I9300%2520Build/JZO54K)%2520AppleWebKit/534.30%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Mobile%2520Safari/534.30%2520%5BFB_IAB/FB4A;FBAV/91.0.0.17.68;%5D u=http://fun01.cc/ups/27933/post/600x314/582c68f30b8e1594.jpg - Hit Iw8x859mSjICS3lp9a_y3h3C-u0oJHqCZn-NLIctha5Sxd3jf3DKfQ== cf.jare.io http 473 0.006 - - - Hit HTTP/1.1 5 | 2016-11-19 10:38:51 TPE50 54463 101.138.119.202 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297509 Mozilla/5.0%2520(Linux;%2520Android%25204.4.2;%2520ASUS_T00P%2520Build/KOT49H)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Chrome/30.0.0.0%2520Mobile%2520Safari/537.36%2520%5BFB_IAB/FB4A;FBAV/103.0.0.20.72;%5D u=http://fun01.cc/ups/29023/post/600x314/582eb63d02b95834.jpg - Hit Man-IEBIlSJe4hyE7OTmcK5Y_fDOQJp9jCDknPUEkhz8P3whgqbS8w== cf.jare.io http 555 0.004 - - - Hit HTTP/1.1 6 | 2016-11-19 10:38:56 TPE50 831 101.138.97.108 GET djk1be5eatcae.cloudfront.net / 304 http://fun01.cc/post/289419?agree=1 Mozilla/5.0%2520(Linux;%2520Android%25206.0.1;%2520HTC_A9u%2520Build/MMB29M;%2520wv)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Chrome/49.0.2623.105%2520Mobile%2520Safari/537.36%2520%5BFB_IAB/FB4A;FBAV/103.0.0.20.72;%5D u=http://fun01.cc/ups/24212/post/600x314/580dba45d0528424.png - Hit 0BLwOCjzLUaGgcESDdCKLBhHFOq3XpnIwRw-pfjJHfGaEBlAZIPmuQ== cf.jare.io http 602 0.003 - - - Hit HTTP/1.1 7 | 2016-11-19 10:38:49 SIN2 187981 101.127.227.192 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297541 Mozilla/5.0%2520(Linux;%2520Android%25206.0.1;%2520SM-G930F%2520Build/MMB29K;%2520wv)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Chrome/54.0.2840.85%2520Mobile%2520Safari/537.36%2520%5BFB_IAB/FB4A;FBAV/103.0.0.20.72;%5D u=http://fun01.cc/ups/2131/post/600x314/582ee7d79bf6f247.jpg - Hit 5HaDB0isE9STYcDdcyhESlv5v-m-EAmVaP5a28qiTs57Gukg0hNs-A== cf.jare.io http 505 0.002 - - - Hit HTTP/1.1 8 | 2016-11-19 10:38:30 SIN3 202919 14.100.136.4 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297681?fb=471741276327026 Mozilla/5.0%2520(Linux;%2520Android%25206.0.1;%2520SM-G930F%2520Build/MMB29K;%2520wv)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Chrome/49.0.2623.105%2520Mobile%2520Safari/537.36%2520%5BFB_IAB/FB4A;FBAV/103.0.0.20.72;%5D u=http://fun01.cc/ups/2131/post/600x314/58300daef1b7c554.jpg - Hit 53L6fWZ9waAgXjdbUsGppYNk0Nkg4HgRB2aEeoAMw8Zbpw0nTRpKQA== cf.jare.io http 525 0.007 - - - Hit HTTP/1.1 9 | 2016-11-19 10:38:49 SIN3 187981 219.92.206.238 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297541?fb=471741276327026 Mozilla/5.0%2520(Linux;%2520Android%25205.0.2;%2520vivo%2520Y51%2520Build/LRX22G)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Chrome/54.0.2840.85%2520Mobile%2520Safari/537.36 u=http://fun01.cc/ups/2131/post/600x314/582ee7d79bf6f247.jpg - Hit 3xk6LCigO_IFnidx_e6ufcMimyZ-NBLJRG29jwAp8GOVIwNrIxqh7w== cf.jare.io http 450 0.002 - - - Hit HTTP/1.1 10 | 2016-11-19 10:38:41 SYD1 18809 49.197.205.235 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/243413?t=30569 Mozilla/5.0%2520(iPhone;%2520CPU%2520iPhone%2520OS%252010_1_1%2520like%2520Mac%2520OS%2520X)%2520AppleWebKit/602.2.14%2520(KHTML,%2520like%2520Gecko)%2520Mobile/14B100%2520%5BFBAN/FBIOS;FBAV/72.0.0.40.71;FBBV/43917395;FBRV/0;FBDV/iPhone8,2;FBMD/iPhone;FBSN/iOS;FBSV/10.1.1;FBSS/3;FBCR/OPTUS;FBID/phone;FBLC/zh_TW;FBOP/5%5D u=http://fun01.cc/ups/27795/post/600x314/290969926971729.jpg - Hit iNRA1cxVCSdsBSp3l7cltYbWZZQy66YbB0NHza24__M-6teCFN22WA== cf.jare.io http 508 0.002 - - - Hit HTTP/1.1 11 | 2016-11-19 10:38:33 SIN2 53273 115.133.19.8 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297392?ref=authormore Mozilla/5.0%2520(Linux;%2520Android%25205.0.2;%2520Mi%25204i%2520Build/LRX22G)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Chrome/54.0.2840.85%2520Mobile%2520Safari/537.36 u=http://fun01.cc/ups/28068/post/600x314/582dbb689682b450.jpg - Hit 786cMISa0X6OJ-q9AMd8vygyOegYpOa_gxsn7oSGOJ5kySvB5_QU1A== cf.jare.io http 441 0.003 - - - Hit HTTP/1.1 12 | 2016-11-19 10:38:43 TPE50 23614 1.174.121.63 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297026 Mozilla/5.0%2520(Linux;%2520U;%2520Android%25204.3;%2520zh-tw;%2520GT-I9500%2520Build/JSS15J)%2520AppleWebKit/534.30%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Mobile%2520Safari/534.30%2520%5BFB_IAB/FB4A;FBAV/103.0.0.20.72;%5D u=http://fun01.cc/ups/27933/post/600x314/582c68f30b8e1594.jpg - Hit VsFHc54MywUzx0H6tN-h2E2AeTmwLPbQjYgUHB8LmmmzO-c4pNwpDw== cf.jare.io http 472 0.006 - - - Hit HTTP/1.1 13 | 2016-11-19 10:38:55 TPE50 913 124.9.192.74 GET djk1be5eatcae.cloudfront.net / 304 http://fun01.cc/post/292990 Mozilla/5.0%2520(Linux;%2520Android%25205.1.1;%2520SAMSUNG%2520SM-G531Y%2520Build/LMY48B)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520SamsungBrowser/3.3%2520Chrome/38.0.2125.102%2520Mobile%2520Safari/537.36 u=http://fun01.cc/ups/11413/post/600x314/581c153664281554.jpg - Miss b0D_JlqRUx6dxN-_7j3hlXDWLUIymMO1jaqG9OM-_xZKPB-xk2GKdg== cf.jare.io http 573 0.450 - - - Miss HTTP/1.1 14 | 2016-11-19 10:39:00 TPE50 42244 118.170.224.188 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297641 Mozilla/5.0%2520(Linux;%2520Android%25205.0.1;%2520SM-N910U%2520Build/LRX22C)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Chrome/54.0.2840.85%2520Mobile%2520Safari/537.36 u=http://fun01.cc/ups/12262/post/600x314/1479524001747382.jpg - Hit rGyDhi2qZSNbwCGFOxNqK1Ge94_VAvdq1BxTEfek_xIf3hmjUFAMbQ== cf.jare.io http 441 0.002 - - - Hit HTTP/1.1 15 | 2016-11-19 10:38:20 SFO9 41021 173.252.120.109 GET djk1be5eatcae.cloudfront.net / 200 - facebookexternalhit/1.1%2520(+http://www.facebook.com/externalhit_uatext.php) u=http%253A%252F%252Ffun01.cc%252Fups%252F12262%252Fpost%252F600x314%252F1479512362737265.jpg - Hit K0saJdeWl1yw7uxzx2iNKnx8aCMAwI8mQLuwycppOLCWjoUZiVw_ag== cf.jare.io http 270 0.001 - - - Hit HTTP/1.1 16 | 2016-11-19 10:38:53 SFO9 97451 66.220.158.121 GET djk1be5eatcae.cloudfront.net / 200 - facebookexternalhit/1.1%2520(+http://www.facebook.com/externalhit_uatext.php) u=http%253A%252F%252Ffun01.cc%252Fups%252F12262%252Fpost%252F600x314%252F1479536064580949.jpg - Hit ChwdzwUCx5IkABHC4w1BETHfO4Gwg4eITdfQJuM30x6zPryOKHPUMA== cf.jare.io http 270 0.001 - - - Hit HTTP/1.1 17 | 2016-11-19 10:38:47 TPE50 19742 122.254.24.216 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297594 Mozilla/5.0%2520(Linux;%2520U;%2520Android%25205.0.2;%2520zh-tw;%2520Redmi%2520Note%25202%2520Build/LRX22G)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Chrome/42.0.0.0%2520Mobile%2520Safari/537.36%2520XiaoMi/MiuiBrowser/2.1.1 u=http://fun01.cc/ups/964/post/600x314/119681118282765.jpg - Hit 3HzcNxfDdAB3PtGqXr0TlcYK_n3XcUu-tMsYZCUqGT-ckelSeZJYZQ== cf.jare.io http 453 0.012 - - - Hit HTTP/1.1 18 | 2016-11-19 10:38:48 TPE50 821 49.216.215.254 GET djk1be5eatcae.cloudfront.net / 304 http://fun01.cc/post/297534?agree=1 Mozilla/5.0%2520(Linux;%2520Android%25206.0;%2520HUAWEI%2520VNS-L22%2520Build/HUAWEIVNS-L22;%2520wv)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Chrome/51.0.2704.81%2520Mobile%2520Safari/537.36%2520%5BFB_IAB/FB4A;FBAV/103.0.0.20.72;%5D u=http://fun01.cc/ups/2108/post/600x314/582ee15820920239.jpg - Hit Dnm0xVKTfsgvPG-uemdHIQqWMD3eGrQFTuk8TC02Ahcz6dbhw_BCxg== cf.jare.io http 611 0.004 - - - Hit HTTP/1.1 19 | 2016-11-19 10:38:51 TPE50 40043 111.249.222.218 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297020 Mozilla/5.0%2520(Linux;%2520Android%25205.0.2;%2520ASUS_Z00LD%2520Build/LRX22G;%2520wv)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Chrome/54.0.2840.85%2520Mobile%2520Safari/537.36%2520%5BFB_IAB/FB4A;FBAV/103.0.0.20.72;%5D u=http://fun01.cc/ups/27933/post/600x314/582c678faa08a413.jpg - Hit mORkZcpG9WdnrQYuFcTsZFUiqOrRj2zXZhPg_-jSADbyn99yQtdTOw== cf.jare.io http 508 0.003 - - - Hit HTTP/1.1 20 | 2016-11-19 10:38:57 TPE50 15288 49.219.104.239 GET djk1be5eatcae.cloudfront.net / 200 http://fun01.cc/post/297683?fb=Times168168&xsign=3b450509e450e5c799e1c0edc0d968d7&xkey=314795519292 Mozilla/5.0%2520(Linux;%2520Android%25206.0.1;%2520ASUS_Z00ED%2520Build/MMB29P;%2520wv)%2520AppleWebKit/537.36%2520(KHTML,%2520like%2520Gecko)%2520Version/4.0%2520Chrome/54.0.2840.85%2520Mobile%2520Safari/537.36 u=http://fun01.cc/ups/27552/post/600x314/583013b27b686665.jpg - Hit ArxNAEV7RDr_ZX6IDw5ZpkSoz1NslteFApYeAJjXD5zUktkcA5CkCw== cf.jare.io http 546 0.001 - - - Hit HTTP/1.1 21 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 4.0.0 8 | 9 | com.jcabi 10 | parent 11 | 0.68.0 12 | 13 | io.jare 14 | jare 15 | 1.0-SNAPSHOT 16 | jar 17 | jare 18 | jare 19 | http://www.jare.io 20 | 2016 21 | 22 | Yegor Bugayenko 23 | https://www.yegor256.com 24 | 25 | 26 | 27 | MIT 28 | https://github.com/yegor256/jare/LICENSE.txt 29 | site 30 | 31 | 32 | 33 | 34 | 1 35 | Yegor Bugayenko 36 | yegor256@gmail.com 37 | Zerocracy 38 | http://www.zerocracy.com 39 | 40 | Architect 41 | Developer 42 | 43 | +3 44 | 45 | 46 | 47 | GitHub 48 | https://github.com/yegor256/jare/issues 49 | 50 | 51 | scm:git:git@github.com:yegor256/jare.git 52 | scm:git:git@github.com:yegor256/jare.git 53 | https://github.com/yegor256/jare 54 | 55 | 56 | GitHub 57 | https://github.com/yegor256/jare 58 | 59 | 60 | 61 | 62 | 63 | 64 | io.sentry 65 | sentry 66 | 8.0.0 67 | 68 | 69 | com.amazonaws 70 | aws-java-sdk-core 71 | 1.12.702 72 | 73 | 74 | com.amazonaws 75 | aws-java-sdk-dynamodb 76 | 1.12.702 77 | 78 | 79 | com.amazonaws 80 | aws-java-sdk-cloudfront 81 | 1.12.702 82 | 83 | 84 | org.takes 85 | takes 86 | 1.24.6 87 | 88 | 89 | com.jcabi.incubator 90 | xembly 91 | 0.32.1 92 | 93 | 94 | org.cactoos 95 | cactoos 96 | 0.56.1 97 | 98 | 99 | xml-apis 100 | xml-apis 101 | 1.0.b2 102 | 103 | 104 | log4j 105 | log4j 106 | 1.2.17 107 | runtime 108 | 109 | 110 | org.hamcrest 111 | hamcrest-core 112 | 3.0 113 | test 114 | 115 | 116 | javax.json 117 | javax.json-api 118 | 1.1.4 119 | 120 | 121 | org.glassfish 122 | javax.json 123 | 1.1.4 124 | 125 | 126 | org.projectlombok 127 | lombok 128 | 1.18.36 129 | 130 | 131 | org.apache.commons 132 | commons-lang3 133 | 3.17.0 134 | 135 | 136 | commons-io 137 | commons-io 138 | 2.18.0 139 | 140 | 141 | com.jcabi 142 | jcabi-xml 143 | 0.31.0 144 | 145 | 146 | com.jcabi 147 | jcabi-s3 148 | 0.19.0 149 | 150 | 151 | com.jcabi 152 | jcabi-aspects 153 | 0.26.0 154 | 155 | 156 | javax.validation 157 | validation-api 158 | 2.0.1.Final 159 | test 160 | 161 | 162 | com.jcabi 163 | jcabi-http 164 | 1.20.1 165 | 166 | 167 | com.jcabi 168 | jcabi-log 169 | 0.24.3 170 | 171 | 172 | com.jcabi 173 | jcabi-dynamo 174 | 0.22.4 175 | 176 | 177 | com.jcabi 178 | jcabi-matchers 179 | 1.8.0 180 | 181 | 182 | org.apache.velocity 183 | velocity 184 | 1.7 185 | runtime 186 | 187 | 188 | com.sun.jersey 189 | jersey-client 190 | 1.19.4 191 | runtime 192 | 193 | 194 | com.jcabi 195 | jcabi-manifests 196 | 2.1.0 197 | 198 | 199 | net.sf.saxon 200 | Saxon-HE 201 | 12.5 202 | runtime 203 | 204 | 205 | com.jcabi 206 | jcabi-jdbc 207 | 0.19.0 208 | test 209 | 210 | 211 | com.h2database 212 | h2 213 | 2.3.232 214 | test 215 | 216 | 217 | com.google.code.findbugs 218 | findbugs-annotations 219 | 3.0.1 220 | provided 221 | 222 | 223 | org.junit.jupiter 224 | junit-jupiter-api 225 | 5.10.3 226 | test 227 | 228 | 229 | org.junit.jupiter 230 | junit-jupiter-engine 231 | 5.10.3 232 | test 233 | 234 | 235 | org.junit.vintage 236 | junit-vintage-engine 237 | 5.10.3 238 | test 239 | 240 | 241 | junit 242 | junit 243 | 4.13.2 244 | test 245 | 246 | 247 | 248 | 249 | 250 | 251 | org.codehaus.mojo 252 | build-helper-maven-plugin 253 | 3.6.0 254 | 255 | 256 | maven-failsafe-plugin 257 | 258 | @{argLine} -Djava.awt.headless=true 259 | 260 | 261 | 262 | maven-surefire-plugin 263 | 264 | @{argLine} -Duser.language=en -Duser.region=US -Djava.awt.headless=true 265 | 266 | 267 | 268 | 269 | 270 | 271 | maven-deploy-plugin 272 | 273 | true 274 | 275 | 276 | 277 | 278 | 279 | 280 | resources 281 | 282 | 283 | pom.xml 284 | 285 | 286 | 287 | 288 | 289 | nl.geodienstencentrum.maven 290 | sass-maven-plugin 291 | 3.7.2 292 | 293 | 294 | generate-css 295 | generate-resources 296 | 297 | update-stylesheets 298 | 299 | 300 | ${basedir}/src/main/scss 301 | ${project.build.outputDirectory}/css 302 | 303 | 304 | 305 | 306 | 307 | com.tunyk.mvn.plugins.htmlcompressor 308 | htmlcompressor-maven-plugin 309 | 1.3 310 | 311 | 312 | compress-xsl 313 | generate-resources 314 | 315 | xml 316 | 317 | 318 | ${basedir}/src/main/resources/xsl 319 | ${project.build.outputDirectory}/xsl 320 | 321 | xsl 322 | 323 | false 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | dynamodb 333 | 334 | 335 | !skipTests 336 | 337 | 338 | 339 | AAAAABBBBBAAAAABBBBB 340 | ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD 341 | 342 | 343 | 344 | 345 | maven-dependency-plugin 346 | 347 | 348 | unpack-dynamodb-local 349 | 350 | unpack 351 | 352 | 353 | 354 | 355 | com.jcabi 356 | DynamoDBLocal 357 | 2023-05-26 358 | zip 359 | ${project.build.directory}/dynamodb-dist 360 | false 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | org.codehaus.mojo 369 | build-helper-maven-plugin 370 | 371 | 372 | reserver-dynamodb-port 373 | 374 | reserve-network-port 375 | 376 | 377 | 378 | dynamo.port 379 | 380 | 381 | 382 | 383 | 384 | 385 | com.jcabi 386 | jcabi-dynamodb-maven-plugin 387 | 0.10.1 388 | 389 | 390 | dynamodb-integration-test 391 | 392 | start 393 | create-tables 394 | stop 395 | 396 | 397 | ${dynamo.port} 398 | ${project.build.directory}/dynamodb-dist 399 | ${failsafe.dynamo.key} 400 | ${failsafe.dynamo.secret} 401 | 402 | ${basedir}/src/test/dynamodb/domains.json
    403 |
    404 |
    405 |
    406 |
    407 |
    408 | 409 | maven-failsafe-plugin 410 | 411 | none 412 | 413 | ${dynamo.port} 414 | 415 | 416 | 417 | 418 | 419 | integration-test 420 | verify 421 | 422 | 423 | 424 | 425 |
    426 |
    427 |
    428 | 429 | hit-refresh 430 | 431 | 432 | 433 | org.codehaus.mojo 434 | exec-maven-plugin 435 | 3.5.0 436 | 437 | 438 | start-server 439 | pre-integration-test 440 | 441 | java 442 | 443 | 444 | io.jare.Entrance 445 | test 446 | false 447 | 448 | --port=${port} 449 | --hit-refresh 450 | 451 | 452 | 453 | dynamo.port 454 | ${dynamo.port} 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | heroku 466 | 467 | 468 | pom.xml 469 | 470 | 471 | 472 | jare 473 | 474 | 475 | maven-jar-plugin 476 | 477 | 478 | ${project.build.outputDirectory}/META-INF/MANIFEST.MF 479 | 480 | 481 | 482 | 483 | maven-dependency-plugin 484 | 485 | 486 | copy-dependencies-for-heroku 487 | package 488 | 489 | copy-dependencies 490 | 491 | 492 | ${project.build.directory}/deps 493 | true 494 | true 495 | true 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | qulice 505 | 506 | 507 | 508 | com.qulice 509 | qulice-maven-plugin 510 | 0.21.1 511 | 512 | 513 | checkstyle:/src/main/resources/images/.* 514 | findbugs:.* 515 | duplicatefinder:.* 516 | 517 | 518 | 519 | 520 | 521 | 522 |
    523 |
    524 | --------------------------------------------------------------------------------