commit a8d46c86f7df0dbc16383fe0c4b149ac11a25ae8 Author: Jörg Prante Date: Thu Oct 20 09:43:33 2022 +0200 initial commit diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..6b27e17 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,80 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '27 21 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Java JDK + uses: actions/setup-java@v3.5.1 + with: + distribution: 'temurin' + java-version: '17' + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..021874f --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +/.settings +/.classpath +/.project +/.gradle +**/data +**/work +**/logs +**/.idea +**/target +**/out +**/build +.DS_Store +*.iml +*~ +*.key +*.crt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..466671c --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Java Net API for servers and clients + +## A consolidated Uniform Resource Locator implementation for Java + +A Uniform Resource Locator (URL) is a compact representation of the +location and access method for a resource available via the Internet. + +Historically, there are many different forms of internet resource representations, for example, +the URL (RFC 1738 as of 1994), the URI (RFC 2396 as of 1998), and IRI (RFC 3987 as of 2005), +and most of them have updated specifications. + +This Java implementation serves as a universal point of handling all +different forms. It follows the syntax of the Uniform Resource Identifier (RFC 3986) +in accordance with the https://url.spec.whatwg.org/[WHATWG URL standard]. + +This alternative implementation of Uniform Resource Locator combines the features of the vanilla URI/URL Java SDK implementations +but removes it peculiarities and deficiencies, such as `java.lang.IllegalArgumentException: Illegal character in path at ... at java.net.URI.create()` + +Normalization, NIO charset encoding/decoding, IPv6, an extensive set of schemes, and path matching have been added. + +Fast building and parsing URLs, improved percent decoding/encoding, and URI templating features are included, to make +this library also useful in URI and IRI contexts. + +While parsing and building, you have better control about address resolving. Only explicit `resolveFromhost` methods +will execute host lookup queries against DNS resolvers, otherwise, no resolving will occur under the hood. + +You can build URLs with a fluent API, for example + +``` +URL.http().host("foo.com").toUrlString() +``` + +And you can parse URLs with a fluent API, for exmaple + +``` +URL url = URL.parser().parse("file:///foo/bar?foo=bar#fragment"); +``` + +There is no external dependency. The size of the jar library is ~118k. The only dependency on `java.net` are the classes + +``` +java.net.IDN +java.net.Inet4Address +java.net.Inet6Address +java.net.InetAddress +``` + +which might get re-implemented in another library at a later time, in a project like Netty DNS resolver. + +## A simple HTTP server + +## A netty-based HTTP server + +# License + +Copyright (C) 2018 Jörg Prante + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +you may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/benchmark/build.gradle b/benchmark/build.gradle new file mode 100644 index 0000000..999eadb --- /dev/null +++ b/benchmark/build.gradle @@ -0,0 +1,14 @@ +plugins { + id "io.morethan.jmhreport" version "0.9.0" +} + +jmhReport { + jmhResultPath = project.file('build/reports/jmh/result.json') + jmhReportOutput = project.file('build/reports/jmh') +} +apply from: rootProject.file('gradle/test/jmh.gradle') + +dependencies { + implementation project(':net') + implementation project(':net-path') +} diff --git a/benchmark/src/jmh/java/org/xbib/net/benchmark/SimplePathBenchmark.java b/benchmark/src/jmh/java/org/xbib/net/benchmark/SimplePathBenchmark.java new file mode 100644 index 0000000..c990f8d --- /dev/null +++ b/benchmark/src/jmh/java/org/xbib/net/benchmark/SimplePathBenchmark.java @@ -0,0 +1,234 @@ +package org.xbib.net.benchmark; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Timeout; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.xbib.net.path.simple.Path; +import org.xbib.net.path.simple.PathComparator; +import org.xbib.net.path.simple.PathMatcher; + +@BenchmarkMode(Mode.Throughput) +@State(Scope.Benchmark) +@Warmup(iterations = 3) +@Measurement(iterations = 3) +@Fork(value = 1, jvmArgsAppend = "-Xmx1024m") +@Threads(4) +@Timeout(time = 10, timeUnit = TimeUnit.MINUTES) +public class SimplePathBenchmark { + + @State(Scope.Benchmark) + public static class AllRoutesPatternParser extends PatternParserData { + + @Setup(Level.Trial) + public void registerPatterns() { + parseRoutes(RouteGenerator.allRoutes()); + } + } + + @State(Scope.Benchmark) + public static class StaticRoutesPatternParser extends PatternParserData { + + @Setup(Level.Trial) + public void registerPatterns() { + parseRoutes(RouteGenerator.staticRoutes()); + } + } + + @State(Scope.Benchmark) + public static class AllRoutesAntPathMatcher extends PathMatcherData { + + @Setup(Level.Trial) + public void registerPatterns() { + parseRoutes(RouteGenerator.allRoutes()); + } + } + + @Benchmark + public void matchAllRoutesWithAntPathMatcher(AllRoutesAntPathMatcher data, Blackhole bh) { + for (String path : data.requestPaths) { + for (String pattern : data.patterns) { + bh.consume(data.matcher.match(pattern, path)); + } + } + } + + @Benchmark + public void matchAndSortAllRoutesWithAntPathMatcher(AllRoutesAntPathMatcher data, Blackhole bh) { + for (String path : data.requestPaths) { + List matches = new ArrayList<>(); + for (String pattern : data.patterns) { + if (data.matcher.match(pattern, path)) { + matches.add(new Path(pattern)); + } + } + matches.sort(new PathComparator(path)); + bh.consume(matches); + } + } + + @State(Scope.Benchmark) + public static class StaticRoutesAntPathMatcher extends PathMatcherData { + + @Setup(Level.Trial) + public void registerPatterns() { + parseRoutes(RouteGenerator.staticRoutes()); + } + } + + @Benchmark + public void matchStaticRoutesWithAntPathMatcher(StaticRoutesAntPathMatcher data, Blackhole bh) { + for (String path : data.requestPaths) { + for (String pattern : data.patterns) { + bh.consume(data.matcher.match(pattern, path)); + } + } + } + + static class PatternParserData { + + List patterns = new ArrayList<>(); + + List requestPaths = new ArrayList<>(); + + void parseRoutes(List routes) { + routes.forEach(route -> { + this.patterns.add(route.pattern); + this.requestPaths.addAll(route.matchingPaths); + }); + } + + } + + static class PathMatcherData { + + PathMatcher matcher = new PathMatcher(); + + List patterns = new ArrayList<>(); + + List requestPaths = new ArrayList<>(); + + void parseRoutes(List routes) { + routes.forEach(route -> { + this.patterns.add(route.pattern); + this.requestPaths.addAll(route.matchingPaths); + }); + } + + } + + /** + * Route in the web application. + * Each route has a path pattern and can generate sets of matching request paths for that pattern. + */ + static class Route { + + private final String pattern; + + private final List matchingPaths; + + public Route(String pattern, String... matchingPaths) { + this.pattern = pattern; + if (matchingPaths.length > 0) { + this.matchingPaths = Arrays.asList(matchingPaths); + } + else { + this.matchingPaths = Collections.singletonList(pattern); + } + } + + public String pattern() { + return this.pattern; + } + + public Iterable matchingPaths() { + return this.matchingPaths; + } + } + + static class RouteGenerator { + + static List staticRoutes() { + return Arrays.asList( + new Route("/"), + new Route("/why-spring"), + new Route("/microservices"), + new Route("/reactive"), + new Route("/event-driven"), + new Route("/cloud"), + new Route("/web-applications"), + new Route("/serverless"), + new Route("/batch"), + new Route("/community/overview"), + new Route("/community/team"), + new Route("/community/events"), + new Route("/community/support"), + new Route("/some/other/section"), + new Route("/blog.atom") + ); + } + + static List captureRoutes() { + return Arrays.asList( + new Route("/guides"), + new Route("/guides/gs/{repositoryName}", + "/guides/gs/rest-service", "/guides/gs/scheduling-tasks", + "/guides/gs/consuming-rest", "/guides/gs/relational-data-access"), + new Route("/projects"), + new Route("/projects/{name}", + "/projects/spring-boot", "/projects/spring-framework", + "/projects/spring-data", "/projects/spring-security", "/projects/spring-cloud"), + new Route("/blog/category/{category}.atom", + "/blog/category/releases.atom", "/blog/category/engineering.atom", + "/blog/category/news.atom"), + new Route("/tools/{name}", "/tools/eclipse", "/tools/vscode"), + new Route("/team/{username}", + "/team/jhoeller", "/team/bclozel", "/team/snicoll", "/team/sdeleuze", "/team/rstoyanchev"), + new Route("/api/projects/{projectId}", + "/api/projects/spring-boot", "/api/projects/spring-framework", + "/api/projects/reactor", "/api/projects/spring-data", + "/api/projects/spring-restdocs", "/api/projects/spring-batch"), + new Route("/api/projects/{projectId}/releases/{version}", + "/api/projects/spring-boot/releases/2.3.0", "/api/projects/spring-framework/releases/5.3.0", + "/api/projects/spring-boot/releases/2.2.0", "/api/projects/spring-framework/releases/5.2.0") + ); + } + + static List regexRoute() { + return Arrays.asList( + new Route("/blog/{year:\\\\d+}/{month:\\\\d+}/{day:\\\\d+}/{slug}", + "/blog/2020/01/01/spring-boot-released", "/blog/2020/02/10/this-week-in-spring", + "/blog/2020/03/12/spring-one-conference-2020", "/blog/2020/05/17/spring-io-barcelona-2020", + "/blog/2020/05/17/spring-io-barcelona-2020", "/blog/2020/06/06/spring-cloud-release"), + new Route("/user/{name:[a-z]+}", + "/user/emily", "/user/example", "/user/spring") + ); + } + + static List allRoutes() { + List routes = new ArrayList<>(); + routes.addAll(staticRoutes()); + routes.addAll(captureRoutes()); + routes.addAll(regexRoute()); + routes.add(new Route("/static/**", "/static/image.png", "/static/style.css")); + routes.add(new Route("/**", "/notfound", "/favicon.ico")); + return routes; + } + + } +} diff --git a/benchmark/src/jmh/java/org/xbib/net/benchmark/SpringPathBenchmark.java b/benchmark/src/jmh/java/org/xbib/net/benchmark/SpringPathBenchmark.java new file mode 100644 index 0000000..b8fe42c --- /dev/null +++ b/benchmark/src/jmh/java/org/xbib/net/benchmark/SpringPathBenchmark.java @@ -0,0 +1,206 @@ +package org.xbib.net.benchmark; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Timeout; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.xbib.net.path.spring.PathContainer; +import org.xbib.net.path.spring.PathPattern; +import org.xbib.net.path.spring.PathPatternParser; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@State(Scope.Benchmark) +@Warmup(iterations = 3) +@Measurement(iterations = 3) +@Fork(value = 1, jvmArgsAppend = "-Xmx1024m") +@Threads(4) +@Timeout(time = 10, timeUnit = TimeUnit.MINUTES) +public class SpringPathBenchmark { + + @State(Scope.Benchmark) + public static class AllRoutesPatternParser extends PatternParserData { + + @Setup(Level.Trial) + public void registerPatterns() { + parseRoutes(RouteGenerator.allRoutes()); + } + } + + @Benchmark + public void matchAllRoutesWithPathPatternParser(AllRoutesPatternParser data, Blackhole bh) { + for (String path : data.requestPaths) { + PathContainer pathContainer = PathContainer.parsePath(path); + for (String pattern : data.patterns) { + PathPattern pathPattern = data.parser.parse(pattern); + bh.consume(pathPattern.matches(pathContainer)); + } + } + } + + @Benchmark + public void matchAndSortAllRoutesWithPathPatternParser(AllRoutesPatternParser data, Blackhole bh) { + for (String path : data.requestPaths) { + PathContainer pathContainer = PathContainer.parsePath(path); + List matches = new ArrayList<>(); + for (String pattern : data.patterns) { + PathPattern pathPattern = data.parser.parse(pattern); + if (pathPattern.matches(pathContainer)) { + matches.add(pathPattern); + } + } + Collections.sort(matches); + bh.consume(matches); + } + } + + @State(Scope.Benchmark) + public static class StaticRoutesPatternParser extends PatternParserData { + + @Setup(Level.Trial) + public void registerPatterns() { + parseRoutes(RouteGenerator.staticRoutes()); + } + } + + @Benchmark + public void matchStaticRoutesWithPathPatternParser(StaticRoutesPatternParser data, Blackhole bh) { + for (String path : data.requestPaths) { + PathContainer pathContainer = PathContainer.parsePath(path); + for (String pattern : data.patterns) { + PathPattern pathPattern = data.parser.parse(pattern); + bh.consume(pathPattern.matches(pathContainer)); + } + } + } + + static class PatternParserData { + + PathPatternParser parser = new PathPatternParser(); + + List patterns = new ArrayList<>(); + + List requestPaths = new ArrayList<>(); + + void parseRoutes(List routes) { + routes.forEach(route -> { + this.patterns.add(route.pattern); + this.requestPaths.addAll(route.matchingPaths); + }); + } + } + + /** + * Route in the web application. + * Each route has a path pattern and can generate sets of matching request paths for that pattern. + */ + static class Route { + + private final String pattern; + + private final List matchingPaths; + + public Route(String pattern, String... matchingPaths) { + this.pattern = pattern; + if (matchingPaths.length > 0) { + this.matchingPaths = Arrays.asList(matchingPaths); + } + else { + this.matchingPaths = Collections.singletonList(pattern); + } + } + + public String pattern() { + return this.pattern; + } + + public Iterable matchingPaths() { + return this.matchingPaths; + } + } + + static class RouteGenerator { + + static List staticRoutes() { + return Arrays.asList( + new Route("/"), + new Route("/why-spring"), + new Route("/microservices"), + new Route("/reactive"), + new Route("/event-driven"), + new Route("/cloud"), + new Route("/web-applications"), + new Route("/serverless"), + new Route("/batch"), + new Route("/community/overview"), + new Route("/community/team"), + new Route("/community/events"), + new Route("/community/support"), + new Route("/some/other/section"), + new Route("/blog.atom") + ); + } + + static List captureRoutes() { + return Arrays.asList( + new Route("/guides"), + new Route("/guides/gs/{repositoryName}", + "/guides/gs/rest-service", "/guides/gs/scheduling-tasks", + "/guides/gs/consuming-rest", "/guides/gs/relational-data-access"), + new Route("/projects"), + new Route("/projects/{name}", + "/projects/spring-boot", "/projects/spring-framework", + "/projects/spring-data", "/projects/spring-security", "/projects/spring-cloud"), + new Route("/blog/category/{category}.atom", + "/blog/category/releases.atom", "/blog/category/engineering.atom", + "/blog/category/news.atom"), + new Route("/tools/{name}", "/tools/eclipse", "/tools/vscode"), + new Route("/team/{username}", + "/team/jhoeller", "/team/bclozel", "/team/snicoll", "/team/sdeleuze", "/team/rstoyanchev"), + new Route("/api/projects/{projectId}", + "/api/projects/spring-boot", "/api/projects/spring-framework", + "/api/projects/reactor", "/api/projects/spring-data", + "/api/projects/spring-restdocs", "/api/projects/spring-batch"), + new Route("/api/projects/{projectId}/releases/{version}", + "/api/projects/spring-boot/releases/2.3.0", "/api/projects/spring-framework/releases/5.3.0", + "/api/projects/spring-boot/releases/2.2.0", "/api/projects/spring-framework/releases/5.2.0") + ); + } + + static List regexRoute() { + return Arrays.asList( + new Route("/blog/{year:\\\\d+}/{month:\\\\d+}/{day:\\\\d+}/{slug}", + "/blog/2020/01/01/spring-boot-released", "/blog/2020/02/10/this-week-in-spring", + "/blog/2020/03/12/spring-one-conference-2020", "/blog/2020/05/17/spring-io-barcelona-2020", + "/blog/2020/05/17/spring-io-barcelona-2020", "/blog/2020/06/06/spring-cloud-release"), + new Route("/user/{name:[a-z]+}", + "/user/emily", "/user/example", "/user/spring") + ); + } + + static List allRoutes() { + List routes = new ArrayList<>(); + routes.addAll(staticRoutes()); + routes.addAll(captureRoutes()); + routes.addAll(regexRoute()); + routes.add(new Route("/static/**", "/static/image.png", "/static/style.css")); + routes.add(new Route("/**", "/notfound", "/favicon.ico")); + return routes; + } + + } +} diff --git a/benchmark/src/jmh/java/org/xbib/net/benchmark/StructurePathBenchmark.java b/benchmark/src/jmh/java/org/xbib/net/benchmark/StructurePathBenchmark.java new file mode 100644 index 0000000..6b5984e --- /dev/null +++ b/benchmark/src/jmh/java/org/xbib/net/benchmark/StructurePathBenchmark.java @@ -0,0 +1,234 @@ +package org.xbib.net.benchmark; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Timeout; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.xbib.net.path.structure.Path; +import org.xbib.net.path.structure.PathComparator; +import org.xbib.net.path.structure.PathMatcher; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@State(Scope.Benchmark) +@Warmup(iterations = 3) +@Measurement(iterations = 3) +@Fork(value = 1, jvmArgsAppend = "-Xmx1024m") +@Threads(4) +@Timeout(time = 10, timeUnit = TimeUnit.MINUTES) +public class StructurePathBenchmark { + + @State(Scope.Benchmark) + public static class AllRoutesPatternParser extends PatternParserData { + + @Setup(Level.Trial) + public void registerPatterns() { + parseRoutes(RouteGenerator.allRoutes()); + } + } + + @State(Scope.Benchmark) + public static class StaticRoutesPatternParser extends PatternParserData { + + @Setup(Level.Trial) + public void registerPatterns() { + parseRoutes(RouteGenerator.staticRoutes()); + } + } + + @State(Scope.Benchmark) + public static class AllRoutesAntPathMatcher extends PathMatcherData { + + @Setup(Level.Trial) + public void registerPatterns() { + parseRoutes(RouteGenerator.allRoutes()); + } + } + + @Benchmark + public void matchAllRoutesWithAntPathMatcher(AllRoutesAntPathMatcher data, Blackhole bh) { + for (String path : data.requestPaths) { + for (String pattern : data.patterns) { + bh.consume(data.matcher.match(pattern, path)); + } + } + } + + @Benchmark + public void matchAndSortAllRoutesWithAntPathMatcher(AllRoutesAntPathMatcher data, Blackhole bh) { + for (String path : data.requestPaths) { + List matches = new ArrayList<>(); + for (String pattern : data.patterns) { + if (data.matcher.match(pattern, path)) { + matches.add(new Path(pattern)); + } + } + matches.sort(new PathComparator(path)); + bh.consume(matches); + } + } + + @State(Scope.Benchmark) + public static class StaticRoutesAntPathMatcher extends PathMatcherData { + + @Setup(Level.Trial) + public void registerPatterns() { + parseRoutes(RouteGenerator.staticRoutes()); + } + } + + @Benchmark + public void matchStaticRoutesWithAntPathMatcher(StaticRoutesAntPathMatcher data, Blackhole bh) { + for (String path : data.requestPaths) { + for (String pattern : data.patterns) { + bh.consume(data.matcher.match(pattern, path)); + } + } + } + + static class PatternParserData { + + List patterns = new ArrayList<>(); + + List requestPaths = new ArrayList<>(); + + void parseRoutes(List routes) { + routes.forEach(route -> { + this.patterns.add(route.pattern); + this.requestPaths.addAll(route.matchingPaths); + }); + } + + } + + static class PathMatcherData { + + PathMatcher matcher = new PathMatcher(); + + List patterns = new ArrayList<>(); + + List requestPaths = new ArrayList<>(); + + void parseRoutes(List routes) { + routes.forEach(route -> { + this.patterns.add(route.pattern); + this.requestPaths.addAll(route.matchingPaths); + }); + } + + } + + /** + * Route in the web application. + * Each route has a path pattern and can generate sets of matching request paths for that pattern. + */ + static class Route { + + private final String pattern; + + private final List matchingPaths; + + public Route(String pattern, String... matchingPaths) { + this.pattern = pattern; + if (matchingPaths.length > 0) { + this.matchingPaths = Arrays.asList(matchingPaths); + } + else { + this.matchingPaths = Collections.singletonList(pattern); + } + } + + public String pattern() { + return this.pattern; + } + + public Iterable matchingPaths() { + return this.matchingPaths; + } + } + + static class RouteGenerator { + + static List staticRoutes() { + return Arrays.asList( + new Route("/"), + new Route("/why-spring"), + new Route("/microservices"), + new Route("/reactive"), + new Route("/event-driven"), + new Route("/cloud"), + new Route("/web-applications"), + new Route("/serverless"), + new Route("/batch"), + new Route("/community/overview"), + new Route("/community/team"), + new Route("/community/events"), + new Route("/community/support"), + new Route("/some/other/section"), + new Route("/blog.atom") + ); + } + + static List captureRoutes() { + return Arrays.asList( + new Route("/guides"), + new Route("/guides/gs/{repositoryName}", + "/guides/gs/rest-service", "/guides/gs/scheduling-tasks", + "/guides/gs/consuming-rest", "/guides/gs/relational-data-access"), + new Route("/projects"), + new Route("/projects/{name}", + "/projects/spring-boot", "/projects/spring-framework", + "/projects/spring-data", "/projects/spring-security", "/projects/spring-cloud"), + new Route("/blog/category/{category}.atom", + "/blog/category/releases.atom", "/blog/category/engineering.atom", + "/blog/category/news.atom"), + new Route("/tools/{name}", "/tools/eclipse", "/tools/vscode"), + new Route("/team/{username}", + "/team/jhoeller", "/team/bclozel", "/team/snicoll", "/team/sdeleuze", "/team/rstoyanchev"), + new Route("/api/projects/{projectId}", + "/api/projects/spring-boot", "/api/projects/spring-framework", + "/api/projects/reactor", "/api/projects/spring-data", + "/api/projects/spring-restdocs", "/api/projects/spring-batch"), + new Route("/api/projects/{projectId}/releases/{version}", + "/api/projects/spring-boot/releases/2.3.0", "/api/projects/spring-framework/releases/5.3.0", + "/api/projects/spring-boot/releases/2.2.0", "/api/projects/spring-framework/releases/5.2.0") + ); + } + + static List regexRoute() { + return Arrays.asList( + new Route("/blog/{year:\\\\d+}/{month:\\\\d+}/{day:\\\\d+}/{slug}", + "/blog/2020/01/01/spring-boot-released", "/blog/2020/02/10/this-week-in-spring", + "/blog/2020/03/12/spring-one-conference-2020", "/blog/2020/05/17/spring-io-barcelona-2020", + "/blog/2020/05/17/spring-io-barcelona-2020", "/blog/2020/06/06/spring-cloud-release"), + new Route("/user/{name:[a-z]+}", + "/user/emily", "/user/example", "/user/spring") + ); + } + + static List allRoutes() { + List routes = new ArrayList<>(); + routes.addAll(staticRoutes()); + routes.addAll(captureRoutes()); + routes.addAll(regexRoute()); + routes.add(new Route("/static/**", "/static/image.png", "/static/style.css")); + routes.add(new Route("/**", "/notfound", "/favicon.ico")); + return routes; + } + + } +} diff --git a/benchmark/src/jmh/reports/result-20220213.json b/benchmark/src/jmh/reports/result-20220213.json new file mode 100644 index 0000000..df4af19 --- /dev/null +++ b/benchmark/src/jmh/reports/result-20220213.json @@ -0,0 +1,502 @@ +[ + { + "jmhVersion" : "1.34", + "benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchAllRoutesWithAntPathMatcher", + "mode" : "thrpt", + "threads" : 1, + "forks" : 5, + "jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java", + "jvmArgs" : [ + "-Dfile.encoding=UTF-8", + "-Duser.country=DE", + "-Duser.language=de", + "-Duser.variant" + ], + "jdkVersion" : "17.0.2", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "17.0.2+8-LTS", + "warmupIterations" : 5, + "warmupTime" : "10 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "10 s", + "measurementBatchSize" : 1, + "primaryMetric" : { + "score" : 979.0629333513093, + "scoreError" : 27.642716728605016, + "scoreConfidence" : [ + 951.4202166227043, + 1006.7056500799143 + ], + "scorePercentiles" : { + "0.0" : 879.6177196347368, + "50.0" : 996.1943219563238, + "90.0" : 1010.1573254891413, + "95.0" : 1011.419317810492, + "99.0" : 1011.8817472575878, + "99.9" : 1011.8817472575878, + "99.99" : 1011.8817472575878, + "99.999" : 1011.8817472575878, + "99.9999" : 1011.8817472575878, + "100.0" : 1011.8817472575878 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1009.2169360206196, + 1007.6289077910706, + 1010.3403157672683, + 979.6478990918882, + 927.4713639881832 + ], + [ + 954.1692363754581, + 1006.7576171864503, + 1008.6971779542434, + 987.6616019498011, + 1006.9774361173152 + ], + [ + 1003.6756349360693, + 1008.777218930438, + 996.8131753845925, + 1010.0353319703898, + 1011.8817472575878 + ], + [ + 996.1943219563238, + 987.4294465450744, + 980.0787394199706, + 924.6048449498232, + 955.5666395887631 + ], + [ + 1005.7277746036237, + 936.5475165296377, + 879.6177196347368, + 917.0897875515193, + 963.9649422818879 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.34", + "benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchAllRoutesWithPathPatternParser", + "mode" : "thrpt", + "threads" : 1, + "forks" : 5, + "jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java", + "jvmArgs" : [ + "-Dfile.encoding=UTF-8", + "-Duser.country=DE", + "-Duser.language=de", + "-Duser.variant" + ], + "jdkVersion" : "17.0.2", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "17.0.2+8-LTS", + "warmupIterations" : 5, + "warmupTime" : "10 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "10 s", + "measurementBatchSize" : 1, + "primaryMetric" : { + "score" : 44254.50565969629, + "scoreError" : 1524.9519049748324, + "scoreConfidence" : [ + 42729.553754721455, + 45779.457564671124 + ], + "scorePercentiles" : { + "0.0" : 41271.26175680116, + "50.0" : 43804.78755944657, + "90.0" : 48137.73981257094, + "95.0" : 48652.8016902135, + "99.0" : 48702.20603834606, + "99.9" : 48702.20603834606, + "99.99" : 48702.20603834606, + "99.999" : 48702.20603834606, + "99.9999" : 48702.20603834606, + "100.0" : 48702.20603834606 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 43954.01874148045, + 43804.78755944657, + 43280.17445633301, + 44021.35079059644, + 43077.16644786855 + ], + [ + 41715.626689627716, + 43111.08050914323, + 42619.336800500045, + 41271.26175680116, + 43714.16736786474 + ], + [ + 43084.22399673246, + 41654.897785714944, + 43662.041021929304, + 44422.07541071771, + 45034.34913614275 + ], + [ + 44618.34940975135, + 44743.65546839248, + 42666.36782811372, + 44101.84625525753, + 43335.04048928811 + ], + [ + 48702.20603834606, + 47490.34864222843, + 45869.52757654424, + 47871.216435682116, + 48537.52487790418 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.34", + "benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchAndSortAllRoutesWithAntPathMatcher", + "mode" : "thrpt", + "threads" : 1, + "forks" : 5, + "jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java", + "jvmArgs" : [ + "-Dfile.encoding=UTF-8", + "-Duser.country=DE", + "-Duser.language=de", + "-Duser.variant" + ], + "jdkVersion" : "17.0.2", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "17.0.2+8-LTS", + "warmupIterations" : 5, + "warmupTime" : "10 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "10 s", + "measurementBatchSize" : 1, + "primaryMetric" : { + "score" : 950.1275103274515, + "scoreError" : 15.190556194806026, + "scoreConfidence" : [ + 934.9369541326455, + 965.3180665222576 + ], + "scorePercentiles" : { + "0.0" : 911.3943124100621, + "50.0" : 952.2968617887568, + "90.0" : 977.7307762395892, + "95.0" : 981.1789404332043, + "99.0" : 982.0691544544186, + "99.9" : 982.0691544544186, + "99.99" : 982.0691544544186, + "99.999" : 982.0691544544186, + "99.9999" : 982.0691544544186, + "100.0" : 982.0691544544186 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 982.0691544544186, + 955.0374171216056, + 979.1017743837043, + 964.6624852400518, + 925.0276825554569 + ], + [ + 956.1755582451195, + 911.3943124100621, + 929.0652340073174, + 967.4455956409554, + 952.9508945384788 + ], + [ + 973.057696831325, + 966.6347503815825, + 936.0666443887872, + 976.8167774768458, + 951.504251121885 + ], + [ + 952.2968617887568, + 921.966200024008, + 916.1240790196013, + 933.0207219756703, + 945.3684132560395 + ], + [ + 939.5733852782138, + 975.007311710178, + 956.226440100797, + 940.4884972825927, + 946.1056189528312 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.34", + "benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchAndSortAllRoutesWithPathPatternParser", + "mode" : "thrpt", + "threads" : 1, + "forks" : 5, + "jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java", + "jvmArgs" : [ + "-Dfile.encoding=UTF-8", + "-Duser.country=DE", + "-Duser.language=de", + "-Duser.variant" + ], + "jdkVersion" : "17.0.2", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "17.0.2+8-LTS", + "warmupIterations" : 5, + "warmupTime" : "10 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "10 s", + "measurementBatchSize" : 1, + "primaryMetric" : { + "score" : 40031.37477838221, + "scoreError" : 1996.2389298981366, + "scoreConfidence" : [ + 38035.135848484075, + 42027.61370828035 + ], + "scorePercentiles" : { + "0.0" : 36433.00109114869, + "50.0" : 39320.61428369096, + "90.0" : 45155.11092333606, + "95.0" : 45599.71727569494, + "99.0" : 45693.3908403424, + "99.9" : 45693.3908403424, + "99.99" : 45693.3908403424, + "99.999" : 45693.3908403424, + "99.9999" : 45693.3908403424, + "100.0" : 45693.3908403424 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 39599.68477777575, + 37973.60369831034, + 38396.27633481238, + 39253.010054103164, + 37429.51267783532 + ], + [ + 38067.106506328586, + 39124.57632651807, + 38262.53596403596, + 39303.94121151709, + 39504.46473802137 + ], + [ + 39794.17254168896, + 39705.71096962702, + 39652.38632950062, + 39579.80206238178, + 39512.01803650355 + ], + [ + 44115.77467607626, + 45004.42112232619, + 45381.14562485087, + 44725.72512753276, + 45693.3908403424 + ], + [ + 36433.00109114869, + 39320.61428369096, + 38484.66091818098, + 38495.814342280995, + 37971.019204165306 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.34", + "benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchStaticRoutesWithAntPathMatcher", + "mode" : "thrpt", + "threads" : 1, + "forks" : 5, + "jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java", + "jvmArgs" : [ + "-Dfile.encoding=UTF-8", + "-Duser.country=DE", + "-Duser.language=de", + "-Duser.variant" + ], + "jdkVersion" : "17.0.2", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "17.0.2+8-LTS", + "warmupIterations" : 5, + "warmupTime" : "10 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "10 s", + "measurementBatchSize" : 1, + "primaryMetric" : { + "score" : 8205.295027193253, + "scoreError" : 253.93643368708845, + "scoreConfidence" : [ + 7951.358593506165, + 8459.23146088034 + ], + "scorePercentiles" : { + "0.0" : 7494.992406948476, + "50.0" : 8250.043607694692, + "90.0" : 8682.981256843032, + "95.0" : 8702.07708088446, + "99.0" : 8703.611244566133, + "99.9" : 8703.611244566133, + "99.99" : 8703.611244566133, + "99.999" : 8703.611244566133, + "99.9999" : 8703.611244566133, + "100.0" : 8703.611244566133 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 7776.33432977133, + 7796.534803257248, + 7976.205818850183, + 7750.053487676609, + 8023.12289153653 + ], + [ + 7849.010784866861, + 7494.992406948476, + 7888.447757517585, + 7982.353388245457, + 8308.311812674916 + ], + [ + 8046.8394027785125, + 8184.99855687572, + 8209.36631211148, + 8363.572625644612, + 8460.14147216783 + ], + [ + 8658.496160722716, + 8654.928791158243, + 8703.611244566133, + 8698.497365627227, + 8672.637184320234 + ], + [ + 8296.063727961213, + 8396.558144577311, + 8250.043607694692, + 8381.721189986161, + 8309.5324122941 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.34", + "benchmark" : "org.xbib.net.benchmark.PathMatchingBenchmark.matchStaticRoutesWithPathPatternParser", + "mode" : "thrpt", + "threads" : 1, + "forks" : 5, + "jvm" : "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java", + "jvmArgs" : [ + "-Dfile.encoding=UTF-8", + "-Duser.country=DE", + "-Duser.language=de", + "-Duser.variant" + ], + "jdkVersion" : "17.0.2", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "17.0.2+8-LTS", + "warmupIterations" : 5, + "warmupTime" : "10 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "10 s", + "measurementBatchSize" : 1, + "primaryMetric" : { + "score" : 365562.56886055216, + "scoreError" : 18569.95078039208, + "scoreConfidence" : [ + 346992.61808016006, + 384132.51964094426 + ], + "scorePercentiles" : { + "0.0" : 325787.95505597815, + "50.0" : 374718.5719088665, + "90.0" : 392889.71549621335, + "95.0" : 398253.89062451763, + "99.0" : 399176.92717245553, + "99.9" : 399176.92717245553, + "99.99" : 399176.92717245553, + "99.999" : 399176.92717245553, + "99.9999" : 399176.92717245553, + "100.0" : 399176.92717245553 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 388662.8727349311, + 390749.43337413616, + 387249.9287947881, + 386499.16250676767, + 374425.2685252388 + ], + [ + 352727.7248253849, + 349601.7502852739, + 351124.6614028987, + 333424.47001959843, + 333542.1583066851 + ], + [ + 327552.12555473513, + 325787.95505597815, + 335012.9126031102, + 331770.3252415796, + 335493.0603334665 + ], + [ + 399176.92717245553, + 396100.1386793292, + 381384.6900987156, + 372390.3028315935, + 376181.92271467706 + ], + [ + 374718.5719088665, + 382122.1544260723, + 385231.46982420154, + 384140.7704616707, + 383993.4638316473 + ] + ] + }, + "secondaryMetrics" : { + } + } +] + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..7b2a7dd --- /dev/null +++ b/build.gradle @@ -0,0 +1,46 @@ +buildscript { + repositories { + maven { + url 'https://xbib.org/repository' + } + } + dependencies { + classpath "org.xbib.gradle.plugin:gradle-plugin-shadow:1.1.1" + } +} + +plugins { + id "de.marcphilipp.nexus-publish" version "0.4.0" + id "io.codearte.nexus-staging" version "0.21.1" + id "org.xbib.gradle.plugin.asciidoctor" version "2.5.2.1" +} + +wrapper { + gradleVersion = libs.versions.gradle.get() + distributionType = Wrapper.DistributionType.ALL +} + +ext { + user = 'xbib' + name = 'net' + description = 'Network classes for Java' + inceptionYear = '2016' + url = 'https://github.com/' + user + '/' + name + scmUrl = 'https://github.com/' + user + '/' + name + scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:ssh://git@github.com:' + user + '/' + name + '.git' + issueManagementSystem = 'Github' + issueManagementUrl = ext.scmUrl + '/issues' + licenseName = 'The Apache License, Version 2.0' + licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' +} + +subprojects { + apply from: rootProject.file('gradle/ide/idea.gradle') + apply from: rootProject.file('gradle/repositories/maven.gradle') + apply from: rootProject.file('gradle/compile/java.gradle') + apply from: rootProject.file('gradle/test/junit5.gradle') + apply from: rootProject.file('gradle/documentation/asciidoc.gradle') + apply from: rootProject.file('gradle/publish/maven.gradle') +} +apply from: rootProject.file('gradle/publish/sonatype.gradle') diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..14a1edc --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7f9ea7d --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +group = org.xbib +name = net +version = 3.0.0 + +org.gradle.warning.mode = ALL diff --git a/gradle/compile/java.gradle b/gradle/compile/java.gradle new file mode 100644 index 0000000..9cd3f3b --- /dev/null +++ b/gradle/compile/java.gradle @@ -0,0 +1,30 @@ +apply plugin: 'java-library' + +java { + modularity.inferModulePath.set(true) +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +jar { + manifest { + attributes('Implementation-Version': project.version) + } + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +tasks.withType(JavaCompile) { + options.compilerArgs.add('-Xlint:all,-exports') +} + +javadoc { + options.addStringOption('Xdoclint:none', '-quiet') +} diff --git a/gradle/documentation/asciidoc.gradle b/gradle/documentation/asciidoc.gradle new file mode 100644 index 0000000..87196cf --- /dev/null +++ b/gradle/documentation/asciidoc.gradle @@ -0,0 +1,13 @@ +apply plugin: 'org.xbib.gradle.plugin.asciidoctor' + +asciidoctor { + attributes 'source-highlighter': 'coderay', + toc: 'left', + doctype: 'book', + icons: 'font', + encoding: 'utf-8', + sectlink: true, + sectanchors: true, + linkattrs: true, + imagesdir: 'img' +} \ No newline at end of file diff --git a/gradle/ide/idea.gradle b/gradle/ide/idea.gradle new file mode 100644 index 0000000..5bd2095 --- /dev/null +++ b/gradle/ide/idea.gradle @@ -0,0 +1,8 @@ +apply plugin: 'idea' + +idea { + module { + outputDir file('build/classes/java/main') + testOutputDir file('build/classes/java/test') + } +} diff --git a/gradle/publish/ivy.gradle b/gradle/publish/ivy.gradle new file mode 100644 index 0000000..fe0a848 --- /dev/null +++ b/gradle/publish/ivy.gradle @@ -0,0 +1,27 @@ +apply plugin: 'ivy-publish' + +publishing { + repositories { + ivy { + url = "https://xbib.org/repo" + } + } + publications { + ivy(IvyPublication) { + from components.java + descriptor { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + author { + name = 'Jörg Prante' + url = 'http://example.com/users/jane' + } + descriptor.description { + text = rootProject.ext.description + } + } + } + } +} \ No newline at end of file diff --git a/gradle/publish/maven.gradle b/gradle/publish/maven.gradle new file mode 100644 index 0000000..3786359 --- /dev/null +++ b/gradle/publish/maven.gradle @@ -0,0 +1,64 @@ + +apply plugin: "de.marcphilipp.nexus-publish" + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + pom { + name = project.name + description = rootProject.ext.description + url = rootProject.ext.url + inceptionYear = rootProject.ext.inceptionYear + packaging = 'jar' + organization { + name = 'xbib' + url = 'https://xbib.org' + } + developers { + developer { + id = 'jprante' + name = 'Jörg Prante' + email = 'joergprante@gmail.com' + url = 'https://github.com/jprante' + } + } + scm { + url = rootProject.ext.scmUrl + connection = rootProject.ext.scmConnection + developerConnection = rootProject.ext.scmDeveloperConnection + } + issueManagement { + system = rootProject.ext.issueManagementSystem + url = rootProject.ext.issueManagementUrl + } + licenses { + license { + name = rootProject.ext.licenseName + url = rootProject.ext.licenseUrl + distribution = 'repo' + } + } + } + } + } +} + +if (project.hasProperty("signing.keyId")) { + apply plugin: 'signing' + signing { + sign publishing.publications.mavenJava + } +} + +if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) { + nexusPublishing { + repositories { + sonatype { + username = project.property('ossrhUsername') + password = project.property('ossrhPassword') + packageGroup = "org.xbib" + } + } + } +} diff --git a/gradle/publish/sonatype.gradle b/gradle/publish/sonatype.gradle new file mode 100644 index 0000000..e1813f3 --- /dev/null +++ b/gradle/publish/sonatype.gradle @@ -0,0 +1,11 @@ + +if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) { + + apply plugin: 'io.codearte.nexus-staging' + + nexusStaging { + username = project.property('ossrhUsername') + password = project.property('ossrhPassword') + packageGroup = "org.xbib" + } +} diff --git a/gradle/quality/sonarqube.gradle b/gradle/quality/sonarqube.gradle new file mode 100644 index 0000000..ec439b2 --- /dev/null +++ b/gradle/quality/sonarqube.gradle @@ -0,0 +1,50 @@ + +subprojects { + + sonarqube { + properties { + property "sonar.projectName", "${project.group} ${project.name}" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.tests", "src/test/java" + property "sonar.scm.provider", "git" + property "sonar.junit.reportsPath", "build/test-results/test/" + } + } + + + tasks.withType(Checkstyle) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } + } + + tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } + } + + checkstyle { + //configFile = rootProject.file('config/checkstyle/checkstyle.xml') + ignoreFailures = true + showViolations = true + } + + spotbugs { + effort = "max" + reportLevel = "low" + //includeFilter = file("findbugs-exclude.xml") + } + + tasks.withType(com.github.spotbugs.SpotBugsTask) { + ignoreFailures = true + reports { + xml.enabled = false + html.enabled = true + } + } +} \ No newline at end of file diff --git a/gradle/repositories/maven.gradle b/gradle/repositories/maven.gradle new file mode 100644 index 0000000..ec58acb --- /dev/null +++ b/gradle/repositories/maven.gradle @@ -0,0 +1,4 @@ +repositories { + mavenLocal() + mavenCentral() +} diff --git a/gradle/test/jmh.gradle b/gradle/test/jmh.gradle new file mode 100644 index 0000000..8c38e5c --- /dev/null +++ b/gradle/test/jmh.gradle @@ -0,0 +1,22 @@ +sourceSets { + jmh { + java.srcDirs = ['src/jmh/java'] + resources.srcDirs = ['src/jmh/resources'] + compileClasspath += sourceSets.main.runtimeClasspath + } +} + +dependencies { + jmhImplementation 'org.openjdk.jmh:jmh-core:1.34' + jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.34' +} + +task jmh(type: JavaExec, group: 'jmh', dependsOn: jmhClasses) { + mainClass.set('org.openjdk.jmh.Main') + classpath = sourceSets.jmh.compileClasspath + sourceSets.jmh.runtimeClasspath + project.file('build/reports/jmh').mkdirs() + args '-rf', 'json' + args '-rff', project.file('build/reports/jmh/result.json') +} + +classes.finalizedBy(jmhClasses) diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle new file mode 100644 index 0000000..ea3ee77 --- /dev/null +++ b/gradle/test/junit5.gradle @@ -0,0 +1,36 @@ +dependencies { + testImplementation libs.junit.jupiter.api + testImplementation libs.junit.jupiter.params + testImplementation libs.hamcrest + testRuntimeOnly libs.junit.jupiter.engine +} + +test { + useJUnitPlatform() + failFast = false + jvmArgs '--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED', + '--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED', + '--add-exports=java.base/sun.nio.ch=ALL-UNNAMED', + '--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED', + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + '--add-opens=java.base/java.nio=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED' + systemProperty 'java.util.logging.config.file', 'src/test/resources/logging.properties' + systemProperty 'io.netty.tryReflectionSetAccessible', 'true' + testLogging { + events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED' + } + afterSuite { desc, result -> + if (!desc.parent) { + println "\nTest result: ${result.resultType}" + println "Test summary: ${result.testCount} tests, " + + "${result.successfulTestCount} succeeded, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped" + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8fad3f5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/net-bouncycastle/build.gradle b/net-bouncycastle/build.gradle new file mode 100644 index 0000000..6e1a161 --- /dev/null +++ b/net-bouncycastle/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':net-security') + api libs.bouncycastle +} diff --git a/net-bouncycastle/src/main/java/module-info.java b/net-bouncycastle/src/main/java/module-info.java new file mode 100644 index 0000000..2d6a788 --- /dev/null +++ b/net-bouncycastle/src/main/java/module-info.java @@ -0,0 +1,10 @@ +import org.xbib.net.security.CertificateProvider; +import org.xbib.net.bouncycastle.BouncyCastleCertificateProvider; + +module org.xbib.net.bouncycastle { + requires org.xbib.net.security; + requires org.bouncycastle.pkix; + requires org.bouncycastle.provider; + exports org.xbib.net.bouncycastle; + provides CertificateProvider with BouncyCastleCertificateProvider; +} diff --git a/net-bouncycastle/src/main/java/org/xbib/net/bouncycastle/BouncyCastleCertificateProvider.java b/net-bouncycastle/src/main/java/org/xbib/net/bouncycastle/BouncyCastleCertificateProvider.java new file mode 100644 index 0000000..a57ef1e --- /dev/null +++ b/net-bouncycastle/src/main/java/org/xbib/net/bouncycastle/BouncyCastleCertificateProvider.java @@ -0,0 +1,66 @@ +package org.xbib.net.bouncycastle; + +import org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.operator.OperatorCreationException; +import org.xbib.net.security.CertificateProvider; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class BouncyCastleCertificateProvider implements CertificateProvider { + + private static final SecureRandom secureRandom = new SecureRandom(); + + public static final Provider BOUNCYCASTLE = new BouncyCastleProvider(); + + static { + if (Security.getProvider("BC") == null) { + Security.addProvider(BOUNCYCASTLE); + } + } + + public BouncyCastleCertificateProvider() { + } + + @SuppressWarnings("unchecked") + @Override + public Map.Entry> provide(InputStream key, String password, InputStream chain) + throws CertificateException, IOException { + PEMParser pemParser = new PEMParser(new InputStreamReader(key, StandardCharsets.US_ASCII)); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter() + .setProvider(BOUNCYCASTLE); + Object object = pemParser.readObject(); + KeyPair kp = converter.getKeyPair((PEMKeyPair) object); + PrivateKey privateKey = kp.getPrivate(); + return Map.entry(privateKey, new CertificateFactory().engineGenerateCertificates(chain)); + } + + @Override + public Map.Entry> provideSelfSigned(String fullQualifiedDomainName) throws CertificateException, IOException { + try { + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(); + selfSignedCertificate.generate(fullQualifiedDomainName, secureRandom, 2048); + return Map.entry(selfSignedCertificate.getPrivateKey(), List.of(selfSignedCertificate.getCertificate())); + } catch (NoSuchProviderException | NoSuchAlgorithmException | OperatorCreationException e) { + throw new IOException(e); + } + } +} diff --git a/net-bouncycastle/src/main/java/org/xbib/net/bouncycastle/SelfSignedCertificate.java b/net-bouncycastle/src/main/java/org/xbib/net/bouncycastle/SelfSignedCertificate.java new file mode 100644 index 0000000..52c6953 --- /dev/null +++ b/net-bouncycastle/src/main/java/org/xbib/net/bouncycastle/SelfSignedCertificate.java @@ -0,0 +1,164 @@ +package org.xbib.net.bouncycastle; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.util.PrivateKeyFactory; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder; +import org.bouncycastle.util.encoders.Base64; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; + +import static org.xbib.net.bouncycastle.BouncyCastleCertificateProvider.BOUNCYCASTLE; + +/** + * Generates a temporary self-signed certificate for testing purposes. + */ +public final class SelfSignedCertificate { + + /** Current time minus 1 year, just in case software clock goes back due to time synchronization */ + private static final Date DEFAULT_NOT_BEFORE = new Date(System.currentTimeMillis() - 86400000L * 365); + + /** The maximum possible value in X.509 specification: 9999-12-31 23:59:59 */ + private static final Date DEFAULT_NOT_AFTER = new Date(253402300799000L); + + private static final String BEGIN_KEY = "-----BEGIN PRIVATE KEY-----"; + + private static final String END_KEY = "-----END PRIVATE KEY-----"; + + private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + + private static final String END_CERT = "-----END CERTIFICATE-----"; + + private byte[] keyBytes; + + private byte[] certBytes; + + private X509CertificateHolder cert; + + private PrivateKey key; + + public SelfSignedCertificate() { + } + + /** + * Creates a new instance. + * + * @param fqdn a fully qualified domain name + * @param random the {@link SecureRandom} to use + * @param bits the number of bits of the generated private key + * @throws NoSuchAlgorithmException if algorithm does not exist + * @throws NoSuchProviderException if provider does not exist + * @throws OperatorCreationException if provider does not exist + * @throws IOException if generation fails + */ + public void generate(String fqdn, SecureRandom random, int bits) + throws IOException, NoSuchProviderException, NoSuchAlgorithmException, OperatorCreationException { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", "BC"); + keyGen.initialize(bits, random); + KeyPair keypair = keyGen.generateKeyPair(); + this.key = keypair.getPrivate(); + X500Name name = new X500Name("CN=" + fqdn); + SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(keypair.getPublic().getEncoded()); + X509v3CertificateBuilder certificateBuilder = + new X509v3CertificateBuilder(name, BigInteger.valueOf(System.currentTimeMillis()), + DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER, name, subjectPublicKeyInfo); + AlgorithmIdentifier sigAlgId = + new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256WithRSAEncryption"); + AlgorithmIdentifier digestAlgId = + new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); + AsymmetricKeyParameter caPrivateKeyParameters = PrivateKeyFactory.createKey(key.getEncoded()); + ContentSigner contentSigner = new BcRSAContentSignerBuilder(sigAlgId, digestAlgId) + .build(caPrivateKeyParameters); + this.cert = certificateBuilder.build(contentSigner); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(BEGIN_KEY.getBytes(StandardCharsets.US_ASCII)); + outputStream.write('\n'); + writeEncoded(key.getEncoded(), outputStream); + outputStream.write(END_KEY.getBytes(StandardCharsets.US_ASCII)); + outputStream.write('\n'); + this.keyBytes = outputStream.toByteArray(); + outputStream = new ByteArrayOutputStream(); + outputStream.write(BEGIN_CERT.getBytes(StandardCharsets.US_ASCII)); + outputStream.write('\n'); + writeEncoded(cert.getEncoded(), outputStream); + outputStream.write(END_CERT.getBytes(StandardCharsets.US_ASCII)); + outputStream.write('\n'); + this.certBytes = outputStream.toByteArray(); + } + + /** + * Returns the generated X.509 certificate file in PEM format. + * @return input stream of certificate + */ + public InputStream getCertificateInputStream() { + return new ByteArrayInputStream(certBytes); + } + + /** + * Returns the generated RSA private key file in PEM format. + * @return input stream of private key + */ + public InputStream getPrivateKeyInputStream() { + return new ByteArrayInputStream(keyBytes); + } + + /** + * Returns the generated RSA private key. + * @return private key + */ + public PrivateKey getPrivateKey() { + return key; + } + + public X509Certificate getCertificate() throws CertificateException { + return new JcaX509CertificateConverter() + .setProvider(BOUNCYCASTLE) + .getCertificate(cert); + } + + public void exportPEM(OutputStream outputStream) throws IOException { + outputStream.write(keyBytes); + outputStream.write(certBytes); + } + + private void writeEncoded(byte[] bytes, OutputStream outputStream) throws IOException { + byte[] buf = new byte[64]; + byte[] base64 = Base64.encode(bytes); + for (int i = 0; i < base64.length; i += buf.length) { + int index = 0; + while (index != buf.length) { + if ((i + index) >= base64.length) { + break; + } + buf[index] = base64[i + index]; + index++; + } + outputStream.write(buf, 0, index); + outputStream.write('\n'); + } + } +} diff --git a/net-bouncycastle/src/main/resources/META-INF/services/java.security.Provider b/net-bouncycastle/src/main/resources/META-INF/services/java.security.Provider new file mode 100644 index 0000000..50255d0 --- /dev/null +++ b/net-bouncycastle/src/main/resources/META-INF/services/java.security.Provider @@ -0,0 +1 @@ +org.bouncycastle.jce.provider.BouncyCastleProvider \ No newline at end of file diff --git a/net-bouncycastle/src/main/resources/META-INF/services/org.xbib.net.security.CertificateProvider b/net-bouncycastle/src/main/resources/META-INF/services/org.xbib.net.security.CertificateProvider new file mode 100644 index 0000000..d135cf9 --- /dev/null +++ b/net-bouncycastle/src/main/resources/META-INF/services/org.xbib.net.security.CertificateProvider @@ -0,0 +1 @@ +org.xbib.net.bouncycastle.BouncyCastleCertificateProvider \ No newline at end of file diff --git a/net-mime/src/main/java/module-info.java b/net-mime/src/main/java/module-info.java new file mode 100644 index 0000000..5914131 --- /dev/null +++ b/net-mime/src/main/java/module-info.java @@ -0,0 +1,5 @@ +module org.xbib.net.mime { + exports org.xbib.net.mime; + exports org.xbib.net.mime.stream; + requires java.logging; +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/Chunk.java b/net-mime/src/main/java/org/xbib/net/mime/Chunk.java new file mode 100644 index 0000000..dcb9a3a --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/Chunk.java @@ -0,0 +1,24 @@ +package org.xbib.net.mime; + +import java.io.IOException; +import java.nio.ByteBuffer; + +final class Chunk { + volatile Chunk next; + volatile Data data; + + public Chunk(Data data) { + this.data = data; + } + + /** + * Creates a new chunk and adds to linked list. + * + * @param dataHead of the linked list + * @param buf MIME part partial data + * @return created chunk + */ + public Chunk createNext(DataHead dataHead, ByteBuffer buf) throws IOException { + return next = new Chunk(data.createNext(dataHead, buf)); + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/Content.java b/net-mime/src/main/java/org/xbib/net/mime/Content.java new file mode 100644 index 0000000..160b8ae --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/Content.java @@ -0,0 +1,21 @@ +package org.xbib.net.mime; + +import java.nio.ByteBuffer; + +public final class Content implements MimeEvent { + + private final ByteBuffer buf; + + Content(ByteBuffer buf) { + this.buf = buf; + } + + @Override + public Type getEventType() { + return Type.CONTENT; + } + + public ByteBuffer getData() { + return buf; + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/Data.java b/net-mime/src/main/java/org/xbib/net/mime/Data.java new file mode 100644 index 0000000..33bfe7a --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/Data.java @@ -0,0 +1,43 @@ +package org.xbib.net.mime; + +import java.io.IOException; +import java.nio.ByteBuffer; + +interface Data { + + /** + * size of the chunk given by the parser + * + * @return size of the chunk + */ + int size(); + + /** + * TODO: should the return type be ByteBuffer ?? + * Return part's partial data. The data is read only. + * + * @return a byte array which contains {#size()} bytes. The returned + * array may be larger than {#size()} bytes and contains data + * from offset 0. + */ + byte[] read() throws IOException; + + /** + * Write this partial data to a file + * + * @param file to which the data needs to be written + * @return file pointer before the write operation(at which the data is + * written from) + */ + long writeTo(DataFile file) throws IOException; + + /** + * Factory method to create a Data. The implementation could + * be file based one or memory based one. + * + * @param dataHead start of the linked list of data objects + * @param buf contains partial content for a part + * @return Data + */ + Data createNext(DataHead dataHead, ByteBuffer buf) throws IOException; +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/DataFile.java b/net-mime/src/main/java/org/xbib/net/mime/DataFile.java new file mode 100644 index 0000000..6304986 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/DataFile.java @@ -0,0 +1,50 @@ +package org.xbib.net.mime; + +import java.io.File; +import java.io.IOException; + +final class DataFile { + private WeakDataFile weak; + private long writePointer; + + DataFile(File file) throws IOException { + writePointer=0; + weak = new WeakDataFile(this, file); + } + + void close() throws IOException { + weak.close(); + } + + /** + * Read data from the given file pointer position. + * + * @param pointer read position + * @param buf that needs to be filled + * @param offset the start offset of the data. + * @param length of data that needs to be read + */ + synchronized void read(long pointer, byte[] buf, int offset, int length ) throws IOException { + weak.read(pointer, buf, offset, length); + } + + void renameTo(File f) throws IOException { + weak.renameTo(f); + } + + /** + * Write data to the file + * + * @param data that needs to written to a file + * @param offset start offset in the data + * @param length no bytes to write + * @return file pointer before the write operation(or at which the + * data is written) + */ + synchronized long writeTo(byte[] data, int offset, int length) throws IOException { + long temp = writePointer; + writePointer = weak.writeTo(writePointer, data, offset, length); + return temp; + } + +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/DataHead.java b/net-mime/src/main/java/org/xbib/net/mime/DataHead.java new file mode 100644 index 0000000..7d00ba6 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/DataHead.java @@ -0,0 +1,248 @@ +package org.xbib.net.mime; + +import java.io.*; +import java.nio.ByteBuffer; + +/** + * Represents an attachment part in a MIME message. MIME message parsing is done + * lazily using a pull parser, so the part may not have all the data. {@link #read} + * and {@link #readOnce} may trigger the actual parsing the message. In fact, + * parsing of an attachment part may be triggered by calling {@link #read} methods + * on some other attachment parts. All this happens behind the scenes so the + * application developer need not worry about these details. + */ +final class DataHead { + + /** + * Linked list to keep the part's content + */ + volatile Chunk head, tail; + + /** + * If the part is stored in a file, non-null. + */ + DataFile dataFile; + + private final MimePart part; + + boolean readOnce; + volatile long inMemory; + + /** + * Used only for debugging. This records where readOnce() is called. + */ + private Throwable consumedAt; + + DataHead(MimePart part) { + this.part = part; + } + + void addBody(ByteBuffer buf) throws IOException { + synchronized(this) { + inMemory += buf.limit(); + } + if (tail != null) { + tail = tail.createNext(this, buf); + } else { + head = tail = new Chunk(new MemoryData(buf)); + } + } + + void doneParsing() { + } + + void moveTo(File f) throws IOException, MimeException { + if (dataFile != null) { + dataFile.renameTo(f); + } else { + try (OutputStream os = new FileOutputStream(f)) { + InputStream in = readOnce(); + byte[] buf = new byte[8192]; + int len; + while ((len = in.read(buf)) != -1) { + os.write(buf, 0, len); + } + } + } + } + + void close() throws IOException { + head = tail = null; + if (dataFile != null) { + dataFile.close(); + } + } + + /** + * Can get the attachment part's content multiple times. That means + * the full content needs to be there in memory or on the file system. + * Calling this method would trigger parsing for the part's data. So + * do not call this unless it is required(otherwise, just wrap MIMEPart + * into a object that returns InputStream for e.g DataHandler) + * + * @return data for the part's content + */ + public InputStream read() throws IOException, MimeException { + if (readOnce) { + throw new IllegalStateException("readOnce() is called before, read() cannot be called later."); + } + + // Trigger parsing for the part + while(tail == null) { + if (!part.msg.makeProgress()) { + throw new IllegalStateException("No such MIME Part: "+part); + } + } + + if (head == null) { + throw new IllegalStateException("Already read. Probably readOnce() is called before."); + } + return new ReadMultiStream(); + } + /** + * Used for an assertion. Returns true when readOnce() is not already called. + * or otherwise throw an exception. + * + *

+ * Calling this method also marks the stream as 'consumed' + * + * @return true if readOnce() is not called before + */ + @SuppressWarnings("ThrowableInitCause") + private boolean unconsumed() { + if (consumedAt != null) { + AssertionError error = new AssertionError("readOnce() is already called before. See the nested exception from where it's called."); + error.initCause(consumedAt); + throw error; + } + consumedAt = new Exception().fillInStackTrace(); + return true; + } + + /** + * Can get the attachment part's content only once. The content + * will be lost after the method. Content data is not be stored + * on the file system or is not kept in the memory for the + * following case: + * - Attachement parts contents are accessed sequentially + * + * In general, take advantage of this when the data is used only + * once. + * + * @return data for the part's content + */ + public InputStream readOnce() throws IOException, MimeException { + unconsumed(); + if (readOnce) { + throw new MimeException("readOnce() is called before. It can only be called once."); + } + readOnce = true; + while(tail == null) { + if (!part.msg.makeProgress() && tail == null) { + throw new MimeException("No such Part: "+part); + } + } + InputStream in = new ReadOnceStream(); + head = null; + return in; + } + + class ReadMultiStream extends InputStream { + Chunk current; + int offset; + int len; + byte[] buf; + boolean closed; + + public ReadMultiStream() throws IOException { + this.current = head; + len = current.data.size(); + buf = current.data.read(); + } + + @Override + public int read(byte b[], int off, int sz) throws IOException { + try { + if (!fetch()) { + return -1; + } + } catch (MimeException e) { + throw new IOException(e); + } + + sz = Math.min(sz, len-offset); + System.arraycopy(buf,offset,b,off,sz); + offset += sz; + return sz; + } + + @Override + public int read() throws IOException { + try { + if (!fetch()) { + return -1; + } + } catch (MimeException e) { + throw new IOException(e); + } + return (buf[offset++] & 0xff); + } + + void adjustInMemoryUsage() { + // Nothing to do in this case. + } + + /** + * Gets to the next chunk if we are done with the current one. + * @return true if any data available + * @throws IOException when i/o error + */ + private boolean fetch() throws IOException, MimeException { + if (closed) { + throw new IOException("Stream already closed"); + } + if (current == null) { + return false; + } + + while(offset==len) { + while(!part.parsed && current.next == null) { + part.msg.makeProgress(); + } + current = current.next; + + if (current == null) { + return false; + } + adjustInMemoryUsage(); + this.offset = 0; + this.buf = current.data.read(); + this.len = current.data.size(); + } + return true; + } + + @Override + public void close() throws IOException { + super.close(); + current = null; + closed = true; + } + } + + final class ReadOnceStream extends ReadMultiStream { + + public ReadOnceStream() throws IOException { + } + + @Override + void adjustInMemoryUsage() { + synchronized(DataHead.this) { + inMemory -= current.data.size(); // adjust current memory usage + } + } + + } + + +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/EndMessage.java b/net-mime/src/main/java/org/xbib/net/mime/EndMessage.java new file mode 100644 index 0000000..204c200 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/EndMessage.java @@ -0,0 +1,14 @@ +package org.xbib.net.mime; + +public final class EndMessage implements MimeEvent { + + static final EndMessage INSTANCE = new EndMessage(); + + public EndMessage() { + } + + @Override + public Type getEventType() { + return Type.END_MESSAGE; + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/EndPart.java b/net-mime/src/main/java/org/xbib/net/mime/EndPart.java new file mode 100644 index 0000000..d09b13e --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/EndPart.java @@ -0,0 +1,14 @@ +package org.xbib.net.mime; + +public final class EndPart implements MimeEvent { + + public static final EndPart INSTANCE = new EndPart(); + + public EndPart() { + } + + @Override + public Type getEventType() { + return Type.END_PART; + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/FileData.java b/net-mime/src/main/java/org/xbib/net/mime/FileData.java new file mode 100644 index 0000000..2f378f3 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/FileData.java @@ -0,0 +1,45 @@ +package org.xbib.net.mime; + +import java.io.IOException; +import java.nio.ByteBuffer; + +final class FileData implements Data { + + private final DataFile file; + + private final long pointer; + + private final int length; + + FileData(DataFile file, ByteBuffer buf) throws IOException { + this(file, file.writeTo(buf.array(), 0, buf.limit()), buf.limit()); + } + + FileData(DataFile file, long pointer, int length) { + this.file = file; + this.pointer = pointer; + this.length = length; + } + + @Override + public byte[] read() throws IOException { + byte[] buf = new byte[length]; + file.read(pointer, buf, 0, length); + return buf; + } + + @Override + public long writeTo(DataFile file) { + throw new IllegalStateException(); + } + + @Override + public int size() { + return length; + } + + @Override + public Data createNext(DataHead dataHead, ByteBuffer buf) throws IOException { + return new FileData(file, buf); + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/Header.java b/net-mime/src/main/java/org/xbib/net/mime/Header.java new file mode 100644 index 0000000..531fb1c --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/Header.java @@ -0,0 +1,19 @@ +package org.xbib.net.mime; + +public interface Header { + + /** + * Returns the name of this header. + * + * @return name of the header + */ + String getName(); + + /** + * Returns the value of this header. + * + * @return value of the header + */ + String getValue(); +} + diff --git a/net-mime/src/main/java/org/xbib/net/mime/Headers.java b/net-mime/src/main/java/org/xbib/net/mime/Headers.java new file mode 100644 index 0000000..3996d95 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/Headers.java @@ -0,0 +1,18 @@ +package org.xbib.net.mime; + +public final class Headers implements MimeEvent { + MimeParser.InternetHeaders ih; + + Headers(MimeParser.InternetHeaders ih) { + this.ih = ih; + } + + @Override + public Type getEventType() { + return Type.HEADERS; + } + + public MimeParser.InternetHeaders getHeaders() { + return ih; + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MemoryData.java b/net-mime/src/main/java/org/xbib/net/mime/MemoryData.java new file mode 100644 index 0000000..89b5825 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MemoryData.java @@ -0,0 +1,85 @@ +package org.xbib.net.mime; + +import java.nio.ByteBuffer; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Keeps the Part's partial content data in memory. + */ +final class MemoryData implements Data { + + private static final Logger LOGGER = Logger.getLogger(MemoryData.class.getName()); + + private final byte[] data; + + private final int len; + + private final boolean isOnlyMemory; + + private final long threshold; + + private final File tempDir; + + MemoryData(ByteBuffer byteBuffer) { + this(byteBuffer, false, 1048576L, new File(System.getProperty("java.io.tmpdir"))); + } + + MemoryData(ByteBuffer buf, boolean isOnlyMemory, long threshold, File tempDir) { + data = buf.array(); // TODO + len = buf.limit(); + this.isOnlyMemory = isOnlyMemory; + this.threshold = threshold; + this.tempDir = tempDir; + } + + @Override + public int size() { + return len; + } + + @Override + public byte[] read() { + return data; + } + + @Override + public long writeTo(DataFile file) throws IOException { + return file.writeTo(data, 0, len); + } + + @Override + public Data createNext(DataHead dataHead, ByteBuffer buf) throws IOException { + if (!isOnlyMemory && dataHead.inMemory >= threshold) { + //String prefix = config.getTempFilePrefix(); + //String suffix = config.getTempFileSuffix(); + File tempFile = createTempFile("MIME", ".mime", tempDir); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Created temp file = {0}", tempFile); + } + dataHead.dataFile = new DataFile(tempFile); + if (dataHead.head != null) { + for (Chunk c = dataHead.head; c != null; c = c.next) { + long pointer = c.data.writeTo(dataHead.dataFile); + c.data = new FileData(dataHead.dataFile, pointer, len); + } + } + return new FileData(dataHead.dataFile, buf); + } else { + return new MemoryData(buf, isOnlyMemory, threshold, tempDir); + } + } + + private static File createTempFile(String prefix, String suffix, File dir) throws IOException { + if (dir != null) { + Path path = dir.toPath(); + return Files.createTempFile(path, prefix, suffix).toFile(); + } else { + return Files.createTempFile(prefix, suffix).toFile(); + } + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeEvent.java b/net-mime/src/main/java/org/xbib/net/mime/MimeEvent.java new file mode 100644 index 0000000..eb08564 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeEvent.java @@ -0,0 +1,24 @@ +package org.xbib.net.mime; + +public interface MimeEvent { + + /** + * Returns a event for parser's current cursor location in the MIME message. + * + * {@link Type#START_MESSAGE} and {@link Type#START_MESSAGE} events + * are generated only once. + * + * {@link Type#START_PART}, {@link Type#END_PART}, {@link Type#HEADERS} + * events are generated only once for each attachment part. + * + * {@link Type#CONTENT} event may be generated more than once for an attachment + * part. + * + * @return event type + */ + Type getEventType(); + + enum Type { + START_MESSAGE, START_PART, HEADERS, CONTENT, END_PART, END_MESSAGE + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeException.java b/net-mime/src/main/java/org/xbib/net/mime/MimeException.java new file mode 100644 index 0000000..7b298ac --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeException.java @@ -0,0 +1,17 @@ +package org.xbib.net.mime; + +@SuppressWarnings("serial") +public class MimeException extends Exception { + + public MimeException(String message) { + super(message); + } + + public MimeException(Throwable throwable) { + super(throwable); + } + + public MimeException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeMessage.java b/net-mime/src/main/java/org/xbib/net/mime/MimeMessage.java new file mode 100644 index 0000000..f7214e0 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeMessage.java @@ -0,0 +1,223 @@ +package org.xbib.net.mime; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents MIME message. MIME message parsing is done lazily using a + * pull parser. + */ +public class MimeMessage implements Closeable { + + private static final Logger LOGGER = Logger.getLogger(MimeMessage.class.getName()); + + private final InputStream in; + private final Iterator it; + private boolean parsed; + private MimePart currentPart; + private int currentIndex; + + private final List partsList = new ArrayList<>(); + private final Map partsMap = new HashMap<>(); + + /** + * Creates a MIME message from the content's stream. The content stream + * is closed when EOF is reached. + * + * @param in MIME message stream + * @param boundary the separator for parts(pass it without --) + */ + public MimeMessage(InputStream in, String boundary) { + this.in = in; + MimeParser parser = new MimeParser(in, boundary); + it = parser.iterator(); + //if (config.isParseEagerly()) { + // parseAll(); + //} + } + + /** + * Gets all the attachments by parsing the entire MIME message. Avoid + * this if possible since it is an expensive operation. + * + * @return list of attachments. + */ + public List getAttachments() throws MimeException, IOException { + if (!parsed) { + parseAll(); + } + return partsList; + } + + /** + * Creates nth attachment lazily. It doesn't validate + * if the message has so many attachments. To + * do the validation, the message needs to be parsed. + * The parsing of the message is done lazily and is done + * while reading the bytes of the part. + * + * @param index sequential order of the part. starts with zero. + * @return attachemnt part + */ + public MimePart getPart(int index) throws MimeException { + LOGGER.log(Level.FINE, "index={0}", index); + MimePart part = (index < partsList.size()) ? partsList.get(index) : null; + if (parsed && part == null) { + throw new MimeException("There is no " + index + " attachment part "); + } + if (part == null) { + // Parsing will done lazily and will be driven by reading the part + part = new MimePart(this); + partsList.add(index, part); + } + LOGGER.log(Level.FINE, "Got attachment at index=" + index + " attachment=" + part); + return part; + } + + /** + * Creates a lazy attachment for a given Content-ID. It doesn't validate + * if the message contains an attachment with the given Content-ID. To + * do the validation, the message needs to be parsed. The parsing of the + * message is done lazily and is done while reading the bytes of the part. + * + * @param contentId Content-ID of the part, expects Content-ID without {@code <, >} + * @return attachemnt part + */ + public MimePart getPart(String contentId) throws MimeException { + LOGGER.log(Level.FINE, "Content-ID = " + contentId); + MimePart part = getDecodedCidPart(contentId); + if (parsed && part == null) { + throw new MimeException("There is no part with Content-ID = " + contentId); + } + if (part == null) { + // Parsing is done lazily and is driven by reading the part + part = new MimePart(this, contentId); + partsMap.put(contentId, part); + } + LOGGER.log(Level.FINE, "got part for Content-ID = " + contentId + " part = " + part); + return part; + } + + // this is required for Indigo interop, it writes content-id without escaping + private MimePart getDecodedCidPart(String cid) { + MimePart part = partsMap.get(cid); + if (part == null) { + if (cid.indexOf('%') != -1) { + // TODO do not use URLDecoder + String tempCid = URLDecoder.decode(cid, StandardCharsets.UTF_8); + part = partsMap.get(tempCid); + } + } + return part; + } + + /** + * Parses the whole MIME message eagerly + */ + public final void parseAll() throws MimeException, IOException { + while (makeProgress()) { + // Nothing to do + } + } + + /** + * Closes all parsed {@link MimePart parts} and cleans up + * any resources that are held by this {@code MimeMessage} (for e.g. deletes temp files). + * This method is safe to call even if parsing of message failed. + */ + @Override + public void close() throws IOException { + close(partsList); + close(partsMap.values()); + } + + private void close(final Collection parts) throws IOException { + for (final MimePart part : parts) { + part.close(); + } + } + + /** + * Parses the MIME message in a pull fashion. + * + * @return false if the parsing is completed. + */ + public synchronized boolean makeProgress() throws MimeException, IOException { + if (!it.hasNext()) { + return false; + } + MimeEvent event = it.next(); + switch (event.getEventType()) { + case START_MESSAGE: + LOGGER.log(Level.FINE, "MIMEEvent=" + MimeEvent.Type.START_MESSAGE); + break; + + case START_PART: + LOGGER.log(Level.FINE, "MIMEEvent=" + MimeEvent.Type.START_PART); + break; + + case HEADERS: + LOGGER.log(Level.FINE, "MIMEEvent=" + MimeEvent.Type.HEADERS); + Headers headers = (Headers) event; + MimeParser.InternetHeaders ih = headers.getHeaders(); + List cids = ih.getHeader("content-id"); + String cid = (cids != null) ? cids.get(0) : currentIndex + ""; + if (cid.length() > 2 && cid.charAt(0) == '<') { + cid = cid.substring(1, cid.length() - 1); + } + MimePart listPart = (currentIndex < partsList.size()) ? partsList.get(currentIndex) : null; + MimePart mapPart = getDecodedCidPart(cid); + if (listPart == null && mapPart == null) { + currentPart = getPart(cid); + partsList.add(currentIndex, currentPart); + } else if (listPart == null) { + currentPart = mapPart; + partsList.add(currentIndex, mapPart); + } else if (mapPart == null) { + currentPart = listPart; + currentPart.setContentId(cid); + partsMap.put(cid, currentPart); + } else if (listPart != mapPart) { + throw new MimeException("ereated two different attachments using Content-ID and index"); + } + currentPart.setHeaders(ih); + break; + + case CONTENT: + LOGGER.log(Level.FINER, "MIMEEvent=" + MimeEvent.Type.CONTENT); + Content content = (Content) event; + ByteBuffer buf = content.getData(); + currentPart.addBody(buf); + break; + + case END_PART: + LOGGER.log(Level.FINE, "MIMEEvent=" + MimeEvent.Type.END_PART); + currentPart.doneParsing(); + ++currentIndex; + break; + + case END_MESSAGE: + LOGGER.log(Level.FINE, "MIMEEvent=" + MimeEvent.Type.END_MESSAGE); + parsed = true; + try { + in.close(); + } catch (IOException ioe) { + throw new MimeException(ioe); + } + break; + } + return true; + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeMultipart.java b/net-mime/src/main/java/org/xbib/net/mime/MimeMultipart.java new file mode 100644 index 0000000..477dfcd --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeMultipart.java @@ -0,0 +1,16 @@ +package org.xbib.net.mime; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Map; + +public interface MimeMultipart { + + Map getHeaders(); + + int getLength(); + + ByteBuffer getBody(); + + Charset getCharset(); +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeMultipartHandler.java b/net-mime/src/main/java/org/xbib/net/mime/MimeMultipartHandler.java new file mode 100644 index 0000000..63a8fdc --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeMultipartHandler.java @@ -0,0 +1,7 @@ +package org.xbib.net.mime; + +@FunctionalInterface +public interface MimeMultipartHandler { + + void handle(MimeMultipart multipart); +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeMultipartParser.java b/net-mime/src/main/java/org/xbib/net/mime/MimeMultipartParser.java new file mode 100644 index 0000000..32fbcf6 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeMultipartParser.java @@ -0,0 +1,267 @@ +package org.xbib.net.mime; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A MIME multi part message parser (RFC 2046). + */ +public class MimeMultipartParser { + + private static final Logger logger = Logger.getLogger(MimeMultipartParser.class.getName()); + + private final byte[] boundary; + + private final String subType; + + private String type; + + public MimeMultipartParser(String contentType) throws MimeException { + Objects.requireNonNull(contentType); + int pos = contentType.indexOf(';'); + this.type = pos >= 0 ? contentType.substring(0, pos) : contentType; + this.type = type.trim().toLowerCase(); + this.subType = "multipart".equals(type) ? null : type.substring(10).trim(); + Map headers = parseHeaderLine(contentType); + this.boundary = headers.containsKey("boundary") ? headers.get("boundary").getBytes(StandardCharsets.US_ASCII) : new byte[]{}; + logger.log(Level.INFO, "headers = " + headers + " boundary = " + new String(boundary)); + } + + public String getType() { + return type; + } + + public String getSubType() { + return subType; + } + + public void parse(ByteBuffer byteBuffer, MimeMultipartHandler handler) throws MimeException { + if (boundary == null) { + return; + } + StringBuilder sb = new StringBuilder(); + boolean inHeader = true; + boolean inBody = false; + Integer start = null; + Map headers = new LinkedHashMap<>(); + int eol = 0; + byte[] buffer = new byte[byteBuffer.remaining()]; + byteBuffer.get(buffer); + for (int i = 0; i < buffer.length; i++) { + byte b = buffer[i]; + if (inHeader) { + switch (b) { + case '\r': + break; + case '\n': + if (sb.length() > 0) { + List s = new ArrayList<>(); + StringTokenizer tokenizer = new StringTokenizer(sb.toString(), ":"); + while (tokenizer.hasMoreElements()) { + s.add(tokenizer.nextToken()); + } + String k = s.size() > 0 ? s.get(0) : ""; + String v = s.size() > 1 ? s.get(1) : ""; + if (!k.startsWith("--")) { + headers.put(k.toLowerCase(), v.trim()); + } + eol = 0; + sb.setLength(0); + } else { + eol++; + if (eol >= 1) { + eol = 0; + sb.setLength(0); + inHeader = false; + inBody = true; + } + } + break; + default: + eol = 0; + sb.append((char) b); + break; + } + } + if (inBody) { + int len = headers.containsKey("content-length") ? Integer.parseInt(headers.get("content-length")) : -1; + if (len > 0) { + inBody = false; + inHeader = true; + } else { + if (b != ('\r') && b != ('\n')) { + start = i; + } + if (start != null) { + i = indexOf(buffer, boundary, start, buffer.length); + if (i == -1) { + throw new MimeException("boundary not found"); + } + int l = i - start; + if (l > 4) { + l = l - 4; + } + ByteBuffer body = ByteBuffer.wrap(buffer, start, l); + Map m = new LinkedHashMap<>(); + for (Map.Entry entry : headers.entrySet()) { + m.putAll(parseHeaderLine(entry.getValue())); + } + headers.putAll(m); + Charset charset = StandardCharsets.ISO_8859_1; + if (m.containsKey("charset")) { + charset = Charset.forName(m.get("charset")); + } + if (handler != null) { + handler.handle(new MimePart(headers, body, l, charset)); + } + inBody = false; + inHeader = true; + headers = new LinkedHashMap<>(); + start = null; + eol = -1; + } + } + } + } + } + + private static Map parseHeaderLine(String line) throws MimeException { + Map params = new LinkedHashMap<>(); + int pos = line.indexOf(';'); + String spec = line.substring(pos + 1); + if (pos < 0) { + return params; + } + String key = ""; + String value; + boolean inKey = true; + boolean inString = false; + int start = 0; + int i; + for (i = 0; i < spec.length(); i++) { + switch (spec.charAt(i)) { + case '=': + if (inKey) { + key = spec.substring(start, i).trim().toLowerCase(); + start = i + 1; + inKey = false; + } else if (!inString) { + throw new MimeException(" value has illegal character '=' at " + i + ": " + spec); + } + break; + case ';': + if (inKey) { + if (spec.substring(start, i).trim().length() > 0) { + throw new MimeException(" parameter missing value at " + i + ": " + spec); + } else { + throw new MimeException("parameter key has illegal character ';' at " + i + ": " + spec); + } + } else if (!inString) { + value = spec.substring(start, i).trim(); + params.put(key, value); + key = null; + start = i + 1; + inKey = true; + } + break; + case '"': + if (inKey) { + throw new MimeException("key has illegal character '\"' at " + i + ": " + spec); + } else if (inString) { + value = spec.substring(start, i).trim(); + params.put(key, value); + key = null; + for (i++; i < spec.length() && spec.charAt(i) != (';'); i++) { + if (!Character.isWhitespace(spec.charAt(i))) { + throw new MimeException(" value has garbage after quoted string at " + i + ": " + spec); + } + } + start = i + 1; + inString = false; + inKey = true; + } else { + if (spec.substring(start, i).trim().length() > 0) { + throw new MimeException("value has garbage before quoted string at " + i + ": " + spec); + } + start = i + 1; + inString = true; + } + break; + } + } + if (inKey) { + if (pos > start && spec.substring(start, i).trim().length() > 0) { + throw new MimeException(" missing value at " + i + ": " + spec); + } + } else if (!inString) { + value = spec.substring(start, i).trim(); + params.put(key, value); + } else { + throw new MimeException("has an unterminated quoted string: " + spec); + } + return params; + } + + private static int indexOf(byte[] array, byte[] target, int start, int end) { + if (target.length == 0) { + return 0; + } + outer: + for (int i = start; i < end - target.length + 1; i++) { + for (int j = 0; j < target.length; j++) { + if (array[i + j] != target[j]) { + continue outer; + } + } + return i; + } + return -1; + } + + public static class MimePart implements MimeMultipart { + + private final Map headers; + + private final ByteBuffer body; + + private final int length; + + private final Charset charset; + + public MimePart(Map headers, ByteBuffer body, int length, Charset charSet) { + this.headers = headers; + this.body = body; + this.length = length; + this.charset = charSet; + } + + @Override + public Map getHeaders() { + return headers; + } + + @Override + public ByteBuffer getBody() { + return body; + } + + @Override + public Charset getCharset() { + return charset; + } + + @Override + public int getLength() { + return length; + } + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeParser.java b/net-mime/src/main/java/org/xbib/net/mime/MimeParser.java new file mode 100644 index 0000000..8e5550b --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeParser.java @@ -0,0 +1,558 @@ +package org.xbib.net.mime; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MimeParser implements Iterable { + + private static final Logger logger = Logger.getLogger(MimeParser.class.getName()); + + private static final int NO_WHITESPACE_LENGTH = 1000; + + private final InputStream inputStream; + + private final byte[] boundaryBytes; + + private final int boundaryLength; + + private final int[] badCharacterShift; + + private final int[] goodSuffixShiftTable; + + private final int capacity; + + private final int chunkSize; + + private State state; + + private boolean parsed; + + private boolean done; + + private boolean eof; + + private byte[] buf; + + private int length; + + private boolean bol; + + private int offset; + + public MimeParser(InputStream inputStream, String boundary) { + this(inputStream, boundary, 8192); + } + + public MimeParser(InputStream inputStream, String boundary, int chunkSize) { + this.inputStream = inputStream; + this.boundaryBytes = getBytes("--" + boundary); + this.boundaryLength = boundaryBytes.length; + this.chunkSize = chunkSize; + this.state = State.START_MESSAGE; + this.goodSuffixShiftTable = new int[boundaryLength]; + this.badCharacterShift = new int[128]; + this.done = false; + compileBoundaryPattern(); + this.capacity = chunkSize + 2 + boundaryLength + 4 + NO_WHITESPACE_LENGTH; + this.buf = new byte[capacity]; + } + + private static byte[] getBytes(String s) { + char[] chars = s.toCharArray(); + int size = chars.length; + byte[] bytes = new byte[size]; + for (int i = 0; i < size; i++) { + bytes[i] = (byte) chars[i]; + } + return bytes; + } + + @Override + public MimeEventIterator iterator() { + return new MimeEventIterator(); + } + + /** + * Collects the headers for the current part by parsing the stream. + * + * @return headers for the current part + */ + private InternetHeaders readHeaders() throws IOException { + if (!eof) { + fillBuf(); + } + this.offset = 0; + return new InternetHeaders(); + } + + /** + * Reads and saves the part of the current attachment part's content. + * At the end of this method, buf should have the remaining data + * at index 0. + * + * @return a chunk of the part's content + */ + private ByteBuffer readBody() throws MimeException, IOException { + if (!eof) { + fillBuf(); + } + int start = matchBoundary(buf, 0, length); + if (start == -1) { + int chunkSize = eof ? length : this.chunkSize; + if (eof) { + done = true; + throw new MimeException("reached EOF, but there is no closing MIME boundary"); + } + return adjustBuf(chunkSize, length - chunkSize); + } + int chunkLen = start; + if (bol && start == 0) { + } else if (start > 0 && (buf[start - 1] == '\n' || buf[start - 1] == '\r')) { + --chunkLen; + if (buf[start - 1] == '\n' && start > 1 && buf[start - 2] == '\r') { + --chunkLen; + } + } else { + return adjustBuf(start + 1, length - start - 1); + } + if (start + boundaryLength + 1 < length && buf[start + boundaryLength] == '-' && buf[start + boundaryLength + 1] == '-') { + state = State.END_PART; + done = true; + return adjustBuf(chunkLen, 0); + } + int whitespaceLength = 0; + for (int i = start + boundaryLength; i < length && (buf[i] == ' ' || buf[i] == '\t'); i++) { + ++whitespaceLength; + } + if (start + boundaryLength + whitespaceLength < length && buf[start + boundaryLength + whitespaceLength] == '\n') { + state = State.END_PART; + return adjustBuf(chunkLen, length - start - boundaryLength - whitespaceLength - 1); + } else if (start + boundaryLength + whitespaceLength + 1 < length && buf[start + boundaryLength + whitespaceLength] == '\r' && + buf[start + boundaryLength + whitespaceLength + 1] == '\n') { + state = State.END_PART; + return adjustBuf(chunkLen, length - start - boundaryLength - whitespaceLength - 2); + } else if (start + boundaryLength + whitespaceLength + 1 < length) { + return adjustBuf(chunkLen + 1, length - chunkLen - 1); + } else if (eof) { + done = true; + throw new MimeException("reached EOF, but there is no closing MIME boundary"); + } + return adjustBuf(chunkLen, length - chunkLen); + } + + /** + * Returns a chunk from the original buffer. A new buffer is + * created with the remaining bytes. + * + * @param chunkSize create a chunk with these many bytes + * @param remaining bytes from the end of the buffer that need to be copied to + * the beginning of the new buffer + * @return chunk + */ + private ByteBuffer adjustBuf(int chunkSize, int remaining) { + byte[] temp = buf; + buf = new byte[Math.min(capacity, remaining)]; + System.arraycopy(temp, length - remaining, buf, 0, remaining); + length = remaining; + return ByteBuffer.wrap(temp, 0, chunkSize); + } + + /** + * Skips the preamble to find the first attachment part + */ + private void skipPreamble() throws MimeException, IOException { + while (true) { + if (!eof) { + fillBuf(); + } + int start = matchBoundary(buf, 0, length); + if (start == -1) { + if (eof) { + throw new MimeException("missing start boundary"); + } else { + adjustBuf(length - boundaryLength + 1, boundaryLength - 1); + continue; + } + } + if (start > chunkSize) { + adjustBuf(start, length - start); + continue; + } + int whitespaceLength = 0; + for (int i = start + boundaryLength; i < length && (buf[i] == ' ' || buf[i] == '\t'); i++) { + ++whitespaceLength; + } + if (start + boundaryLength + whitespaceLength < length && (buf[start + boundaryLength + whitespaceLength] == '\n' || buf[start + boundaryLength + whitespaceLength] == '\r')) { + if (buf[start + boundaryLength + whitespaceLength] == '\n') { + adjustBuf(start + boundaryLength + whitespaceLength + 1, length - start - boundaryLength - whitespaceLength - 1); + break; + } else if (start + boundaryLength + whitespaceLength + 1 < length && buf[start + boundaryLength + whitespaceLength + 1] == '\n') { + adjustBuf(start + boundaryLength + whitespaceLength + 2, length - start - boundaryLength - whitespaceLength - 2); + break; + } + } + adjustBuf(start + 1, length - start - 1); + } + logger.log(Level.FINER, "skipped the preamble. buffer length =" + length); + } + + /** + * Boyer-Moore search method. Copied from java.util.regex.Pattern.java + * Pre calculates arrays needed to generate the bad character + * shift and the good suffix shift. Only the last seven bits + * are used to see if chars match; This keeps the tables small + * and covers the heavily used ASCII range, but occasionally + * results in an aliased match for the bad character shift. + */ + private void compileBoundaryPattern() { + int i; + int j; + for (i = 0; i < boundaryLength; i++) { + badCharacterShift[boundaryBytes[i] & 0x7F] = i + 1; + } + NEXT: + for (i = boundaryLength; i > 0; i--) { + for (j = boundaryLength - 1; j >= i; j--) { + if (boundaryBytes[j] == boundaryBytes[j - i]) { + goodSuffixShiftTable[j - 1] = i; + } else { + continue NEXT; + } + } + while (j > 0) { + goodSuffixShiftTable[--j] = i; + } + } + goodSuffixShiftTable[boundaryLength - 1] = 1; + } + + /** + * Finds the boundary in the given buffer using Boyer-Moore algo. + * Copied from java.util.regex.Pattern.java + * + * @param buffer boundary to be searched + * @param off start index + * @param len number of bytes + * @return -1 if there is no match or index where the match starts + */ + private int matchBoundary(byte[] buffer, int off, int len) { + logger.log(Level.FINER, "matching boundary = " + new String(boundaryBytes) + " len = " + boundaryLength); + logger.log(Level.FINER, "off = " + off + " len = " + len + " buffer = " + new String(buffer, off, len)); + int last = len - boundaryLength; + NEXT: + while (off <= last) { + for (int j = boundaryLength - 1; j >= 0; j--) { + byte ch = buffer[off + j]; + if (ch != boundaryBytes[j]) { + off += Math.max(j + 1 - badCharacterShift[ch & 0x7F], goodSuffixShiftTable[j]); + continue NEXT; + } + } + logger.log(Level.FINER, "found at " + off); + return off; + } + logger.log(Level.FINER, "not found!"); + return -1; + } + + /** + * Fills the remaining buf to the full capacity + */ + private void fillBuf() throws IOException { + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "before fillBuf(): buffer len=" + length); + } + while (length < buf.length) { + int read = inputStream.read(buf, length, buf.length - length); + if (read == -1) { + eof = true; + if (logger.isLoggable(Level.FINE)) { + logger.fine("closing the input stream"); + } + inputStream.close(); + break; + } else { + length += read; + } + } + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "after fillBuf(): buffer len=" + length); + } + } + + private void doubleBuf() throws IOException { + byte[] temp = new byte[2 * length]; + System.arraycopy(buf, 0, temp, 0, length); + buf = temp; + if (!eof) { + fillBuf(); + } + } + + private String readLine() throws IOException { + int headerLength = 0; + int whitespaceLength = 0; + while (offset + headerLength < length) { + if (buf[offset + headerLength] == '\n') { + whitespaceLength = 1; + break; + } + if (offset + headerLength + 1 == length) { + doubleBuf(); + } + if (offset + headerLength + 1 >= length) { + return null; + } + if (buf[offset + headerLength] == '\r' && buf[offset + headerLength + 1] == '\n') { + whitespaceLength = 2; + break; + } + ++headerLength; + } + if (headerLength == 0) { + adjustBuf(offset + whitespaceLength, length - offset - whitespaceLength); + return null; + } + String hdr = new String(buf, offset, headerLength, StandardCharsets.ISO_8859_1); + offset += headerLength + whitespaceLength; + return hdr; + } + + private enum State { + START_MESSAGE, SKIP_PREAMBLE, START_PART, HEADERS, BODY, END_PART, END_MESSAGE + } + + static class Hdr implements Header { + + private final String name; + + private String line; + + private Hdr(String l) { + int i = l.indexOf(':'); + if (i < 0) { + name = l.trim(); + } else { + name = l.substring(0, i).trim(); + } + line = l; + } + + Hdr(String n, String v) { + name = n; + line = n + ": " + v; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + int i = line.indexOf(':'); + if (i < 0) { + return line; + } + int j; + if (name.equalsIgnoreCase("Content-Description")) { + // Content-Description should retain the folded whitespace after header unfolding - + // rf. RFC2822 section 2.2.3, rf. RFC2822 section 3.2.3 + for (j = i + 1; j < line.length(); j++) { + char c = line.charAt(j); + if (!(c == '\t' || c == '\r' || c == '\n')) { + break; + } + } + } else { + // skip whitespace after ':' + for (j = i + 1; j < line.length(); j++) { + char c = line.charAt(j); + if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) { + break; + } + } + } + return line.substring(j); + } + } + + public class MimeEventIterator implements Iterator { + + MimeEventIterator() { + } + + @Override + public boolean hasNext() { + return !parsed; + } + + @SuppressWarnings("fallthrough") + @Override + public MimeEvent next() { + try { + if (parsed) { + throw new NoSuchElementException(); + } + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "MIMEParser state=" + state.name()); + } + switch (state) { + case START_MESSAGE: + state = State.SKIP_PREAMBLE; + return StartMessage.INSTANCE; + case SKIP_PREAMBLE: + skipPreamble(); + // fall through + case START_PART: + state = State.HEADERS; + return StartPart.INSTANCE; + case HEADERS: + InternetHeaders ih = readHeaders(); + state = State.BODY; + bol = true; + return new Headers(ih); + case BODY: + ByteBuffer buf = readBody(); + bol = false; + return new Content(buf); + case END_PART: + if (done) { + state = State.END_MESSAGE; + } else { + state = State.START_PART; + } + return EndPart.INSTANCE; + case END_MESSAGE: + parsed = true; + return EndMessage.INSTANCE; + } + // unreachable + return null; + } catch (Exception e) { + throw new NoSuchElementException(e.getMessage()); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + /** + * A utility class that manages RFC822 style + * headers. Given an RFC822 format message stream, it reads lines + * until the blank line that indicates end of header. The input stream + * is positioned at the start of the body. The lines are stored + * within the object and can be extracted as either Strings or + * {@link Header} objects. + * + * This class is mostly intended for service providers. MimeMessage + * and MimeBody use this class for holding their headers. + * + * A note on RFC822 and MIME headers + * + * RFC822 and MIME header fields must contain only + * US-ASCII characters. If a header contains non US-ASCII characters, + * it must be encoded as per the rules in RFC 2047. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the setHeader, addHeader, and + * addHeaderLine methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate. + */ + public class InternetHeaders { + + private final List headers = new ArrayList<>(); + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. Store the + * header lines inside this InternetHeaders object. + * Note that the header lines are added into this InternetHeaders + * object, so any existing headers in this object will not be + * affected. + */ + public InternetHeaders() throws IOException { + String line; + String prevline = null; + StringBuilder stringBuilder = new StringBuilder(); + do { + line = readLine(); + if (line != null && (line.startsWith(" ") || line.startsWith("\t"))) { + if (prevline != null) { + stringBuilder.append(prevline); + prevline = null; + } + stringBuilder.append("\r\n").append(line); + } else { + if (prevline != null) { + addHeaderLine(prevline); + } else if (stringBuilder.length() > 0) { + addHeaderLine(stringBuilder.toString()); + stringBuilder.setLength(0); + } + prevline = line; + } + } while (line != null && line.length() > 0); + } + + /** + * Return all the values for the specified header. The + * values are String objects. Returns null + * if no headers with the specified name exist. + * + * @param name header name + * @return array of header values, or null if none + */ + public List getHeader(String name) { + List v = new ArrayList<>(); + for (Hdr h : headers) { + if (name.equalsIgnoreCase(h.name)) { + v.add(h.getValue()); + } + } + return v.isEmpty() ? null : v; + } + + /** + * Return all the headers as an Enumeration of + * {@link Header} objects. + * + * @return Header objects + */ + public List getAllHeaders() { + return headers; + } + + /** + * Add an RFC822 header line to the header store. + * If the line starts with a space or tab (a continuation line), + * add it to the last header line in the list. + * Note that RFC822 headers can only contain US-ASCII characters + * + * @param line raw RFC822 header line + */ + private void addHeaderLine(String line) { + char c = line.charAt(0); + if (c == ' ' || c == '\t') { + Hdr h = headers.get(headers.size() - 1); + h.line += "\r\n" + line; + } else { + headers.add(new Hdr(line)); + } + } + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimePart.java b/net-mime/src/main/java/org/xbib/net/mime/MimePart.java new file mode 100644 index 0000000..9309717 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimePart.java @@ -0,0 +1,229 @@ +package org.xbib.net.mime; + +import org.xbib.net.mime.stream.MimeStream; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Represents an attachment part in a MIME message. MIME message parsing is done + * lazily using a pull parser, so the part may not have all the data. {@link #read} + * and {@link #readOnce} may trigger the actual parsing the message. In fact, + * parsing of an attachment part may be triggered by calling {@link #read} methods + * on some other attachment parts. All this happens behind the scenes so the + * application developer need not worry about these details. + */ +public class MimePart implements Closeable { + + private volatile boolean closed; + private volatile MimeParser.InternetHeaders headers; + private volatile String contentId; + private String contentType; + private String contentTransferEncoding; + + volatile boolean parsed; + final MimeMessage msg; + private final DataHead dataHead; + + private final Object lock = new Object(); + + MimePart(MimeMessage msg) { + this.msg = msg; + this.dataHead = new DataHead(this); + } + + MimePart(MimeMessage msg, String contentId) { + this(msg); + this.contentId = contentId; + } + + /** + * Can get the attachment part's content multiple times. That means + * the full content needs to be there in memory or on the file system. + * Calling this method would trigger parsing for the part's data. So + * do not call this unless it is required(otherwise, just wrap MimePart + * into a object that returns InputStream for e.g DataHandler) + * + * @return data for the part's content + */ + public InputStream read() throws IOException, MimeException { + return MimeStream.decode(dataHead.read(), contentTransferEncoding); + } + + /** + * Cleans up any resources that are held by this part (for e.g. deletes + * the temp file that is used to serve this part's content). After + * calling this, one shouldn't call {@link #read()} or {@link #readOnce()} + */ + @Override + public void close() throws IOException { + if (!closed) { + synchronized (lock) { + if (!closed) { + dataHead.close(); + closed = true; + } + } + } + } + + /** + * Can get the attachment part's content only once. The content + * will be lost after the method. Content data is not be stored + * on the file system or is not kept in the memory for the + * following case: + * - Attachement parts contents are accessed sequentially + * + * In general, take advantage of this when the data is used only + * once. + * + * @return data for the part's content + */ + public InputStream readOnce() throws IOException, MimeException { + return MimeStream.decode(dataHead.readOnce(), contentTransferEncoding); + } + + /** + * Send the content to the File + * @param f file to store the content + */ + public void moveTo(File f) throws IOException, MimeException { + dataHead.moveTo(f); + } + + /** + * Returns Content-ID MIME header for this attachment part + * + * @return Content-ID of the part + */ + public String getContentId() throws MimeException, IOException { + if (contentId == null) { + getHeaders(); + } + return contentId; + } + + /** + * Returns Content-Transfer-Encoding MIME header for this attachment part + * + * @return Content-Transfer-Encoding of the part + */ + public String getContentTransferEncoding() throws MimeException, IOException { + if (contentTransferEncoding == null) { + getHeaders(); + } + return contentTransferEncoding; + } + + /** + * Returns Content-Type MIME header for this attachment part + * + * @return Content-Type of the part + */ + public String getContentType() throws MimeException, IOException { + if (contentType == null) { + getHeaders(); + } + return contentType; + } + + private void getHeaders() throws MimeException, IOException { + // Trigger parsing for the part headers + while (headers == null) { + if (!msg.makeProgress()) { + if (headers == null) { + throw new IllegalStateException("internal Error. Didn't get Headers even after complete parsing"); + } + } + } + } + + /** + * Return all the values for the specified header. + * Returns null if no headers with the + * specified name exist. + * + * @param name header name + * @return list of header values, or null if none + */ + public List getHeader(String name) throws MimeException, IOException { + getHeaders(); + return headers.getHeader(name); + } + + /** + * Return all the headers + * + * @return list of Header objects + */ + public List getAllHeaders() throws MimeException, IOException { + getHeaders(); + return headers.getAllHeaders(); + } + + /** + * Callback to set headers + * + * @param headers MIME headers for the part + */ + void setHeaders(MimeParser.InternetHeaders headers) throws MimeException, IOException { + this.headers = headers; + List ct = getHeader("Content-Type"); + this.contentType = (ct == null) ? "application/octet-stream" : ct.get(0); + List cte = getHeader("Content-Transfer-Encoding"); + this.contentTransferEncoding = (cte == null) ? "binary" : cte.get(0); + } + + /** + * Callback to notify that there is a partial content for the part + * + * @param buf content data for the part + */ + void addBody(ByteBuffer buf) throws IOException { + dataHead.addBody(buf); + } + + /** + * Callback to indicate that parsing is done for this part + * (no more update events for this part) + */ + void doneParsing() { + parsed = true; + dataHead.doneParsing(); + } + + /** + * Callback to set Content-ID for this part + * @param cid Content-ID of the part + */ + void setContentId(String cid) { + this.contentId = cid; + } + + /** + * Callback to set Content-Transfer-Encoding for this part + * @param cte Content-Transfer-Encoding of the part + */ + void setContentTransferEncoding(String cte) { + this.contentTransferEncoding = cte; + } + + /** + * Return {@code true} if this part has already been closed, {@code false} otherwise. + * + * @return {@code true} if this part has already been closed, {@code false} otherwise. + */ + public boolean isClosed() { + return closed; + } + + @Override + public String toString() { + return "Part="+contentId+":"+contentTransferEncoding; + } + +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeTypeEntry.java b/net-mime/src/main/java/org/xbib/net/mime/MimeTypeEntry.java new file mode 100644 index 0000000..20b5cfb --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeTypeEntry.java @@ -0,0 +1,21 @@ +package org.xbib.net.mime; + +public class MimeTypeEntry { + + private final String type; + + private final String extension; + + public MimeTypeEntry(String type, String extension) { + this.type = type; + this.extension = extension; + } + + public String getType() { + return type; + } + + public String getExtension() { + return extension; + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeTypeFile.java b/net-mime/src/main/java/org/xbib/net/mime/MimeTypeFile.java new file mode 100644 index 0000000..6bc1b3e --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeTypeFile.java @@ -0,0 +1,105 @@ +package org.xbib.net.mime; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + +public class MimeTypeFile { + + private final Map extensions; + + private String prev; + + public MimeTypeFile(URL url) throws IOException { + this.extensions = new HashMap<>(); + parse(url); + } + + public Map getExtensions() { + return extensions; + } + + public MimeTypeEntry getMimeTypeEntry(String extension) { + return extensions.get(extension); + } + + public String getMimeTypeString(String extension) { + MimeTypeEntry entry = getMimeTypeEntry(extension); + return entry != null ? entry.getType() : null; + } + + private void parse(URL url) throws IOException { + try (InputStream inputStream = url.openStream()) { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + bufferedReader.lines().forEach(line -> { + if (prev == null) { + prev = line; + } else { + prev = prev + line; + } + int end = prev.length(); + if (prev.length() > 0 && prev.charAt(end - 1) == 92) { + prev = prev.substring(0, end - 1); + } else{ + parseEntry(prev); + prev = null; + } + }); + if (prev != null) { + parseEntry(prev); + } + } + } + + private void parseEntry(String line) { + String mimetype = null; + String ext; + String entry; + line = line.trim(); + StringTokenizer strtok = new StringTokenizer(line); + if (line.isEmpty()) { + return; + } + if (line.startsWith("#")) { + return; + } + if (line.indexOf(61) > 0) { + while (strtok.hasMoreTokens()) { + String num_tok = strtok.nextToken(); + entry = null; + if (strtok.hasMoreTokens() && "=".equals(strtok.nextToken()) && strtok.hasMoreTokens()) { + entry = strtok.nextToken(); + } + if (entry == null) { + return; + } + if ("type".equals(num_tok)) { + mimetype = entry; + } else if ("exts".equals(num_tok)) { + StringTokenizer st = new StringTokenizer(entry, ","); + while (st.hasMoreTokens()) { + ext = st.nextToken(); + MimeTypeEntry entry1 = new MimeTypeEntry(mimetype, ext); + extensions.put(ext, entry1); + } + } + } + } else { + if (strtok.countTokens() == 0) { + return; + } + mimetype = strtok.nextToken(); + while (strtok.hasMoreTokens()) { + ext = strtok.nextToken(); + MimeTypeEntry entry2 = new MimeTypeEntry(mimetype, ext); + extensions.put(ext, entry2); + } + } + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeTypeService.java b/net-mime/src/main/java/org/xbib/net/mime/MimeTypeService.java new file mode 100644 index 0000000..7fafa59 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeTypeService.java @@ -0,0 +1,71 @@ +package org.xbib.net.mime; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MimeTypeService { + + public static final String DEFAULT_TYPE = "application/octet-stream"; + + private final Map extensions; + + public MimeTypeService() { + this.extensions = new HashMap<>(); + try { + List mimeTypeFiles = new ArrayList<>(); + String s = "META-INF/mime.types"; + boolean found = false; + ClassLoader classLoader = getClass().getClassLoader(); + Enumeration e = classLoader.getResources(s); + while (e != null && e.hasMoreElements()) { + URL url = e.nextElement(); + if (url != null) { + mimeTypeFiles.add(new MimeTypeFile(url)); + found = true; + } + } + if (!found) { + MimeTypeFile mtf = loadResource(classLoader, "/" + s); + if (mtf != null) { + mimeTypeFiles.add(mtf); + } + } + MimeTypeFile defaultMimeTypeFile = loadResource(classLoader, "/META-INF/mimetypes.default"); + if (defaultMimeTypeFile != null) { + mimeTypeFiles.add(defaultMimeTypeFile); + } + for (MimeTypeFile mimeTypeFile : mimeTypeFiles) { + extensions.putAll(mimeTypeFile.getExtensions()); + } + } catch (IOException e) { + // ignore + } + } + + public String getContentType(String nameWithSuffix) { + if (nameWithSuffix == null) { + return null; + } + int pos = nameWithSuffix.lastIndexOf('.'); + if (pos < 0) { + return DEFAULT_TYPE; + } else { + String ext = nameWithSuffix.substring(pos + 1); + if (ext.length() == 0) { + return DEFAULT_TYPE; + } else { + return extensions.containsKey(ext) ? extensions.get(ext).getType() : DEFAULT_TYPE; + } + } + } + + private static MimeTypeFile loadResource(ClassLoader classLoader, String name) throws IOException { + URL url = classLoader.getResource(name); + return url != null ? new MimeTypeFile(url) : null; + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/MimeTypeUtil.java b/net-mime/src/main/java/org/xbib/net/mime/MimeTypeUtil.java new file mode 100644 index 0000000..f89d70a --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/MimeTypeUtil.java @@ -0,0 +1,92 @@ +package org.xbib.net.mime; + +import java.net.URLConnection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public class MimeTypeUtil { + + /** + * A map from extension to MIME types, which is queried before + * {@link URLConnection#guessContentTypeFromName(String)}, so that + * important extensions are always mapped to the right MIME types. + */ + private static final Map EXTENSION_TO_MEDIA_TYPE; + + static { + Map map = new HashMap<>(); + // Text files + add(map, "text/css", "css"); + add(map, "text/html", "html", "htm"); + add(map, "text/plain", "txt"); + + // Image files + add(map, "image/gif", "gif"); + add(map, "image/jpeg", "jpeg", "jpg"); + add(map, "image/png", "png"); + add(map, "image/svg+xml", "svg", "svgz"); + add(map, "image/x-icon", "ico"); + + // Font files + add(map, "application/x-font-ttf", "ttc", "ttf"); + add(map, "application/font-woff", "woff"); + add(map, "application/font-woff2", "woff2"); + add(map, "application/vnd.ms-fontobject", "eot"); + add(map, "font/opentype", "otf"); + + // JavaScript, XML, etc + add(map, "application/javascript", "js", "map"); + add(map, "application/json", "json"); + add(map, "application/pdf", "pdf"); + add(map, "application/xhtml+xml", "xhtml", "xhtm"); + add(map, "application/xml", "xml", "xsd"); + add(map, "application/xml-dtd", "dtd"); + + EXTENSION_TO_MEDIA_TYPE = Collections.unmodifiableMap(map); + } + + private MimeTypeUtil() { + } + + private static void add(Map extensionToMediaType, + String mediaType, + String... extensions) { + for (String s : extensions) { + extensionToMediaType.put(s, mediaType); + } + } + + public static String guessFromPath(String path, boolean preCompressed) { + requireNonNull(path, "path"); + String extension = extractExtension(path, preCompressed); + if (extension == null) { + return null; + } + String mediaType = EXTENSION_TO_MEDIA_TYPE.get(extension); + if (mediaType != null) { + return mediaType; + } + String guessedContentType = URLConnection.guessContentTypeFromName(path); + return guessedContentType != null ? guessedContentType : "application/octet-stream"; + } + + private static String extractExtension(String path, boolean preCompressed) { + String s = path; + // If the path is for a precompressed file, it will have an additional extension indicating the + // encoding, which we don't want to use when determining content type. + if (preCompressed) { + s = s.substring(0, s.lastIndexOf('.')); + } + int dotIdx = s.lastIndexOf('.'); + int slashIdx = s.lastIndexOf('/'); + if (dotIdx < 0 || slashIdx > dotIdx) { + // No extension + return null; + } + return s.substring(dotIdx + 1).toLowerCase(Locale.ROOT); + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/StartMessage.java b/net-mime/src/main/java/org/xbib/net/mime/StartMessage.java new file mode 100644 index 0000000..6682a8a --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/StartMessage.java @@ -0,0 +1,14 @@ +package org.xbib.net.mime; + +public final class StartMessage implements MimeEvent { + + static final StartMessage INSTANCE = new StartMessage(); + + public StartMessage() { + } + + @Override + public Type getEventType() { + return Type.START_MESSAGE; + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/StartPart.java b/net-mime/src/main/java/org/xbib/net/mime/StartPart.java new file mode 100644 index 0000000..1de2916 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/StartPart.java @@ -0,0 +1,14 @@ +package org.xbib.net.mime; + +public final class StartPart implements MimeEvent { + + static final StartPart INSTANCE = new StartPart(); + + public StartPart() { + } + + @Override + public Type getEventType() { + return Type.START_PART; + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/WeakDataFile.java b/net-mime/src/main/java/org/xbib/net/mime/WeakDataFile.java new file mode 100644 index 0000000..58cd3ec --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/WeakDataFile.java @@ -0,0 +1,103 @@ +package org.xbib.net.mime; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Removing files based on this + * article + */ +final class WeakDataFile extends WeakReference { + + private static final Logger LOGGER = Logger.getLogger(WeakDataFile.class.getName()); + private static int TIMEOUT = 10; //milliseconds + private static ReferenceQueue refQueue = new ReferenceQueue(); + private static Queue refList = new ConcurrentLinkedQueue<>(); + private File file; + private final RandomAccessFile raf; + private static boolean hasCleanUpExecutor = false; + static { + int delay = 10; + try { + delay = Integer.getInteger("org.xbib.net.mime.delay", 10); + } catch (SecurityException se) { + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, "Cannot read ''{0}'' property, using defaults.", + new Object[] {"org.xbib.net.mime.delay"}); + } + } + } + + WeakDataFile(DataFile df, File file) throws IOException { + super(df, refQueue); + refList.add(this); + this.file = file; + raf = new RandomAccessFile(file, "rw"); + if (!hasCleanUpExecutor) { + drainRefQueueBounded(); + } + } + + synchronized void read(long pointer, byte[] buf, int offset, int length ) throws IOException { + raf.seek(pointer); + raf.readFully(buf, offset, length); + } + + synchronized long writeTo(long pointer, byte[] data, int offset, int length) throws IOException { + raf.seek(pointer); + raf.write(data, offset, length); + return raf.getFilePointer(); // Update pointer for next write + } + + void close() throws IOException { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Deleting file = {0}", file.getName()); + } + refList.remove(this); + raf.close(); + boolean deleted = file.delete(); + if (!deleted) { + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.log(Level.INFO, "File {0} was not deleted", file.getAbsolutePath()); + } + } + } + + void renameTo(File f) throws IOException { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Moving file={0} to={1}", new Object[]{file, f}); + } + refList.remove(this); + raf.close(); + Path target = Files.move(file.toPath(), f.toPath(), StandardCopyOption.REPLACE_EXISTING); + boolean renamed = f.toPath().equals(target); + if (!renamed) { + if (LOGGER.isLoggable(Level.INFO)) { + throw new IOException("File " + file.getAbsolutePath() + + " was not moved to " + f.getAbsolutePath()); + } + } + file = target.toFile(); + } + + static void drainRefQueueBounded() throws IOException { + WeakDataFile weak; + while (( weak = (WeakDataFile) refQueue.poll()) != null ) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Cleaning file = {0} from reference queue.", weak.file); + } + weak.close(); + } + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/stream/BASE64DecoderStream.java b/net-mime/src/main/java/org/xbib/net/mime/stream/BASE64DecoderStream.java new file mode 100644 index 0000000..3f6eeb5 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/stream/BASE64DecoderStream.java @@ -0,0 +1,438 @@ +package org.xbib.net.mime.stream; + +import org.xbib.net.mime.MimeException; + +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * This class implements a BASE64 Decoder. It is implemented as + * a FilterInputStream, so one can just wrap this class around + * any input stream and read bytes from this filter. The decoding + * is done as the bytes are read out. + */ +final class BASE64DecoderStream extends FilterInputStream { + /** + * This character array provides the character to value map + * based on RFC1521. + */ + private final static char[] pem_array = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2 + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3 + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4 + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5 + 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6 + '4', '5', '6', '7', '8', '9', '+', '/' // 7 + }; + private final static byte[] pem_convert_array = new byte[256]; + + static { + for (int i = 0; i < 255; i++) { + pem_convert_array[i] = -1; + } + for (int i = 0; i < pem_array.length; i++) { + pem_convert_array[pem_array[i]] = (byte) i; + } + } + + // buffer of decoded bytes for single byte reads + private final byte[] buffer = new byte[3]; + // buffer for almost 8K of typical 76 chars + CRLF lines, + // used by getByte method. this buffer contains encoded bytes. + private final byte[] input_buffer = new byte[78 * 105]; + private int bufsize = 0; // size of the cache + private int index = 0; // index into the cache + private int input_pos = 0; + private int input_len = 0; + + /** + * Create a BASE64 decoder that decodes the specified input stream. + * The System property mail.mime.base64.ignoreerrors + * controls whether errors in the encoded data cause an exception + * or are ignored. The default is false (errors cause exception). + * + * @param in the input stream + */ + public BASE64DecoderStream(InputStream in) { + super(in); + } + + /** + * Base64 decode a byte array. No line breaks are allowed. + * This method is suitable for short strings, such as those + * in the IMAP AUTHENTICATE protocol, but not to decode the + * entire content of a MIME part. + *

+ * NOTE: inbuf may only contain valid base64 characters. + * Whitespace is not ignored. + */ + public static byte[] decode(byte[] inbuf) { + int size = (inbuf.length / 4) * 3; + if (size == 0) { + return inbuf; + } + + if (inbuf[inbuf.length - 1] == '=') { + size--; + if (inbuf[inbuf.length - 2] == '=') { + size--; + } + } + byte[] outbuf = new byte[size]; + + int inpos = 0, outpos = 0; + size = inbuf.length; + while (size > 0) { + int val; + int osize = 3; + val = pem_convert_array[inbuf[inpos++] & 0xff]; + val <<= 6; + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + val <<= 6; + if (inbuf[inpos] != '=') { + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + } else { + osize--; + } + val <<= 6; + if (inbuf[inpos] != '=') { + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + } else { + osize--; + } + if (osize > 2) { + outbuf[outpos + 2] = (byte) (val & 0xff); + } + val >>= 8; + if (osize > 1) { + outbuf[outpos + 1] = (byte) (val & 0xff); + } + val >>= 8; + outbuf[outpos] = (byte) (val & 0xff); + outpos += osize; + size -= 4; + } + return outbuf; + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an int in the range 0 + * to 255. If no byte is available because the end of + * the stream has been reached, the value -1 is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return next byte of data, or -1 if the end of the + * stream is reached. + * @throws IOException if an I/O error occurs. + * @see FilterInputStream#in + */ + @Override + public int read() throws IOException { + if (index >= bufsize) { + try { + bufsize = decode(buffer, 0, buffer.length); + } catch (MimeException e) { + throw new IOException(e); + } + if (bufsize <= 0) { + return -1; + } + index = 0; // reset index into buffer + } + return buffer[index++] & 0xff; // Zero off the MSB + } + + /** + * Reads up to len decoded bytes of data from this input stream + * into an array of bytes. This method blocks until some input is + * available. + *

+ * + * @param buf the buffer into which the data is read. + * @param off the start offset of the data. + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @throws IOException if an I/O error occurs. + */ + @Override + public int read(byte[] buf, int off, int len) throws IOException { + // empty out single byte read buffer + int off0 = off; + while (index < bufsize && len > 0) { + buf[off++] = buffer[index++]; + len--; + } + if (index >= bufsize) { + bufsize = index = 0; + } + + int bsize = (len / 3) * 3; // round down to multiple of 3 bytes + if (bsize > 0) { + int size = 0; + try { + size = decode(buf, off, bsize); + } catch (MimeException e) { + throw new IOException(e); + } + off += size; + len -= size; + + if (size != bsize) { // hit EOF? + if (off == off0) { + return -1; + } else { + return off - off0; + } + } + } + + // finish up with a partial read if necessary + for (; len > 0; len--) { + int c = read(); + if (c == -1) { + break; + } + buf[off++] = (byte) c; + } + + if (off == off0) { + return -1; + } else { + return off - off0; + } + } + + /** + * Skips over and discards n bytes of data from this stream. + */ + @Override + public long skip(long n) throws IOException { + long skipped = 0; + while (n-- > 0 && read() >= 0) { + skipped++; + } + return skipped; + } + + /** + * Tests if this input stream supports marks. Currently this class + * does not support marks + */ + @Override + public boolean markSupported() { + return false; // Maybe later .. + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. However, this figure is only + * a close approximation in case the original encoded stream + * contains embedded CRLFs; since the CRLFs are discarded, not decoded + */ + @Override + public int available() throws IOException { + // This is only an estimate, since in.available() + // might include CRLFs too .. + return ((in.available() * 3) / 4 + (bufsize - index)); + } + + /** + * The decoder algorithm. Most of the complexity here is dealing + * with error cases. Returns the number of bytes decoded, which + * may be zero. Decoding is done by filling an int with 4 6-bit + * values by shifting them in from the bottom and then extracting + * 3 8-bit bytes from the int by shifting them out from the bottom. + * + * @param outbuf the buffer into which to put the decoded bytes + * @param pos position in the buffer to start filling + * @param len the number of bytes to fill + * @return the number of bytes filled, always a multiple + * of three, and may be zero + * @exception IOException if the data is incorrectly formatted + */ + private int decode(byte[] outbuf, int pos, int len) throws IOException, MimeException { + int pos0 = pos; + while (len >= 3) { + /* + * We need 4 valid base64 characters before we start decoding. + * We skip anything that's not a valid base64 character (usually + * just CRLF). + */ + int got = 0; + int val = 0; + while (got < 4) { + int i = getByte(); + if (i == -1 || i == -2) { + boolean atEOF; + if (i == -1) { + if (got == 0) { + return pos - pos0; + } + throw new MimeException( + "BASE64Decoder: Error in encoded stream: " + + "needed 4 valid base64 characters " + + "but only got " + got + " before EOF" + + recentChars()); + } else { // i == -2 + // found a padding character, we're at EOF + // XXX - should do something to make EOF "sticky" + if (got < 2) { + throw new MimeException( + "BASE64Decoder: Error in encoded stream: " + + "needed at least 2 valid base64 characters," + + " but only got " + got + + " before padding character (=)" + + recentChars()); + } + + // didn't get any characters before padding character? + if (got == 0) { + return pos - pos0; + } + atEOF = false; // need to keep reading + } + + // pad partial result with zeroes + + // how many bytes will we produce on output? + // (got always < 4, so size always < 3) + int size = got - 1; + if (size == 0) { + size = 1; + } + + // handle the one padding character we've seen + got++; + val <<= 6; + + while (got < 4) { + if (!atEOF) { + // consume the rest of the padding characters, + // filling with zeroes + i = getByte(); + if (i == -1) { + throw new MimeException( + "BASE64Decoder: Error in encoded " + + "stream: hit EOF while looking for " + + "padding characters (=)" + + recentChars()); + } else if (i != -2) { + throw new MimeException( + "BASE64Decoder: Error in encoded " + + "stream: found valid base64 " + + "character after a padding character " + + "(=)" + recentChars()); + } + } + val <<= 6; + got++; + } + + // now pull out however many valid bytes we got + val >>= 8; // always skip first one + if (size == 2) { + outbuf[pos + 1] = (byte) (val & 0xff); + } + val >>= 8; + outbuf[pos] = (byte) (val & 0xff); + // len -= size; // not needed, return below + pos += size; + return pos - pos0; + } else { + // got a valid byte + val <<= 6; + got++; + val |= i; + } + } + + // read 4 valid characters, now extract 3 bytes + outbuf[pos + 2] = (byte) (val & 0xff); + val >>= 8; + outbuf[pos + 1] = (byte) (val & 0xff); + val >>= 8; + outbuf[pos] = (byte) (val & 0xff); + len -= 3; + pos += 3; + } + return pos - pos0; + } + + /** + * Read the next valid byte from the input stream. + * Buffer lots of data from underlying stream in input_buffer, + * for efficiency. + * + * @return the next byte, -1 on EOF, or -2 if next byte is '=' + * (padding at end of encoded data) + */ + private int getByte() throws IOException { + int c; + do { + if (input_pos >= input_len) { + try { + input_len = in.read(input_buffer); + } catch (EOFException ex) { + return -1; + } + if (input_len <= 0) { + return -1; + } + input_pos = 0; + } + // get the next byte in the buffer + c = input_buffer[input_pos++] & 0xff; + // is it a padding byte? + if (c == '=') { + return -2; + } + // no, convert it + c = pem_convert_array[c]; + // loop until we get a legitimate byte + } while (c == -1); + return c; + } + + /** + * Return the most recent characters, for use in an error message. + */ + private String recentChars() { + // reach into the input buffer and extract up to 10 + // recent characters, to help in debugging. + StringBuilder errstr = new StringBuilder(); + int nc = Math.min(input_pos, 10); + if (nc > 0) { + errstr.append(", the ").append(nc).append(" most recent characters were: \""); + for (int k = input_pos - nc; k < input_pos; k++) { + char c = (char) (input_buffer[k] & 0xff); + switch (c) { + case '\r': + errstr.append("\\r"); + break; + case '\n': + errstr.append("\\n"); + break; + case '\t': + errstr.append("\\t"); + break; + default: + if (c >= ' ' && c < 0177) { + errstr.append(c); + } else { + errstr.append("\\").append((int) c); + } + } + } + errstr.append("\""); + } + return errstr.toString(); + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/stream/Base64.java b/net-mime/src/main/java/org/xbib/net/mime/stream/Base64.java new file mode 100644 index 0000000..239325a --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/stream/Base64.java @@ -0,0 +1,634 @@ +package org.xbib.net.mime.stream; + +import java.nio.charset.StandardCharsets; + +/** + * Utilities for encoding and decoding the Base64 representation of + * binary data. See RFCs 2045 and 3548. + */ +public class Base64 { + /** + * Default values for encoder/decoder flags. + */ + public static final int DEFAULT = 0; + /** + * Encoder flag bit to omit the padding '=' characters at the end + * of the output (if any). + */ + public static final int NO_PADDING = 1; + /** + * Encoder flag bit to omit all line terminators (i.e., the output + * will be on one long line). + */ + public static final int NO_WRAP = 2; + /** + * Encoder flag bit to indicate lines should be terminated with a + * CRLF pair instead of just an LF. Has no effect if {@code + * NO_WRAP} is specified as well. + */ + public static final int CRLF = 4; + /** + * Encoder/decoder flag bit to indicate using the "URL and + * filename safe" variant of Base64 (see RFC 3548 section 4) where + * {@code -} and {@code _} are used in place of {@code +} and + * {@code /}. + */ + public static final int URL_SAFE = 8; + /** + * Flag to pass to indicate that it + * should not close the output stream it is wrapping when it + * itself is closed. + */ + public static final int NO_CLOSE = 16; + + private Base64() { + } // don't instantiate + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + *

The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the input array to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int flags) { + return decode(input, 0, input.length, flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + *

The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the data to decode + * @param offset the position within the input array at which to start + * @param len the number of bytes of input to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int offset, int len, int flags) { + // Allocate space for the most data the input could represent. + // (It could contain less if it contains whitespace, etc.) + Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]); + if (!decoder.process(input, offset, len, true)) { + throw new IllegalArgumentException("bad base-64"); + } + // Maybe we got lucky and allocated exactly enough output space. + if (decoder.op == decoder.output.length) { + return decoder.output; + } + // Need to shorten the array, so allocate a new one of the + // right size and copy. + byte[] temp = new byte[decoder.op]; + System.arraycopy(decoder.output, 0, temp, 0, decoder.op); + return temp; + } + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int flags) { + return new String(encode(input, flags), StandardCharsets.US_ASCII); + } + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int offset, int len, int flags) { + return new String(encode(input, offset, len, flags), StandardCharsets.US_ASCII); + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int flags) { + return encode(input, 0, input.length, flags); + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int offset, int len, int flags) { + Encoder encoder = new Encoder(flags, null); + // Compute the exact length of the array we will produce. + int output_len = len / 3 * 4; + // Account for the tail of the data and the padding bytes, if any. + if (encoder.do_padding) { + if (len % 3 > 0) { + output_len += 4; + } + } else { + switch (len % 3) { + case 0: + break; + case 1: + output_len += 2; + break; + case 2: + output_len += 3; + break; + } + } + // Account for the newlines, if any. + if (encoder.do_newline && len > 0) { + output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) * + (encoder.do_cr ? 2 : 1); + } + encoder.output = new byte[output_len]; + encoder.process(input, offset, len, true); + assert encoder.op == output_len; + return encoder.output; + } + + static abstract class Coder { + public byte[] output; + public int op; + + /** + * Encode/decode another block of input data. this.output is + * provided by the caller, and must be big enough to hold all + * the coded data. On exit, this.opwill be set to the length + * of the coded data. + * + * @param finish true if this is the final call to process for + * this object. Will finalize the coder state and + * include any final bytes in the output. + * @return true if the input so far is good; false if some + * error has been detected in the input stream.. + */ + public abstract boolean process(byte[] input, int offset, int len, boolean finish); + + /** + * @return the maximum number of bytes a call to process() + * could produce for the given number of input bytes. This may + * be an overestimate. + */ + public abstract int maxOutputSize(int len); + } + + /* package */ static class Decoder extends Coder { + /** + * Lookup table for turning bytes into their position in the + * Base64 alphabet. + */ + private static final int[] DECODE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + /** + * Decode lookup table for the "web safe" variant (RFC 3548 + * sec. 4) where - and _ replace + and /. + */ + private static final int[] DECODE_WEBSAFE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + /** + * Non-data values in the DECODE arrays. + */ + private static final int SKIP = -1; + private static final int EQUALS = -2; + final private int[] alphabet; + /** + * States 0-3 are reading through the next input tuple. + * State 4 is having read one '=' and expecting exactly + * one more. + * State 5 is expecting no more data or padding characters + * in the input. + * State 6 is the error state; an error has been detected + * in the input and no future input can "fix" it. + */ + private int state; // state number (0 to 6) + private int value; + + public Decoder(int flags, byte[] output) { + this.output = output; + alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE; + state = 0; + value = 0; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could decode to. + */ + public int maxOutputSize(int len) { + return len * 3 / 4 + 10; + } + + /** + * Decode another block of input data. + * + * @return true if the state machine is still healthy. false if + * bad base-64 data has been detected in the input stream. + */ + public boolean process(byte[] input, int offset, int len, boolean finish) { + if (this.state == 6) { + return false; + } + int p = offset; + len += offset; + // Using local variables makes the decoder about 12% + // faster than if we manipulate the member variables in + // the loop. (Even alphabet makes a measurable + // difference, which is somewhat surprising to me since + // the member variable is final.) + int state = this.state; + int value = this.value; + int op = 0; + final byte[] output = this.output; + final int[] alphabet = this.alphabet; + while (p < len) { + // Try the fast path: we're starting a new tuple and the + // next four bytes of the input stream are all data + // bytes. This corresponds to going through states + // 0-1-2-3-0. We expect to use this method for most of + // the data. + // + // If any of the next four bytes of input are non-data + // (whitespace, etc.), value will end up negative. (All + // the non-data values in decode are small negative + // numbers, so shifting any of them up and or'ing them + // together will result in a value with its top bit set.) + // + // You can remove this whole block and the output should + // be the same, just slower. + if (state == 0) { + while (p + 4 <= len && + (value = ((alphabet[input[p] & 0xff] << 18) | + (alphabet[input[p + 1] & 0xff] << 12) | + (alphabet[input[p + 2] & 0xff] << 6) | + (alphabet[input[p + 3] & 0xff]))) >= 0) { + output[op + 2] = (byte) value; + output[op + 1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + p += 4; + } + if (p >= len) break; + } + // The fast path isn't available -- either we've read a + // partial tuple, or the next four input bytes aren't all + // data, or whatever. Fall back to the slower state + // machine implementation. + int d = alphabet[input[p++] & 0xff]; + switch (state) { + case 0: + if (d >= 0) { + value = d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + case 1: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + case 2: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect exactly one more padding character. + output[op++] = (byte) (value >> 4); + state = 4; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + case 3: + if (d >= 0) { + // Emit the output triple and return to state 0. + value = (value << 6) | d; + output[op + 2] = (byte) value; + output[op + 1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + state = 0; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect no further data or padding characters. + output[op + 1] = (byte) (value >> 2); + output[op] = (byte) (value >> 10); + op += 2; + state = 5; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + case 4: + if (d == EQUALS) { + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + case 5: + if (d != SKIP) { + this.state = 6; + return false; + } + break; + } + } + if (!finish) { + // We're out of input, but a future call could provide + // more. + this.state = state; + this.value = value; + this.op = op; + return true; + } + // Done reading input. Now figure out where we are left in + // the state machine and finish up. + switch (state) { + case 0: + // Output length is a multiple of three. Fine. + break; + case 1: + case 4: + // Read one padding '=' when we expected 2. Illegal. + // Read one extra input byte, which isn't enough to + // make another output byte. Illegal. + this.state = 6; + return false; + case 2: + // Read two extra input bytes, enough to emit 1 more + // output byte. Fine. + output[op++] = (byte) (value >> 4); + break; + case 3: + // Read three extra input bytes, enough to emit 2 more + // output bytes. Fine. + output[op++] = (byte) (value >> 10); + output[op++] = (byte) (value >> 2); + break; + case 5: + // Read all the padding '='s we expected and no more. + // Fine. + break; + } + this.state = state; + this.op = op; + return true; + } + } + + static class Encoder extends Coder { + /** + * Emit a new line every this many output tuples. Corresponds to + * a 76-character line length (the maximum allowable according to + * RFC 2045). + */ + public static final int LINE_GROUPS = 19; + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte[] ENCODE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', + }; + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte[] ENCODE_WEBSAFE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', + }; + final public boolean do_padding; + final public boolean do_newline; + final public boolean do_cr; + final private byte[] tail; + final private byte[] alphabet; + int tailLen; + private int count; + + public Encoder(int flags, byte[] output) { + this.output = output; + do_padding = (flags & NO_PADDING) == 0; + do_newline = (flags & NO_WRAP) == 0; + do_cr = (flags & CRLF) != 0; + alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; + tail = new byte[2]; + tailLen = 0; + count = do_newline ? LINE_GROUPS : -1; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could encode to. + */ + public int maxOutputSize(int len) { + return len * 8 / 5 + 10; + } + + public boolean process(byte[] input, int offset, int len, boolean finish) { + // Using local variables makes the encoder about 9% faster. + final byte[] alphabet = this.alphabet; + final byte[] output = this.output; + int op = 0; + int count = this.count; + int p = offset; + len += offset; + int v = -1; + // First we need to concatenate the tail of the previous call + // with any input bytes available now and see if we can empty + // the tail. + switch (tailLen) { + case 0: + // There was no tail. + break; + case 1: + if (p + 2 <= len) { + // A 1-byte tail with at least 2 bytes of + // input available now. + v = ((tail[0] & 0xff) << 16) | + ((input[p++] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + } + break; + case 2: + if (p + 1 <= len) { + // A 2-byte tail with at least 1 byte of input. + v = ((tail[0] & 0xff) << 16) | + ((tail[1] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + } + break; + } + if (v != -1) { + output[op++] = alphabet[(v >> 18) & 0x3f]; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + // At this point either there is no tail, or there are fewer + // than 3 bytes of input available. + // The main loop, turning 3 input bytes into 4 output bytes on + // each iteration. + while (p + 3 <= len) { + v = ((input[p] & 0xff) << 16) | + ((input[p + 1] & 0xff) << 8) | + (input[p + 2] & 0xff); + output[op] = alphabet[(v >> 18) & 0x3f]; + output[op + 1] = alphabet[(v >> 12) & 0x3f]; + output[op + 2] = alphabet[(v >> 6) & 0x3f]; + output[op + 3] = alphabet[v & 0x3f]; + p += 3; + op += 4; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + if (finish) { + // Finish up the tail of the input. Note that we need to + // consume any bytes in tail before any bytes + // remaining in input; there should be at most two bytes + // total. + if (p - tailLen == len - 1) { + int t = 0; + v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4; + tailLen -= t; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (p - tailLen == len - 2) { + int t = 0; + v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) | + (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2); + tailLen -= t; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (do_newline && op > 0 && count != LINE_GROUPS) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + assert tailLen == 0; + assert p == len; + } else { + // Save the leftovers in tail to be consumed on the next + // call to encodeInternal. + if (p == len - 1) { + tail[tailLen++] = input[p]; + } else if (p == len - 2) { + tail[tailLen++] = input[p]; + tail[tailLen++] = input[p + 1]; + } + } + this.op = op; + this.count = count; + return true; + } + } +} \ No newline at end of file diff --git a/net-mime/src/main/java/org/xbib/net/mime/stream/Base64InputStream.java b/net-mime/src/main/java/org/xbib/net/mime/stream/Base64InputStream.java new file mode 100644 index 0000000..d962ef9 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/stream/Base64InputStream.java @@ -0,0 +1,153 @@ +package org.xbib.net.mime.stream; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * An InputStream that does Base64 decoding on the data read through it. + */ +public class Base64InputStream extends FilterInputStream { + + private static final byte[] EMPTY = new byte[0]; + + private static final int BUFFER_SIZE = 2048; + + private final Base64.Coder coder; + + private boolean eof; + + private byte[] inputBuffer; + + private int outputStart; + + private int outputEnd; + + public Base64InputStream(InputStream inputStream) { + this(inputStream, Base64.DEFAULT); + } + + /** + * An InputStream that performs Base64 decoding on the data read + * from the wrapped stream. + * + * @param inputStream the InputStream to read the source data from + * @param flags bit flags for controlling the decoder; see the + * constants in {@link Base64} + */ + public Base64InputStream(InputStream inputStream, int flags) { + this(inputStream, flags, false); + } + + /** + * Performs Base64 encoding or decoding on the data read from the + * wrapped InputStream. + * + * @param inputStream the InputStream to read the source data from + * @param flags bit flags for controlling the decoder; see the + * constants in {@link Base64} + * @param encode true to encode, false to decode + */ + public Base64InputStream(InputStream inputStream, int flags, boolean encode) { + super(inputStream); + eof = false; + inputBuffer = new byte[BUFFER_SIZE]; + if (encode) { + coder = new Base64.Encoder(flags, null); + } else { + coder = new Base64.Decoder(flags, null); + } + coder.output = new byte[coder.maxOutputSize(BUFFER_SIZE)]; + outputStart = 0; + outputEnd = 0; + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public void mark(int readlimit) { + throw new UnsupportedOperationException(); + } + + @Override + public void reset() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() throws IOException { + in.close(); + inputBuffer = null; + } + + @Override + public int available() { + return outputEnd - outputStart; + } + + @Override + public long skip(long n) throws IOException { + if (outputStart >= outputEnd) { + refill(); + } + if (outputStart >= outputEnd) { + return 0; + } + long bytes = Math.min(n, outputEnd-outputStart); + outputStart += bytes; + return bytes; + } + + @Override + public int read() throws IOException { + if (outputStart >= outputEnd) { + refill(); + } + if (outputStart >= outputEnd) { + return -1; + } else { + return coder.output[outputStart++] & 0xff; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (outputStart >= outputEnd) { + refill(); + } + if (outputStart >= outputEnd) { + return -1; + } + int bytes = Math.min(len, outputEnd-outputStart); + System.arraycopy(coder.output, outputStart, b, off, bytes); + outputStart += bytes; + return bytes; + } + + /** + * Read data from the input stream into inputBuffer, then + * decode/encode it into the empty coder.output, and reset the + * outputStart and outputEnd pointers. + */ + private void refill() throws IOException { + if (eof) { + return; + } + int bytesRead = in.read(inputBuffer); + boolean success; + if (bytesRead == -1) { + eof = true; + success = coder.process(EMPTY, 0, 0, true); + } else { + success = coder.process(inputBuffer, 0, bytesRead, false); + } + if (!success) { + throw new IOException("bad base64 data"); + } + outputEnd = coder.op; + outputStart = 0; + } +} \ No newline at end of file diff --git a/net-mime/src/main/java/org/xbib/net/mime/stream/Base64OutputStream.java b/net-mime/src/main/java/org/xbib/net/mime/stream/Base64OutputStream.java new file mode 100644 index 0000000..0e19b51 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/stream/Base64OutputStream.java @@ -0,0 +1,143 @@ +package org.xbib.net.mime.stream; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * An OutputStream that does Base64 encoding on the data written to + * it, writing the resulting data to another OutputStream. + */ +public class Base64OutputStream extends FilterOutputStream { + + private static final byte[] EMPTY = new byte[0]; + + private final Base64.Coder coder; + + private final int flags; + + private byte[] buffer = null; + + private int bpos = 0; + + public Base64OutputStream(OutputStream outputStream) { + this(outputStream, Base64.DEFAULT); + } + + /** + * Performs Base64 encoding on the data written to the stream, + * writing the encoded data to another OutputStream. + * + * @param outputStream the OutputStream to write the encoded data to + * @param flags bit flags for controlling the encoder; see the + * constants in {@link Base64} + */ + public Base64OutputStream(OutputStream outputStream, int flags) { + this(outputStream, flags, true); + } + /** + * Performs Base64 encoding or decoding on the data written to the + * stream, writing the encoded/decoded data to another + * OutputStream. + * + * @param outputStream the OutputStream to write the encoded data to + * @param flags bit flags for controlling the encoder; see the + * constants in {@link Base64} + * @param encode true to encode, false to decode + */ + public Base64OutputStream(OutputStream outputStream, int flags, boolean encode) { + super(outputStream); + this.flags = flags; + if (encode) { + coder = new Base64.Encoder(flags, null); + } else { + coder = new Base64.Decoder(flags, null); + } + } + public void write(int b) throws IOException { + // To avoid invoking the encoder/decoder routines for single + // bytes, we buffer up calls to write(int) in an internal + // byte array to transform them into writes of decently-sized + // arrays. + if (buffer == null) { + buffer = new byte[1024]; + } + if (bpos >= buffer.length) { + // internal buffer full; write it out. + internalWrite(buffer, 0, bpos, false); + bpos = 0; + } + buffer[bpos++] = (byte) b; + } + + /** + * Flush any buffered data from calls to write(int). Needed + * before doing a write(byte[], int, int) or a close(). + */ + private void flushBuffer() throws IOException { + if (bpos > 0) { + internalWrite(buffer, 0, bpos, false); + bpos = 0; + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (len <= 0) return; + flushBuffer(); + internalWrite(b, off, len, false); + } + + @Override + public void close() throws IOException { + IOException thrown = null; + try { + flushBuffer(); + internalWrite(EMPTY, 0, 0, true); + } catch (IOException e) { + thrown = e; + } + try { + if ((flags & Base64.NO_CLOSE) == 0) { + out.close(); + } else { + out.flush(); + } + } catch (IOException e) { + if (thrown == null) { + thrown = e; + } else { + thrown.addSuppressed(e); + } + } + if (thrown != null) { + throw thrown; + } + } + + /** + * Write the given bytes to the encoder/decoder. + * + * @param finish true if this is the last batch of input, to cause + * encoder/decoder state to be finalized. + */ + private void internalWrite(byte[] b, int off, int len, boolean finish) throws IOException { + coder.output = adjust(coder.output, coder.maxOutputSize(len)); + if (!coder.process(b, off, len, finish)) { + throw new IOException("bad base64 data"); + } + out.write(coder.output, 0, coder.op); + } + + /** + * If b.length is at least len, return b. + * Otherwise return a new byte array of length len. + */ + private byte[] adjust(byte[] b, int len) { + if (b == null || b.length < len) { + return new byte[len]; + } else { + return b; + } + } +} \ No newline at end of file diff --git a/net-mime/src/main/java/org/xbib/net/mime/stream/Hex.java b/net-mime/src/main/java/org/xbib/net/mime/stream/Hex.java new file mode 100644 index 0000000..0e16bf8 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/stream/Hex.java @@ -0,0 +1,40 @@ +package org.xbib.net.mime.stream; + +import java.util.Objects; + +public class Hex { + + private Hex() { + } + + public static String toHex(byte[] data) { + StringBuilder sb = new StringBuilder(); + for (byte b : data) { + sb.append(Character.forDigit((b & 240) >> 4, 16)).append(Character.forDigit((b & 15), 16)); + } + return sb.toString(); + } + + public static byte[] fromHex(String hex) { + Objects.requireNonNull(hex); + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) fromHex(hex.charAt(i), hex.charAt(i + 1)); + } + return data; + } + + public static int fromHex(int b1, int b2) { + int i1 = Character.digit(b1, 16); + if (i1 == -1) { + throw new IllegalArgumentException("invalid character in hexadecimal: " + b1); + } + int i2 = Character.digit(b2, 16); + if (i2 == -1) { + throw new IllegalArgumentException("invalid character in hexadecimal: " + b2); + } + return (i1 << 4) + i2; + } + +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/stream/MimeStream.java b/net-mime/src/main/java/org/xbib/net/mime/stream/MimeStream.java new file mode 100644 index 0000000..90d8425 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/stream/MimeStream.java @@ -0,0 +1,25 @@ +package org.xbib.net.mime.stream; + +import org.xbib.net.mime.MimeException; + +import java.io.InputStream; + +public class MimeStream { + + private MimeStream() { + } + + public static InputStream decode(InputStream inputStream, String encoding) throws MimeException { + if (encoding.equalsIgnoreCase("base64")) + return new BASE64DecoderStream(inputStream); + else if (encoding.equalsIgnoreCase("quoted-printable")) + return new QuotedPrintableInputStream(inputStream); + else if (encoding.equalsIgnoreCase("binary") || + encoding.equalsIgnoreCase("7bit") || + encoding.equalsIgnoreCase("8bit")) + return inputStream; + else { + throw new MimeException("Unknown encoding: " + encoding); + } + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/stream/QuotedPrintableInputStream.java b/net-mime/src/main/java/org/xbib/net/mime/stream/QuotedPrintableInputStream.java new file mode 100644 index 0000000..2b14857 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/stream/QuotedPrintableInputStream.java @@ -0,0 +1,98 @@ +package org.xbib.net.mime.stream; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; + +public final class QuotedPrintableInputStream extends FilterInputStream { + + private int spaces = 0; + + public QuotedPrintableInputStream(InputStream in) { + super(new PushbackInputStream(in, 2)); + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an int in the range 0 + * to 255. If no byte is available because the end of + * the stream has been reached, the value -1 is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @throws IOException if an I/O error occurs. + */ + @Override + public int read() throws IOException { + if (spaces > 0) { + spaces--; + return ' '; + } + int c = in.read(); + if (c == ' ') { + while ((c = in.read()) == ' ') { + spaces++; + } + if (c == '\r' || c == '\n' || c == -1) { + spaces = 0; + } else { + ((PushbackInputStream) in).unread(c); + c = ' '; + } + return c; + } else if (c == '=') { + int a = in.read(); + if (a == '\n') { + return read(); + } else if (a == '\r') { + int b = in.read(); + if (b != '\n') { + ((PushbackInputStream) in).unread(b); + } + return read(); + } else if (a == -1) { + return -1; + } else { + return Hex.fromHex(a, in.read()); + } + } + return c; + } + + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int i, c; + for (i = 0; i < len; i++) { + if ((c = read()) == -1) { + if (i == 0) { + i = -1; + } + break; + } + buf[off + i] = (byte) c; + } + return i; + } + + @Override + public long skip(long n) throws IOException { + long skipped = 0; + while (n-- > 0 && read() >= 0) { + skipped++; + } + return skipped; + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int available() throws IOException { + return in.available(); + } +} diff --git a/net-mime/src/main/java/org/xbib/net/mime/stream/QuotedPrintableOutputStream.java b/net-mime/src/main/java/org/xbib/net/mime/stream/QuotedPrintableOutputStream.java new file mode 100644 index 0000000..1fa3578 --- /dev/null +++ b/net-mime/src/main/java/org/xbib/net/mime/stream/QuotedPrintableOutputStream.java @@ -0,0 +1,143 @@ +package org.xbib.net.mime.stream; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class QuotedPrintableOutputStream extends FilterOutputStream { + + // The encoding table + private final static char[] hex = { + '0','1', '2', '3', '4', '5', '6', '7', + '8','9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + private int count = 0; + + private final int bytesPerLine; + + private boolean gotSpace = false; + + private boolean gotCR = false; + + /** + * Create a QP encoder that encodes the specified input stream + * @param out the output stream + * @param bytesPerLine the number of bytes per line. The encoder + * inserts a CRLF sequence after this many number + * of bytes. + */ + public QuotedPrintableOutputStream(OutputStream out, int bytesPerLine) { + super(out); + this.bytesPerLine = bytesPerLine - 1; + } + + /** + * Create a QP encoder that encodes the specified input stream. + * Inserts the CRLF sequence after outputting 76 bytes. + * @param out the output stream + */ + public QuotedPrintableOutputStream(OutputStream out) { + this(out, 76); + } + + /** + * Encodes len bytes from the specified + * byte array starting at offset off to + * this output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @exception IOException if an I/O error occurs. + */ + public void write(byte[] b, int off, int len) throws IOException { + for (int i = 0; i < len; i++) { + write(b[off + i]); + } + } + + /** + * Encodes b.length bytes to this output stream. + * @param b the data to be written. + * @exception IOException if an I/O error occurs. + */ + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * Encodes the specified byte to this output stream. + * @param c the byte. + * @exception IOException if an I/O error occurs. + */ + public void write(int c) throws IOException { + c = c & 0xff; + if (gotSpace) { + output(' ', c == '\r' || c == '\n'); + gotSpace = false; + } + if (c == '\r') { + gotCR = true; + outputCRLF(); + } else { + if (c == '\n') { + if (!gotCR) { + outputCRLF(); + } + } else { + if (c == ' ') { + gotSpace = true; + } else { + output(c, c < 32 || c >= 127 || c == '='); + } + } + gotCR = false; + } + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be encoded out to the stream. + * @exception IOException if an I/O error occurs. + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Forces any buffered output bytes to be encoded out to the stream + * and closes this output stream + */ + public void close() throws IOException { + out.close(); + } + + private void outputCRLF() throws IOException { + out.write('\r'); + out.write('\n'); + count = 0; + } + + private void output(int c, boolean encode) throws IOException { + if (encode) { + if ((count += 3) > bytesPerLine) { + out.write('='); + out.write('\r'); + out.write('\n'); + count = 3; // set the next line's length + } + out.write('='); + out.write(hex[c >> 4]); + out.write(hex[c & 0xf]); + } else { + if (++count > bytesPerLine) { + out.write('='); + out.write('\r'); + out.write('\n'); + count = 1; // set the next line's length + } + out.write(c); + } + } +} diff --git a/net-mime/src/main/resources/META-INF/mime.types b/net-mime/src/main/resources/META-INF/mime.types new file mode 100755 index 0000000..9532a5e --- /dev/null +++ b/net-mime/src/main/resources/META-INF/mime.types @@ -0,0 +1,1853 @@ +# This file maps Internet media types to unique file extension(s). +# Although created for httpd, this file is used by many software systems +# and has been placed in the public domain for unlimited redisribution. +# +# The table below contains both registered and (common) unregistered types. +# A type that has no unique extension can be ignored -- they are listed +# here to guide configurations toward known types and to make it easier to +# identify "new" types. File extensions are also commonly used to indicate +# content languages and encodings, so choose them carefully. +# +# Internet media types should be registered as described in RFC 4288. +# The registry is at . +# +# MIME type (lowercased) Extensions +# ============================================ ========== +# application/1d-interleaved-parityfec +# application/3gpdash-qoe-report+xml +# application/3gpp-ims+xml +# application/a2l +# application/activemessage +# application/alto-costmap+json +# application/alto-costmapfilter+json +# application/alto-directory+json +# application/alto-endpointcost+json +# application/alto-endpointcostparams+json +# application/alto-endpointprop+json +# application/alto-endpointpropparams+json +# application/alto-error+json +# application/alto-networkmap+json +# application/alto-networkmapfilter+json +# application/aml +application/andrew-inset ez +# application/applefile +application/applixware aw +# application/atf +# application/atfx +application/atom+xml atom +application/atomcat+xml atomcat +# application/atomdeleted+xml +# application/atomicmail +application/atomsvc+xml atomsvc +# application/atxml +# application/auth-policy+xml +# application/bacnet-xdd+zip +# application/batch-smtp +# application/beep+xml +# application/calendar+json +# application/calendar+xml +# application/call-completion +# application/cals-1840 +# application/cbor +# application/ccmp+xml +application/ccxml+xml ccxml +# application/cdfx+xml +application/cdmi-capability cdmia +application/cdmi-container cdmic +application/cdmi-domain cdmid +application/cdmi-object cdmio +application/cdmi-queue cdmiq +# application/cdni +# application/cea +# application/cea-2018+xml +# application/cellml+xml +# application/cfw +# application/cms +# application/cnrp+xml +# application/coap-group+json +# application/commonground +# application/conference-info+xml +# application/cpl+xml +# application/csrattrs +# application/csta+xml +# application/cstadata+xml +# application/csvm+json +application/cu-seeme cu +# application/cybercash +# application/dash+xml +# application/dashdelta +application/davmount+xml davmount +# application/dca-rft +# application/dcd +# application/dec-dx +# application/dialog-info+xml +# application/dicom +# application/dii +# application/dit +# application/dns +application/docbook+xml dbk +# application/dskpp+xml +application/dssc+der dssc +application/dssc+xml xdssc +# application/dvcs +application/ecmascript ecma +# application/edi-consent +# application/edi-x12 +# application/edifact +# application/efi +# application/emergencycalldata.comment+xml +# application/emergencycalldata.deviceinfo+xml +# application/emergencycalldata.providerinfo+xml +# application/emergencycalldata.serviceinfo+xml +# application/emergencycalldata.subscriberinfo+xml +application/emma+xml emma +# application/emotionml+xml +# application/encaprtp +# application/epp+xml +application/epub+zip epub +# application/eshop +# application/example +application/exi exi +# application/fastinfoset +# application/fastsoap +# application/fdt+xml +# application/fits +# application/font-sfnt +application/font-tdpfr pfr +application/font-woff woff +# application/framework-attributes+xml +# application/geo+json +application/gml+xml gml +application/gpx+xml gpx +application/gxf gxf +# application/gzip +# application/h224 +# application/held+xml +# application/http +application/hyperstudio stk +# application/ibe-key-httpRequest+xml +# application/ibe-pkg-reply+xml +# application/ibe-pp-data +# application/iges +# application/im-iscomposing+xml +# application/index +# application/index.cmd +# application/index.obj +# application/index.response +# application/index.vnd +application/inkml+xml ink inkml +# application/iotp +application/ipfix ipfix +# application/ipp +# application/isup +# application/its+xml +application/java-archive jar +application/java-serialized-object ser +application/java-vm class +application/javascript js +# application/jose +# application/jose+json +# application/jrd+json +application/json json +# application/json-patch+json +# application/json-seq +application/jsonml+json jsonml +# application/jwk+json +# application/jwk-set+json +# application/jwt +# application/kpml-httpRequest+xml +# application/kpml-response+xml +# application/ld+json +# application/lgr+xml +# application/link-format +# application/load-control+xml +application/lost+xml lostxml +# application/lostsync+xml +# application/lxf +application/mac-binhex40 hqx +application/mac-compactpro cpt +# application/macwriteii +application/mads+xml mads +application/marc mrc +application/marcxml+xml mrcx +application/mathematica ma nb mb +application/mathml+xml mathml +# application/mathml-content+xml +# application/mathml-presentation+xml +# application/mbms-associated-procedure-description+xml +# application/mbms-deregister+xml +# application/mbms-envelope+xml +# application/mbms-msk+xml +# application/mbms-msk-response+xml +# application/mbms-protection-description+xml +# application/mbms-reception-report+xml +# application/mbms-register+xml +# application/mbms-register-response+xml +# application/mbms-schedule+xml +# application/mbms-user-service-description+xml +application/mbox mbox +# application/media-policy-dataset+xml +# application/media_control+xml +application/mediaservercontrol+xml mscml +# application/merge-patch+json +application/metalink+xml metalink +application/metalink4+xml meta4 +application/mets+xml mets +# application/mf4 +# application/mikey +application/mods+xml mods +# application/moss-keys +# application/moss-signature +# application/mosskey-data +# application/mosskey-httpRequest +application/mp21 m21 mp21 +application/mp4 mp4s +# application/mpeg4-generic +# application/mpeg4-iod +# application/mpeg4-iod-xmt +# application/mrb-consumer+xml +# application/mrb-publish+xml +# application/msc-ivr+xml +# application/msc-mixer+xml +application/msword doc dot +application/mxf mxf +# application/nasdata +# application/news-checkgroups +# application/news-groupinfo +# application/news-transmission +# application/nlsml+xml +# application/nss +# application/ocsp-httpRequest +# application/ocsp-response +application/octet-stream bin dms lrf mar so dist distz pkg bpk dump elc deploy +application/oda oda +# application/odx +application/oebps-package+xml opf +application/ogg ogx +application/omdoc+xml omdoc +application/onenote onetoc onetoc2 onetmp onepkg +application/oxps oxps +# application/p2p-overlay+xml +# application/parityfec +application/patch-ops-error+xml xer +application/pdf pdf +# application/pdx +application/pgp-encrypted pgp +# application/pgp-keys +application/pgp-signature asc sig +application/pics-rules prf +# application/pidf+xml +# application/pidf-diff+xml +application/pkcs10 p10 +# application/pkcs12 +application/pkcs7-mime p7m p7c +application/pkcs7-signature p7s +application/pkcs8 p8 +application/pkix-attr-cert ac +application/pkix-cert cer +application/pkix-crl crl +application/pkix-pkipath pkipath +application/pkixcmp pki +application/pls+xml pls +# application/poc-settings+xml +application/postscript ai eps ps +# application/ppsp-tracker+json +# application/problem+json +# application/problem+xml +# application/provenance+xml +# application/prs.alvestrand.titrax-sheet +application/prs.cww cww +# application/prs.hpub+zip +# application/prs.nprend +# application/prs.plucker +# application/prs.rdf-xml-crypt +# application/prs.xsf+xml +application/pskc+xml pskcxml +# application/qsig +# application/raptorfec +# application/rdap+json +application/rdf+xml rdf +application/reginfo+xml rif +application/relax-ng-compact-syntax rnc +# application/remote-printing +# application/reputon+json +application/resource-lists+xml rl +application/resource-lists-diff+xml rld +# application/rfc+xml +# application/riscos +# application/rlmi+xml +application/rls-services+xml rs +application/rpki-ghostbusters gbr +application/rpki-manifest mft +application/rpki-roa roa +# application/rpki-updown +application/rsd+xml rsd +application/rss+xml rss +application/rtf rtf +# application/rtploopback +# application/rtx +# application/samlassertion+xml +# application/samlmetadata+xml +application/sbml+xml sbml +# application/scaip+xml +# application/scim+json +application/scvp-cv-httpRequest scq +application/scvp-cv-response scs +application/scvp-vp-httpRequest spq +application/scvp-vp-response spp +application/sdp sdp +# application/sep+xml +# application/sep-exi +# application/session-info +# application/set-payment +application/set-payment-initiation setpay +# application/set-registration +application/set-registration-initiation setreg +# application/sgml +# application/sgml-open-catalog +application/shf+xml shf +# application/sieve +# application/simple-filter+xml +# application/simple-message-summary +# application/simplesymbolcontainer +# application/slate +# application/smil +application/smil+xml smi smil +# application/smpte336m +# application/soap+fastinfoset +# application/soap+xml +application/sparql-query rq +application/sparql-results+xml srx +# application/spirits-event+xml +# application/sql +application/srgs gram +application/srgs+xml grxml +application/sru+xml sru +application/ssdl+xml ssdl +application/ssml+xml ssml +# application/tamp-apex-update +# application/tamp-apex-update-confirm +# application/tamp-community-update +# application/tamp-community-update-confirm +# application/tamp-error +# application/tamp-sequence-adjust +# application/tamp-sequence-adjust-confirm +# application/tamp-status-query +# application/tamp-status-response +# application/tamp-update +# application/tamp-update-confirm +application/tei+xml tei teicorpus +application/thraud+xml tfi +# application/timestamp-query +# application/timestamp-reply +application/timestamped-data tsd +# application/ttml+xml +# application/tve-trigger +# application/ulpfec +# application/urc-grpsheet+xml +# application/urc-ressheet+xml +# application/urc-targetdesc+xml +# application/urc-uisocketdesc+xml +# application/vcard+json +# application/vcard+xml +# application/vemmi +# application/vividence.scriptfile +# application/vnd.3gpp-prose+xml +# application/vnd.3gpp-prose-pc3ch+xml +# application/vnd.3gpp.access-transfer-events+xml +# application/vnd.3gpp.bsf+xml +# application/vnd.3gpp.mid-call+xml +application/vnd.3gpp.pic-bw-large plb +application/vnd.3gpp.pic-bw-small psb +application/vnd.3gpp.pic-bw-var pvb +# application/vnd.3gpp.sms +# application/vnd.3gpp.sms+xml +# application/vnd.3gpp.srvcc-ext+xml +# application/vnd.3gpp.srvcc-info+xml +# application/vnd.3gpp.state-and-event-info+xml +# application/vnd.3gpp.ussd+xml +# application/vnd.3gpp2.bcmcsinfo+xml +# application/vnd.3gpp2.sms +application/vnd.3gpp2.tcap tcap +# application/vnd.3lightssoftware.imagescal +application/vnd.3m.post-it-notes pwn +application/vnd.accpac.simply.aso aso +application/vnd.accpac.simply.imp imp +application/vnd.acucobol acu +application/vnd.acucorp atc acutc +application/vnd.adobe.air-application-installer-package+zip air +# application/vnd.adobe.flash.movie +application/vnd.adobe.formscentral.fcdt fcdt +application/vnd.adobe.fxp fxp fxpl +# application/vnd.adobe.partial-upload +application/vnd.adobe.xdp+xml xdp +application/vnd.adobe.xfdf xfdf +# application/vnd.aether.imp +# application/vnd.ah-barcode +application/vnd.ahead.space ahead +application/vnd.airzip.filesecure.azf azf +application/vnd.airzip.filesecure.azs azs +application/vnd.amazon.ebook azw +# application/vnd.amazon.mobi8-ebook +application/vnd.americandynamics.acc acc +application/vnd.amiga.ami ami +# application/vnd.amundsen.maze+xml +application/vnd.android.package-archive apk +# application/vnd.anki +application/vnd.anser-web-certificate-issue-initiation cii +application/vnd.anser-web-funds-transfer-initiation fti +application/vnd.antix.game-component atx +# application/vnd.apache.thrift.binary +# application/vnd.apache.thrift.compact +# application/vnd.apache.thrift.json +# application/vnd.api+json +application/vnd.apple.installer+xml mpkg +application/vnd.apple.mpegurl m3u8 +# application/vnd.arastra.swi +application/vnd.aristanetworks.swi swi +# application/vnd.artsquare +application/vnd.astraea-software.iota iota +application/vnd.audiograph aep +# application/vnd.autopackage +# application/vnd.avistar+xml +# application/vnd.balsamiq.bmml+xml +# application/vnd.balsamiq.bmpr +# application/vnd.bekitzur-stech+json +# application/vnd.biopax.rdf+xml +application/vnd.blueice.multipass mpm +# application/vnd.bluetooth.ep.oob +# application/vnd.bluetooth.le.oob +application/vnd.bmi bmi +application/vnd.businessobjects rep +# application/vnd.cab-jscript +# application/vnd.canon-cpdl +# application/vnd.canon-lips +# application/vnd.cendio.thinlinc.clientconf +# application/vnd.century-systems.tcp_stream +application/vnd.chemdraw+xml cdxml +# application/vnd.chess-pgn +application/vnd.chipnuts.karaoke-mmd mmd +application/vnd.cinderella cdy +# application/vnd.cirpack.isdn-ext +# application/vnd.citationstyles.style+xml +application/vnd.claymore cla +application/vnd.cloanto.rp9 rp9 +application/vnd.clonk.c4group c4g c4d c4f c4p c4u +application/vnd.cluetrust.cartomobile-config c11amc +application/vnd.cluetrust.cartomobile-config-pkg c11amz +# application/vnd.coffeescript +# application/vnd.collection+json +# application/vnd.collection.doc+json +# application/vnd.collection.next+json +# application/vnd.comicbook+zip +# application/vnd.commerce-battelle +application/vnd.commonspace csp +application/vnd.contact.cmsg cdbcmsg +# application/vnd.coreos.ignition+json +application/vnd.cosmocaller cmc +application/vnd.crick.clicker clkx +application/vnd.crick.clicker.keyboard clkk +application/vnd.crick.clicker.palette clkp +application/vnd.crick.clicker.template clkt +application/vnd.crick.clicker.wordbank clkw +application/vnd.criticaltools.wbs+xml wbs +application/vnd.ctc-posml pml +# application/vnd.ctct.ws+xml +# application/vnd.cups-pdf +# application/vnd.cups-postscript +application/vnd.cups-ppd ppd +# application/vnd.cups-raster +# application/vnd.cups-raw +# application/vnd.curl +application/vnd.curl.car car +application/vnd.curl.pcurl pcurl +# application/vnd.cyan.dean.root+xml +# application/vnd.cybank +application/vnd.dart dart +application/vnd.data-vision.rdz rdz +# application/vnd.debian.binary-package +application/vnd.dece.data uvf uvvf uvd uvvd +application/vnd.dece.ttml+xml uvt uvvt +application/vnd.dece.unspecified uvx uvvx +application/vnd.dece.zip uvz uvvz +application/vnd.denovo.fcselayout-link fe_launch +# application/vnd.desmume.movie +# application/vnd.dir-bi.plate-dl-nosuffix +# application/vnd.dm.delegation+xml +application/vnd.dna dna +# application/vnd.document+json +application/vnd.dolby.mlp mlp +# application/vnd.dolby.mobile.1 +# application/vnd.dolby.mobile.2 +# application/vnd.doremir.scorecloud-binary-document +application/vnd.dpgraph dpg +application/vnd.dreamfactory dfac +# application/vnd.drive+json +application/vnd.ds-keypoint kpxx +# application/vnd.dtg.local +# application/vnd.dtg.local.flash +# application/vnd.dtg.local.html +application/vnd.dvb.ait ait +# application/vnd.dvb.dvbj +# application/vnd.dvb.esgcontainer +# application/vnd.dvb.ipdcdftnotifaccess +# application/vnd.dvb.ipdcesgaccess +# application/vnd.dvb.ipdcesgaccess2 +# application/vnd.dvb.ipdcesgpdd +# application/vnd.dvb.ipdcroaming +# application/vnd.dvb.iptv.alfec-base +# application/vnd.dvb.iptv.alfec-enhancement +# application/vnd.dvb.notif-aggregate-root+xml +# application/vnd.dvb.notif-container+xml +# application/vnd.dvb.notif-generic+xml +# application/vnd.dvb.notif-ia-msglist+xml +# application/vnd.dvb.notif-ia-registration-httpRequest+xml +# application/vnd.dvb.notif-ia-registration-response+xml +# application/vnd.dvb.notif-init+xml +# application/vnd.dvb.pfr +application/vnd.dvb.service svc +# application/vnd.dxr +application/vnd.dynageo geo +# application/vnd.dzr +# application/vnd.easykaraoke.cdgdownload +# application/vnd.ecdis-update +application/vnd.ecowin.chart mag +# application/vnd.ecowin.filerequest +# application/vnd.ecowin.fileupdate +# application/vnd.ecowin.series +# application/vnd.ecowin.seriesrequest +# application/vnd.ecowin.seriesupdate +# application/vnd.emclient.accessrequest+xml +application/vnd.enliven nml +# application/vnd.enphase.envoy +# application/vnd.eprints.data+xml +application/vnd.epson.esf esf +application/vnd.epson.msf msf +application/vnd.epson.quickanime qam +application/vnd.epson.salt slt +application/vnd.epson.ssf ssf +# application/vnd.ericsson.quickcall +application/vnd.eszigno3+xml es3 et3 +# application/vnd.etsi.aoc+xml +# application/vnd.etsi.asic-e+zip +# application/vnd.etsi.asic-s+zip +# application/vnd.etsi.cug+xml +# application/vnd.etsi.iptvcommand+xml +# application/vnd.etsi.iptvdiscovery+xml +# application/vnd.etsi.iptvprofile+xml +# application/vnd.etsi.iptvsad-bc+xml +# application/vnd.etsi.iptvsad-cod+xml +# application/vnd.etsi.iptvsad-npvr+xml +# application/vnd.etsi.iptvservice+xml +# application/vnd.etsi.iptvsync+xml +# application/vnd.etsi.iptvueprofile+xml +# application/vnd.etsi.mcid+xml +# application/vnd.etsi.mheg5 +# application/vnd.etsi.overload-control-policy-dataset+xml +# application/vnd.etsi.pstn+xml +# application/vnd.etsi.sci+xml +# application/vnd.etsi.simservs+xml +# application/vnd.etsi.timestamp-token +# application/vnd.etsi.tsl+xml +# application/vnd.etsi.tsl.der +# application/vnd.eudora.data +application/vnd.ezpix-album ez2 +application/vnd.ezpix-package ez3 +# application/vnd.f-secure.mobile +# application/vnd.fastcopy-disk-image +application/vnd.fdf fdf +application/vnd.fdsn.mseed mseed +application/vnd.fdsn.seed seed dataless +# application/vnd.ffsns +# application/vnd.filmit.zfc +# application/vnd.fints +# application/vnd.firemonkeys.cloudcell +application/vnd.flographit gph +application/vnd.fluxtime.clip ftc +# application/vnd.font-fontforge-sfd +application/vnd.framemaker fm frame maker book +application/vnd.frogans.fnc fnc +application/vnd.frogans.ltf ltf +application/vnd.fsc.weblaunch fsc +application/vnd.fujitsu.oasys oas +application/vnd.fujitsu.oasys2 oa2 +application/vnd.fujitsu.oasys3 oa3 +application/vnd.fujitsu.oasysgp fg5 +application/vnd.fujitsu.oasysprs bh2 +# application/vnd.fujixerox.art-ex +# application/vnd.fujixerox.art4 +application/vnd.fujixerox.ddd ddd +application/vnd.fujixerox.docuworks xdw +application/vnd.fujixerox.docuworks.binder xbd +# application/vnd.fujixerox.docuworks.container +# application/vnd.fujixerox.hbpl +# application/vnd.fut-misnet +application/vnd.fuzzysheet fzs +application/vnd.genomatix.tuxedo txd +# application/vnd.geo+json +# application/vnd.geocube+xml +application/vnd.geogebra.file ggb +application/vnd.geogebra.tool ggt +application/vnd.geometry-explorer gex gre +application/vnd.geonext gxt +application/vnd.geoplan g2w +application/vnd.geospace g3w +# application/vnd.gerber +# application/vnd.globalplatform.card-content-mgt +# application/vnd.globalplatform.card-content-mgt-response +application/vnd.gmx gmx +application/vnd.google-earth.kml+xml kml +application/vnd.google-earth.kmz kmz +# application/vnd.gov.sk.e-form+xml +# application/vnd.gov.sk.e-form+zip +# application/vnd.gov.sk.xmldatacontainer+xml +application/vnd.grafeq gqf gqs +# application/vnd.gridmp +application/vnd.groove-account gac +application/vnd.groove-help ghf +application/vnd.groove-identity-message gim +application/vnd.groove-injector grv +application/vnd.groove-tool-message gtm +application/vnd.groove-tool-template tpl +application/vnd.groove-vcard vcg +# application/vnd.hal+json +application/vnd.hal+xml hal +application/vnd.handheld-entertainment+xml zmm +application/vnd.hbci hbci +# application/vnd.hcl-bireports +# application/vnd.hdt +# application/vnd.heroku+json +application/vnd.hhe.lesson-player les +application/vnd.hp-hpgl hpgl +application/vnd.hp-hpid hpid +application/vnd.hp-hps hps +application/vnd.hp-jlyt jlt +application/vnd.hp-pcl pcl +application/vnd.hp-pclxl pclxl +# application/vnd.httphone +application/vnd.hydrostatix.sof-data sfd-hdstx +# application/vnd.hyperdrive+json +# application/vnd.hzn-3d-crossword +# application/vnd.ibm.afplinedata +# application/vnd.ibm.electronic-media +application/vnd.ibm.minipay mpy +application/vnd.ibm.modcap afp listafp list3820 +application/vnd.ibm.rights-management irm +application/vnd.ibm.secure-container sc +application/vnd.iccprofile icc icm +# application/vnd.ieee.1905 +application/vnd.igloader igl +application/vnd.immervision-ivp ivp +application/vnd.immervision-ivu ivu +# application/vnd.ims.imsccv1p1 +# application/vnd.ims.imsccv1p2 +# application/vnd.ims.imsccv1p3 +# application/vnd.ims.lis.v2.result+json +# application/vnd.ims.lti.v2.toolconsumerprofile+json +# application/vnd.ims.lti.v2.toolproxy+json +# application/vnd.ims.lti.v2.toolproxy.id+json +# application/vnd.ims.lti.v2.toolsettings+json +# application/vnd.ims.lti.v2.toolsettings.simple+json +# application/vnd.informedcontrol.rms+xml +# application/vnd.informix-visionary +# application/vnd.infotech.project +# application/vnd.infotech.project+xml +# application/vnd.innopath.wamp.notification +application/vnd.insors.igm igm +application/vnd.intercon.formnet xpw xpx +application/vnd.intergeo i2g +# application/vnd.intertrust.digibox +# application/vnd.intertrust.nncp +application/vnd.intu.qbo qbo +application/vnd.intu.qfx qfx +# application/vnd.iptc.g2.catalogitem+xml +# application/vnd.iptc.g2.conceptitem+xml +# application/vnd.iptc.g2.knowledgeitem+xml +# application/vnd.iptc.g2.newsitem+xml +# application/vnd.iptc.g2.newsmessage+xml +# application/vnd.iptc.g2.packageitem+xml +# application/vnd.iptc.g2.planningitem+xml +application/vnd.ipunplugged.rcprofile rcprofile +application/vnd.irepository.package+xml irp +application/vnd.is-xpr xpr +application/vnd.isac.fcs fcs +application/vnd.jam jam +# application/vnd.japannet-directory-service +# application/vnd.japannet-jpnstore-wakeup +# application/vnd.japannet-payment-wakeup +# application/vnd.japannet-registration +# application/vnd.japannet-registration-wakeup +# application/vnd.japannet-setstore-wakeup +# application/vnd.japannet-verification +# application/vnd.japannet-verification-wakeup +application/vnd.jcp.javame.midlet-rms rms +application/vnd.jisp jisp +application/vnd.joost.joda-archive joda +# application/vnd.jsk.isdn-ngn +application/vnd.kahootz ktz ktr +application/vnd.kde.karbon karbon +application/vnd.kde.kchart chrt +application/vnd.kde.kformula kfo +application/vnd.kde.kivio flw +application/vnd.kde.kontour kon +application/vnd.kde.kpresenter kpr kpt +application/vnd.kde.kspread ksp +application/vnd.kde.kword kwd kwt +application/vnd.kenameaapp htke +application/vnd.kidspiration kia +application/vnd.kinar kne knp +application/vnd.koan skp skd skt skm +application/vnd.kodak-descriptor sse +application/vnd.las.las+xml lasxml +# application/vnd.liberty-httpRequest+xml +application/vnd.llamagraphics.life-balance.desktop lbd +application/vnd.llamagraphics.life-balance.exchange+xml lbe +application/vnd.lotus-1-2-3 123 +application/vnd.lotus-approach apr +application/vnd.lotus-freelance pre +application/vnd.lotus-notes nsf +application/vnd.lotus-organizer org +application/vnd.lotus-screencam scm +application/vnd.lotus-wordpro lwp +application/vnd.macports.portpkg portpkg +# application/vnd.mapbox-vector-tile +# application/vnd.marlin.drm.actiontoken+xml +# application/vnd.marlin.drm.conftoken+xml +# application/vnd.marlin.drm.license+xml +# application/vnd.marlin.drm.mdcf +# application/vnd.mason+json +# application/vnd.maxmind.maxmind-db +application/vnd.mcd mcd +application/vnd.medcalcdata mc1 +application/vnd.mediastation.cdkey cdkey +# application/vnd.meridian-slingshot +application/vnd.mfer mwf +application/vnd.mfmp mfm +# application/vnd.micro+json +application/vnd.micrografx.flo flo +application/vnd.micrografx.igx igx +# application/vnd.microsoft.portable-executable +# application/vnd.miele+json +application/vnd.mif mif +# application/vnd.minisoft-hp3000-save +# application/vnd.mitsubishi.misty-guard.trustweb +application/vnd.mobius.daf daf +application/vnd.mobius.dis dis +application/vnd.mobius.mbk mbk +application/vnd.mobius.mqy mqy +application/vnd.mobius.msl msl +application/vnd.mobius.plc plc +application/vnd.mobius.txf txf +application/vnd.mophun.application mpn +application/vnd.mophun.certificate mpc +# application/vnd.motorola.flexsuite +# application/vnd.motorola.flexsuite.adsi +# application/vnd.motorola.flexsuite.fis +# application/vnd.motorola.flexsuite.gotap +# application/vnd.motorola.flexsuite.kmr +# application/vnd.motorola.flexsuite.ttc +# application/vnd.motorola.flexsuite.wem +# application/vnd.motorola.iprm +application/vnd.mozilla.xul+xml xul +# application/vnd.ms-3mfdocument +application/vnd.ms-artgalry cil +# application/vnd.ms-asf +application/vnd.ms-cab-compressed cab +# application/vnd.ms-color.iccprofile +application/vnd.ms-excel xls xlm xla xlc xlt xlw +application/vnd.ms-excel.addin.macroenabled.12 xlam +application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb +application/vnd.ms-excel.sheet.macroenabled.12 xlsm +application/vnd.ms-excel.template.macroenabled.12 xltm +application/vnd.ms-fontobject eot +application/vnd.ms-htmlhelp chm +application/vnd.ms-ims ims +application/vnd.ms-lrm lrm +# application/vnd.ms-office.activex+xml +application/vnd.ms-officetheme thmx +# application/vnd.ms-opentype +# application/vnd.ms-package.obfuscated-opentype +application/vnd.ms-pki.seccat cat +application/vnd.ms-pki.stl stl +# application/vnd.ms-playready.initiator+xml +application/vnd.ms-powerpoint ppt pps pot +application/vnd.ms-powerpoint.addin.macroenabled.12 ppam +application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm +application/vnd.ms-powerpoint.slide.macroenabled.12 sldm +application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm +application/vnd.ms-powerpoint.template.macroenabled.12 potm +# application/vnd.ms-printdevicecapabilities+xml +# application/vnd.ms-printing.printticket+xml +# application/vnd.ms-printschematicket+xml +application/vnd.ms-project mpp mpt +# application/vnd.ms-tnef +# application/vnd.ms-windows.devicepairing +# application/vnd.ms-windows.nwprinting.oob +# application/vnd.ms-windows.printerpairing +# application/vnd.ms-windows.wsd.oob +# application/vnd.ms-wmdrm.lic-chlg-req +# application/vnd.ms-wmdrm.lic-resp +# application/vnd.ms-wmdrm.meter-chlg-req +# application/vnd.ms-wmdrm.meter-resp +application/vnd.ms-word.document.macroenabled.12 docm +application/vnd.ms-word.template.macroenabled.12 dotm +application/vnd.ms-works wps wks wcm wdb +application/vnd.ms-wpl wpl +application/vnd.ms-xpsdocument xps +# application/vnd.msa-disk-image +application/vnd.mseq mseq +# application/vnd.msign +# application/vnd.multiad.creator +# application/vnd.multiad.creator.cif +# application/vnd.music-niff +application/vnd.musician mus +application/vnd.muvee.style msty +application/vnd.mynfc taglet +# application/vnd.ncd.control +# application/vnd.ncd.reference +# application/vnd.nervana +# application/vnd.netfpx +application/vnd.neurolanguage.nlu nlu +# application/vnd.nintendo.nitro.rom +# application/vnd.nintendo.snes.rom +application/vnd.nitf ntf nitf +application/vnd.noblenet-directory nnd +application/vnd.noblenet-sealer nns +application/vnd.noblenet-web nnw +# application/vnd.nokia.catalogs +# application/vnd.nokia.conml+wbxml +# application/vnd.nokia.conml+xml +# application/vnd.nokia.iptv.config+xml +# application/vnd.nokia.isds-radio-presets +# application/vnd.nokia.landmark+wbxml +# application/vnd.nokia.landmark+xml +# application/vnd.nokia.landmarkcollection+xml +# application/vnd.nokia.n-gage.ac+xml +application/vnd.nokia.n-gage.data ngdat +application/vnd.nokia.n-gage.symbian.install n-gage +# application/vnd.nokia.ncd +# application/vnd.nokia.pcd+wbxml +# application/vnd.nokia.pcd+xml +application/vnd.nokia.radio-preset rpst +application/vnd.nokia.radio-presets rpss +application/vnd.novadigm.edm edm +application/vnd.novadigm.edx edx +application/vnd.novadigm.ext ext +# application/vnd.ntt-local.content-share +# application/vnd.ntt-local.file-transfer +# application/vnd.ntt-local.ogw_remote-access +# application/vnd.ntt-local.sip-ta_remote +# application/vnd.ntt-local.sip-ta_tcp_stream +application/vnd.oasis.opendocument.chart odc +application/vnd.oasis.opendocument.chart-template otc +application/vnd.oasis.opendocument.database odb +application/vnd.oasis.opendocument.formula odf +application/vnd.oasis.opendocument.formula-template odft +application/vnd.oasis.opendocument.graphics odg +application/vnd.oasis.opendocument.graphics-template otg +application/vnd.oasis.opendocument.image odi +application/vnd.oasis.opendocument.image-template oti +application/vnd.oasis.opendocument.presentation odp +application/vnd.oasis.opendocument.presentation-template otp +application/vnd.oasis.opendocument.spreadsheet ods +application/vnd.oasis.opendocument.spreadsheet-template ots +application/vnd.oasis.opendocument.text odt +application/vnd.oasis.opendocument.text-master odm +application/vnd.oasis.opendocument.text-template ott +application/vnd.oasis.opendocument.text-web oth +# application/vnd.obn +# application/vnd.oftn.l10n+json +# application/vnd.oipf.contentaccessdownload+xml +# application/vnd.oipf.contentaccessstreaming+xml +# application/vnd.oipf.cspg-hexbinary +# application/vnd.oipf.dae.svg+xml +# application/vnd.oipf.dae.xhtml+xml +# application/vnd.oipf.mippvcontrolmessage+xml +# application/vnd.oipf.pae.gem +# application/vnd.oipf.spdiscovery+xml +# application/vnd.oipf.spdlist+xml +# application/vnd.oipf.ueprofile+xml +# application/vnd.oipf.userprofile+xml +application/vnd.olpc-sugar xo +# application/vnd.oma-scws-config +# application/vnd.oma-scws-http-httpRequest +# application/vnd.oma-scws-http-response +# application/vnd.oma.bcast.associated-procedure-parameter+xml +# application/vnd.oma.bcast.drm-trigger+xml +# application/vnd.oma.bcast.imd+xml +# application/vnd.oma.bcast.ltkm +# application/vnd.oma.bcast.notification+xml +# application/vnd.oma.bcast.provisioningtrigger +# application/vnd.oma.bcast.sgboot +# application/vnd.oma.bcast.sgdd+xml +# application/vnd.oma.bcast.sgdu +# application/vnd.oma.bcast.simple-symbol-container +# application/vnd.oma.bcast.smartcard-trigger+xml +# application/vnd.oma.bcast.sprov+xml +# application/vnd.oma.bcast.stkm +# application/vnd.oma.cab-address-book+xml +# application/vnd.oma.cab-feature-handler+xml +# application/vnd.oma.cab-pcc+xml +# application/vnd.oma.cab-subs-invite+xml +# application/vnd.oma.cab-user-prefs+xml +# application/vnd.oma.dcd +# application/vnd.oma.dcdc +application/vnd.oma.dd2+xml dd2 +# application/vnd.oma.drm.risd+xml +# application/vnd.oma.group-usage-list+xml +# application/vnd.oma.lwm2m+json +# application/vnd.oma.lwm2m+tlv +# application/vnd.oma.pal+xml +# application/vnd.oma.poc.detailed-progress-report+xml +# application/vnd.oma.poc.final-report+xml +# application/vnd.oma.poc.groups+xml +# application/vnd.oma.poc.invocation-descriptor+xml +# application/vnd.oma.poc.optimized-progress-report+xml +# application/vnd.oma.push +# application/vnd.oma.scidm.messages+xml +# application/vnd.oma.xcap-directory+xml +# application/vnd.omads-email+xml +# application/vnd.omads-file+xml +# application/vnd.omads-folder+xml +# application/vnd.omaloc-supl-init +# application/vnd.onepager +# application/vnd.openblox.game+xml +# application/vnd.openblox.game-binary +# application/vnd.openeye.oeb +application/vnd.openofficeorg.extension oxt +# application/vnd.openxmlformats-officedocument.custom-properties+xml +# application/vnd.openxmlformats-officedocument.customxmlproperties+xml +# application/vnd.openxmlformats-officedocument.drawing+xml +# application/vnd.openxmlformats-officedocument.drawingml.chart+xml +# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml +# application/vnd.openxmlformats-officedocument.extended-properties+xml +# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml +# application/vnd.openxmlformats-officedocument.presentationml.comments+xml +# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml +application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml +application/vnd.openxmlformats-officedocument.presentationml.slide sldx +# application/vnd.openxmlformats-officedocument.presentationml.slide+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml +application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx +# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml +# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml +# application/vnd.openxmlformats-officedocument.presentationml.tags+xml +application/vnd.openxmlformats-officedocument.presentationml.template potx +# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx +# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml +# application/vnd.openxmlformats-officedocument.theme+xml +# application/vnd.openxmlformats-officedocument.themeoverride+xml +# application/vnd.openxmlformats-officedocument.vmldrawing +# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx +# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml +# application/vnd.openxmlformats-package.core-properties+xml +# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml +# application/vnd.openxmlformats-package.relationships+xml +# application/vnd.oracle.resource+json +# application/vnd.orange.indata +# application/vnd.osa.netdeploy +application/vnd.osgeo.mapguide.package mgp +# application/vnd.osgi.bundle +application/vnd.osgi.dp dp +application/vnd.osgi.subsystem esa +# application/vnd.otps.ct-kip+xml +# application/vnd.oxli.countgraph +# application/vnd.pagerduty+json +application/vnd.palm pdb pqa oprc +# application/vnd.panoply +# application/vnd.paos.xml +application/vnd.pawaafile paw +# application/vnd.pcos +application/vnd.pg.format str +application/vnd.pg.osasli ei6 +# application/vnd.piaccess.application-licence +application/vnd.picsel efif +application/vnd.pmi.widget wg +# application/vnd.poc.group-advertisement+xml +application/vnd.pocketlearn plf +application/vnd.powerbuilder6 pbd +# application/vnd.powerbuilder6-s +# application/vnd.powerbuilder7 +# application/vnd.powerbuilder7-s +# application/vnd.powerbuilder75 +# application/vnd.powerbuilder75-s +# application/vnd.preminet +application/vnd.previewsystems.box box +application/vnd.proteus.magazine mgz +application/vnd.publishare-delta-tree qps +application/vnd.pvi.ptid1 ptid +# application/vnd.pwg-multiplexed +# application/vnd.pwg-xhtml-print+xml +# application/vnd.qualcomm.brew-app-res +# application/vnd.quarantainenet +application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb +# application/vnd.quobject-quoxdocument +# application/vnd.radisys.moml+xml +# application/vnd.radisys.msml+xml +# application/vnd.radisys.msml-audit+xml +# application/vnd.radisys.msml-audit-conf+xml +# application/vnd.radisys.msml-audit-conn+xml +# application/vnd.radisys.msml-audit-dialog+xml +# application/vnd.radisys.msml-audit-stream+xml +# application/vnd.radisys.msml-conf+xml +# application/vnd.radisys.msml-dialog+xml +# application/vnd.radisys.msml-dialog-base+xml +# application/vnd.radisys.msml-dialog-fax-detect+xml +# application/vnd.radisys.msml-dialog-fax-sendrecv+xml +# application/vnd.radisys.msml-dialog-group+xml +# application/vnd.radisys.msml-dialog-speech+xml +# application/vnd.radisys.msml-dialog-transform+xml +# application/vnd.rainstor.data +# application/vnd.rapid +# application/vnd.rar +application/vnd.realvnc.bed bed +application/vnd.recordare.musicxml mxl +application/vnd.recordare.musicxml+xml musicxml +# application/vnd.renlearn.rlprint +application/vnd.rig.cryptonote cryptonote +application/vnd.rim.cod cod +application/vnd.rn-realmedia rm +application/vnd.rn-realmedia-vbr rmvb +application/vnd.route66.link66+xml link66 +# application/vnd.rs-274x +# application/vnd.ruckus.download +# application/vnd.s3sms +application/vnd.sailingtracker.track st +# application/vnd.sbm.cid +# application/vnd.sbm.mid2 +# application/vnd.scribus +# application/vnd.sealed.3df +# application/vnd.sealed.csf +# application/vnd.sealed.doc +# application/vnd.sealed.eml +# application/vnd.sealed.mht +# application/vnd.sealed.net +# application/vnd.sealed.ppt +# application/vnd.sealed.tiff +# application/vnd.sealed.xls +# application/vnd.sealedmedia.softseal.html +# application/vnd.sealedmedia.softseal.pdf +application/vnd.seemail see +application/vnd.sema sema +application/vnd.semd semd +application/vnd.semf semf +application/vnd.shana.informed.formdata ifm +application/vnd.shana.informed.formtemplate itp +application/vnd.shana.informed.interchange iif +application/vnd.shana.informed.package ipk +application/vnd.simtech-mindmapper twd twds +# application/vnd.siren+json +application/vnd.smaf mmf +# application/vnd.smart.notebook +application/vnd.smart.teacher teacher +# application/vnd.software602.filler.form+xml +# application/vnd.software602.filler.form-xml-zip +application/vnd.solent.sdkm+xml sdkm sdkd +application/vnd.spotfire.dxp dxp +application/vnd.spotfire.sfs sfs +# application/vnd.sss-cod +# application/vnd.sss-dtf +# application/vnd.sss-ntf +application/vnd.stardivision.calc sdc +application/vnd.stardivision.draw sda +application/vnd.stardivision.impress sdd +application/vnd.stardivision.math smf +application/vnd.stardivision.writer sdw vor +application/vnd.stardivision.writer-global sgl +application/vnd.stepmania.package smzip +application/vnd.stepmania.stepchart sm +# application/vnd.street-stream +# application/vnd.sun.wadl+xml +application/vnd.sun.xml.calc sxc +application/vnd.sun.xml.calc.template stc +application/vnd.sun.xml.draw sxd +application/vnd.sun.xml.draw.template std +application/vnd.sun.xml.impress sxi +application/vnd.sun.xml.impress.template sti +application/vnd.sun.xml.math sxm +application/vnd.sun.xml.writer sxw +application/vnd.sun.xml.writer.global sxg +application/vnd.sun.xml.writer.template stw +application/vnd.sus-calendar sus susp +application/vnd.svd svd +# application/vnd.swiftview-ics +application/vnd.symbian.install sis sisx +application/vnd.syncml+xml xsm +application/vnd.syncml.dm+wbxml bdm +application/vnd.syncml.dm+xml xdm +# application/vnd.syncml.dm.notification +# application/vnd.syncml.dmddf+wbxml +# application/vnd.syncml.dmddf+xml +# application/vnd.syncml.dmtnds+wbxml +# application/vnd.syncml.dmtnds+xml +# application/vnd.syncml.ds.notification +application/vnd.tao.intent-module-archive tao +application/vnd.tcpdump.pcap pcap cap dmp +# application/vnd.tmd.mediaflex.api+xml +# application/vnd.tml +application/vnd.tmobile-livetv tmo +application/vnd.trid.tpt tpt +application/vnd.triscape.mxs mxs +application/vnd.trueapp tra +# application/vnd.truedoc +# application/vnd.ubisoft.webplayer +application/vnd.ufdl ufd ufdl +application/vnd.uiq.theme utz +application/vnd.umajin umj +application/vnd.unity unityweb +application/vnd.uoml+xml uoml +# application/vnd.uplanet.alert +# application/vnd.uplanet.alert-wbxml +# application/vnd.uplanet.bearer-choice +# application/vnd.uplanet.bearer-choice-wbxml +# application/vnd.uplanet.cacheop +# application/vnd.uplanet.cacheop-wbxml +# application/vnd.uplanet.channel +# application/vnd.uplanet.channel-wbxml +# application/vnd.uplanet.list +# application/vnd.uplanet.list-wbxml +# application/vnd.uplanet.listcmd +# application/vnd.uplanet.listcmd-wbxml +# application/vnd.uplanet.signal +# application/vnd.uri-map +# application/vnd.valve.source.material +application/vnd.vcx vcx +# application/vnd.vd-study +# application/vnd.vectorworks +# application/vnd.vel+json +# application/vnd.verimatrix.vcas +# application/vnd.vidsoft.vidconference +application/vnd.visio vsd vst vss vsw +application/vnd.visionary vis +# application/vnd.vividence.scriptfile +application/vnd.vsf vsf +# application/vnd.wap.sic +# application/vnd.wap.slc +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/vnd.webturbo wtb +# application/vnd.wfa.p2p +# application/vnd.wfa.wsc +# application/vnd.windows.devicepairing +# application/vnd.wmc +# application/vnd.wmf.bootstrap +# application/vnd.wolfram.mathematica +# application/vnd.wolfram.mathematica.package +application/vnd.wolfram.player nbp +application/vnd.wordperfect wpd +application/vnd.wqd wqd +# application/vnd.wrq-hp3000-labelled +application/vnd.wt.stf stf +# application/vnd.wv.csp+wbxml +# application/vnd.wv.csp+xml +# application/vnd.wv.ssp+xml +# application/vnd.xacml+json +application/vnd.xara xar +application/vnd.xfdl xfdl +# application/vnd.xfdl.webform +# application/vnd.xmi+xml +# application/vnd.xmpie.cpkg +# application/vnd.xmpie.dpkg +# application/vnd.xmpie.plan +# application/vnd.xmpie.ppkg +# application/vnd.xmpie.xlim +application/vnd.yamaha.hv-dic hvd +application/vnd.yamaha.hv-script hvs +application/vnd.yamaha.hv-voice hvp +application/vnd.yamaha.openscoreformat osf +application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg +# application/vnd.yamaha.remote-setup +application/vnd.yamaha.smaf-audio saf +application/vnd.yamaha.smaf-phrase spf +# application/vnd.yamaha.through-ngn +# application/vnd.yamaha.tunnel-udpencap +# application/vnd.yaoweme +application/vnd.yellowriver-custom-menu cmp +application/vnd.zul zir zirz +application/vnd.zzazz.deck+xml zaz +application/voicexml+xml vxml +# application/vq-rtcpxr +# application/watcherinfo+xml +# application/whoispp-query +# application/whoispp-response +application/widget wgt +application/winhlp hlp +# application/wita +# application/wordperfect5.1 +application/wsdl+xml wsdl +application/wspolicy+xml wspolicy +application/x-7z-compressed 7z +application/x-abiword abw +application/x-ace-compressed ace +# application/x-amf +application/x-apple-diskimage dmg +application/x-authorware-bin aab x32 u32 vox +application/x-authorware-map aam +application/x-authorware-seg aas +application/x-bcpio bcpio +application/x-bittorrent torrent +application/x-blorb blb blorb +application/x-bzip bz +application/x-bzip2 bz2 boz +application/x-cbr cbr cba cbt cbz cb7 +application/x-cdlink vcd +application/x-cfs-compressed cfs +application/x-chat chat +application/x-chess-pgn pgn +# application/x-compress +application/x-conference nsc +application/x-cpio cpio +application/x-csh csh +application/x-debian-package deb udeb +application/x-dgc-compressed dgc +application/x-director dir dcr dxr cst cct cxt w3d fgd swa +application/x-doom wad +application/x-dtbncx+xml ncx +application/x-dtbook+xml dtb +application/x-dtbresource+xml res +application/x-dvi dvi +application/x-envoy evy +application/x-eva eva +application/x-font-bdf bdf +# application/x-font-dos +# application/x-font-framemaker +application/x-font-ghostscript gsf +# application/x-font-libgrx +application/x-font-linux-psf psf +application/x-font-otf otf +application/x-font-pcf pcf +application/x-font-snf snf +# application/x-font-speedo +# application/x-font-sunos-news +application/x-font-ttf ttf ttc +application/x-font-type1 pfa pfb pfm afm +# application/x-font-vfont +application/x-freearc arc +application/x-futuresplash spl +application/x-gca-compressed gca +application/x-glulx ulx +application/x-gnumeric gnumeric +application/x-gramps-xml gramps +application/x-gtar gtar +# application/x-gzip +application/x-hdf hdf +application/x-install-instructions install +application/x-iso9660-image iso +application/x-java-jnlp-file jnlp +application/x-latex latex +application/x-lzh-compressed lzh lha +application/x-mie mie +application/x-mobipocket-ebook prc mobi +application/x-ms-application application +application/x-ms-shortcut lnk +application/x-ms-wmd wmd +application/x-ms-wmz wmz +application/x-ms-xbap xbap +application/x-msaccess mdb +application/x-msbinder obd +application/x-mscardfile crd +application/x-msclip clp +application/x-msdownload exe dll com bat msi +application/x-msmediaview mvb m13 m14 +application/x-msmetafile wmf wmz emf emz +application/x-msmoney mny +application/x-mspublisher pub +application/x-msschedule scd +application/x-msterminal trm +application/x-mswrite wri +application/x-netcdf nc cdf +application/x-nzb nzb +application/x-pkcs12 p12 pfx +application/x-pkcs7-certificates p7b spc +application/x-pkcs7-certreqresp p7r +application/x-rar-compressed rar +application/x-research-info-systems ris +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-silverlight-app xap +application/x-sql sql +application/x-stuffit sit +application/x-stuffitx sitx +application/x-subrip srt +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-t3vm-image t3 +application/x-tads gam +application/x-tar tar +application/x-tcl tcl +application/x-tex tex +application/x-tex-tfm tfm +application/x-texinfo texinfo texi +application/x-tgif obj +application/x-ustar ustar +application/x-wais-source src +# application/x-www-form-urlencoded +application/x-x509-ca-cert der crt +application/x-xfig fig +application/x-xliff+xml xlf +application/x-xpinstall xpi +application/x-xz xz +application/x-zmachine z1 z2 z3 z4 z5 z6 z7 z8 +# application/x400-bp +# application/xacml+xml +application/xaml+xml xaml +# application/xcap-att+xml +# application/xcap-caps+xml +application/xcap-diff+xml xdf +# application/xcap-el+xml +# application/xcap-error+xml +# application/xcap-ns+xml +# application/xcon-conference-info+xml +# application/xcon-conference-info-diff+xml +application/xenc+xml xenc +application/xhtml+xml xhtml xht +# application/xhtml-voice+xml +application/xml xml xsl +application/xml-dtd dtd +# application/xml-external-parsed-entity +# application/xml-patch+xml +# application/xmpp+xml +application/xop+xml xop +application/xproc+xml xpl +application/xslt+xml xslt +application/xspf+xml xspf +application/xv+xml mxml xhvml xvml xvm +application/yang yang +application/yin+xml yin +application/zip zip +# application/zlib +# audio/1d-interleaved-parityfec +# audio/32kadpcm +# audio/3gpp +# audio/3gpp2 +# audio/ac3 +audio/adpcm adp +# audio/amr +# audio/amr-wb +# audio/amr-wb+ +# audio/aptx +# audio/asc +# audio/atrac-advanced-lossless +# audio/atrac-x +# audio/atrac3 +audio/basic au snd +# audio/bv16 +# audio/bv32 +# audio/clearmode +# audio/cn +# audio/dat12 +# audio/dls +# audio/dsr-es201108 +# audio/dsr-es202050 +# audio/dsr-es202211 +# audio/dsr-es202212 +# audio/dv +# audio/dvi4 +# audio/eac3 +# audio/encaprtp +# audio/evrc +# audio/evrc-qcp +# audio/evrc0 +# audio/evrc1 +# audio/evrcb +# audio/evrcb0 +# audio/evrcb1 +# audio/evrcnw +# audio/evrcnw0 +# audio/evrcnw1 +# audio/evrcwb +# audio/evrcwb0 +# audio/evrcwb1 +# audio/evs +# audio/example +# audio/fwdred +# audio/g711-0 +# audio/g719 +# audio/g722 +# audio/g7221 +# audio/g723 +# audio/g726-16 +# audio/g726-24 +# audio/g726-32 +# audio/g726-40 +# audio/g728 +# audio/g729 +# audio/g7291 +# audio/g729d +# audio/g729e +# audio/gsm +# audio/gsm-efr +# audio/gsm-hr-08 +# audio/ilbc +# audio/ip-mr_v2.5 +# audio/isac +# audio/l16 +# audio/l20 +# audio/l24 +# audio/l8 +# audio/lpc +audio/midi mid midi kar rmi +# audio/mobile-xmf +audio/mp4 m4a mp4a +# audio/mp4a-latm +# audio/mpa +# audio/mpa-robust +audio/mpeg mpga mp2 mp2a mp3 m2a m3a +# audio/mpeg4-generic +# audio/musepack +audio/ogg oga ogg spx +# audio/opus +# audio/parityfec +# audio/pcma +# audio/pcma-wb +# audio/pcmu +# audio/pcmu-wb +# audio/prs.sid +# audio/qcelp +# audio/raptorfec +# audio/red +# audio/rtp-enc-aescm128 +# audio/rtp-midi +# audio/rtploopback +# audio/rtx +audio/s3m s3m +audio/silk sil +# audio/smv +# audio/smv-qcp +# audio/smv0 +# audio/sp-midi +# audio/speex +# audio/t140c +# audio/t38 +# audio/telephone-event +# audio/tone +# audio/uemclip +# audio/ulpfec +# audio/vdvi +# audio/vmr-wb +# audio/vnd.3gpp.iufp +# audio/vnd.4sb +# audio/vnd.audiokoz +# audio/vnd.celp +# audio/vnd.cisco.nse +# audio/vnd.cmles.radio-events +# audio/vnd.cns.anp1 +# audio/vnd.cns.inf1 +audio/vnd.dece.audio uva uvva +audio/vnd.digital-winds eol +# audio/vnd.dlna.adts +# audio/vnd.dolby.heaac.1 +# audio/vnd.dolby.heaac.2 +# audio/vnd.dolby.mlp +# audio/vnd.dolby.mps +# audio/vnd.dolby.pl2 +# audio/vnd.dolby.pl2x +# audio/vnd.dolby.pl2z +# audio/vnd.dolby.pulse.1 +audio/vnd.dra dra +audio/vnd.dts dts +audio/vnd.dts.hd dtshd +# audio/vnd.dvb.file +# audio/vnd.everad.plj +# audio/vnd.hns.audio +audio/vnd.lucent.voice lvp +audio/vnd.ms-playready.media.pya pya +# audio/vnd.nokia.mobile-xmf +# audio/vnd.nortel.vbk +audio/vnd.nuera.ecelp4800 ecelp4800 +audio/vnd.nuera.ecelp7470 ecelp7470 +audio/vnd.nuera.ecelp9600 ecelp9600 +# audio/vnd.octel.sbc +# audio/vnd.qcelp +# audio/vnd.rhetorex.32kadpcm +audio/vnd.rip rip +# audio/vnd.sealedmedia.softseal.mpeg +# audio/vnd.vmx.cvsd +# audio/vorbis +# audio/vorbis-config +audio/webm weba +audio/x-aac aac +audio/x-aiff aif aiff aifc +audio/x-caf caf +audio/x-flac flac +audio/x-matroska mka +audio/x-mpegurl m3u +audio/x-ms-wax wax +audio/x-ms-wma wma +audio/x-pn-realaudio ram ra +audio/x-pn-realaudio-plugin rmp +# audio/x-tta +audio/x-wav wav +audio/xm xm +chemical/x-cdx cdx +chemical/x-cif cif +chemical/x-cmdf cmdf +chemical/x-cml cml +chemical/x-csml csml +# chemical/x-pdb +chemical/x-xyz xyz +image/bmp bmp +image/cgm cgm +# image/dicom-rle +# image/emf +# image/example +# image/fits +image/g3fax g3 +image/gif gif +image/ief ief +# image/jls +# image/jp2 +image/jpeg jpeg jpg jpe +# image/jpm +# image/jpx +image/ktx ktx +# image/naplps +image/png png +image/prs.btif btif +# image/prs.pti +# image/pwg-raster +image/sgi sgi +image/svg+xml svg svgz +# image/t38 +image/tiff tiff tif +# image/tiff-fx +image/vnd.adobe.photoshop psd +# image/vnd.airzip.accelerator.azv +# image/vnd.cns.inf2 +image/vnd.dece.graphic uvi uvvi uvg uvvg +image/vnd.djvu djvu djv +image/vnd.dvb.subtitle sub +image/vnd.dwg dwg +image/vnd.dxf dxf +image/vnd.fastbidsheet fbs +image/vnd.fpx fpx +image/vnd.fst fst +image/vnd.fujixerox.edmics-mmr mmr +image/vnd.fujixerox.edmics-rlc rlc +# image/vnd.globalgraphics.pgb +# image/vnd.microsoft.icon +# image/vnd.mix +# image/vnd.mozilla.apng +image/vnd.ms-modi mdi +image/vnd.ms-photo wdp +image/vnd.net-fpx npx +# image/vnd.radiance +# image/vnd.sealed.png +# image/vnd.sealedmedia.softseal.gif +# image/vnd.sealedmedia.softseal.jpg +# image/vnd.svf +# image/vnd.tencent.tap +# image/vnd.valve.source.texture +image/vnd.wap.wbmp wbmp +image/vnd.xiff xif +# image/vnd.zbrush.pcx +image/webp webp +# image/wmf +image/x-3ds 3ds +image/x-cmu-raster ras +image/x-cmx cmx +image/x-freehand fh fhc fh4 fh5 fh7 +image/x-icon ico +image/x-mrsid-image sid +image/x-pcx pcx +image/x-pict pic pct +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-rgb rgb +image/x-tga tga +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +# message/cpim +# message/delivery-status +# message/disposition-notification +# message/example +# message/external-body +# message/feedback-report +# message/global +# message/global-delivery-status +# message/global-disposition-notification +# message/global-headers +# message/http +# message/imdn+xml +# message/news +# message/partial +message/rfc822 eml mime +# message/s-http +# message/sip +# message/sipfrag +# message/tracking-status +# message/vnd.si.simp +# message/vnd.wfa.wsc +# model/example +# model/gltf+json +model/iges igs iges +model/mesh msh mesh silo +model/vnd.collada+xml dae +model/vnd.dwf dwf +# model/vnd.flatland.3dml +model/vnd.gdl gdl +# model/vnd.gs-gdl +# model/vnd.gs.gdl +model/vnd.gtw gtw +# model/vnd.moml+xml +model/vnd.mts mts +# model/vnd.opengex +# model/vnd.parasolid.transmit.binary +# model/vnd.parasolid.transmit.text +# model/vnd.rosette.annotated-data-model +# model/vnd.valve.source.compiled-map +model/vnd.vtu vtu +model/vrml wrl vrml +model/x3d+binary x3db x3dbz +# model/x3d+fastinfoset +model/x3d+vrml x3dv x3dvz +model/x3d+xml x3d x3dz +# model/x3d-vrml +# multipart/alternative +# multipart/appledouble +# multipart/byteranges +# multipart/digest +# multipart/encrypted +# multipart/example +# multipart/form-data +# multipart/header-set +# multipart/mixed +# multipart/parallel +# multipart/related +# multipart/report +# multipart/signed +# multipart/voice-message +# multipart/x-mixed-replace +# text/1d-interleaved-parityfec +text/cache-manifest appcache +text/calendar ics ifb +text/css css +text/csv csv +# text/csv-schema +# text/directory +# text/dns +# text/ecmascript +# text/encaprtp +# text/enriched +# text/example +# text/fwdred +# text/grammar-ref-list +text/html html htm +# text/javascript +# text/jcr-cnd +# text/markdown +# text/mizar +text/n3 n3 +# text/parameters +# text/parityfec +text/plain txt text conf def list log in +# text/provenance-notation +# text/prs.fallenstein.rst +text/prs.lines.tag dsc +# text/prs.prop.logic +# text/raptorfec +# text/red +# text/rfc822-headers +text/richtext rtx +# text/rtf +# text/rtp-enc-aescm128 +# text/rtploopback +# text/rtx +text/sgml sgml sgm +# text/t140 +text/tab-separated-values tsv +text/troff t tr roff man me ms +text/turtle ttl +# text/ulpfec +text/uri-list uri uris urls +text/vcard vcard +# text/vnd.a +# text/vnd.abc +text/vnd.curl curl +text/vnd.curl.dcurl dcurl +text/vnd.curl.mcurl mcurl +text/vnd.curl.scurl scurl +# text/vnd.debian.copyright +# text/vnd.dmclientscript +text/vnd.dvb.subtitle sub +# text/vnd.esmertec.theme-descriptor +text/vnd.fly fly +text/vnd.fmi.flexstor flx +text/vnd.graphviz gv +text/vnd.in3d.3dml 3dml +text/vnd.in3d.spot spot +# text/vnd.iptc.newsml +# text/vnd.iptc.nitf +# text/vnd.latex-z +# text/vnd.motorola.reflex +# text/vnd.ms-mediapackage +# text/vnd.net2phone.commcenter.command +# text/vnd.radisys.msml-basic-layout +# text/vnd.si.uricatalogue +text/vnd.sun.j2me.app-descriptor jad +# text/vnd.trolltech.linguist +# text/vnd.wap.si +# text/vnd.wap.sl +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-asm s asm +text/x-c c cc cxx cpp h hh dic +text/x-fortran f for f77 f90 +text/x-java-source java +text/x-nfo nfo +text/x-opml opml +text/x-pascal p pas +text/x-setext etx +text/x-sfv sfv +text/x-uuencode uu +text/x-vcalendar vcs +text/x-vcard vcf +# text/xml +# text/xml-external-parsed-entity +# video/1d-interleaved-parityfec +video/3gpp 3gp +# video/3gpp-tt +video/3gpp2 3g2 +# video/bmpeg +# video/bt656 +# video/celb +# video/dv +# video/encaprtp +# video/example +video/h261 h261 +video/h263 h263 +# video/h263-1998 +# video/h263-2000 +video/h264 h264 +# video/h264-rcdo +# video/h264-svc +# video/h265 +# video/iso.segment +video/jpeg jpgv +# video/jpeg2000 +video/jpm jpm jpgm +video/mj2 mj2 mjp2 +# video/mp1s +# video/mp2p +# video/mp2t +video/mp4 mp4 mp4v mpg4 +# video/mp4v-es +video/mpeg mpeg mpg mpe m1v m2v +# video/mpeg4-generic +# video/mpv +# video/nv +video/ogg ogv +# video/parityfec +# video/pointer +video/quicktime qt mov +# video/raptorfec +# video/raw +# video/rtp-enc-aescm128 +# video/rtploopback +# video/rtx +# video/smpte292m +# video/ulpfec +# video/vc1 +# video/vnd.cctv +video/vnd.dece.hd uvh uvvh +video/vnd.dece.mobile uvm uvvm +# video/vnd.dece.mp4 +video/vnd.dece.pd uvp uvvp +video/vnd.dece.sd uvs uvvs +video/vnd.dece.video uvv uvvv +# video/vnd.directv.mpeg +# video/vnd.directv.mpeg-tts +# video/vnd.dlna.mpeg-tts +video/vnd.dvb.file dvb +video/vnd.fvt fvt +# video/vnd.hns.video +# video/vnd.iptvforum.1dparityfec-1010 +# video/vnd.iptvforum.1dparityfec-2005 +# video/vnd.iptvforum.2dparityfec-1010 +# video/vnd.iptvforum.2dparityfec-2005 +# video/vnd.iptvforum.ttsavc +# video/vnd.iptvforum.ttsmpeg2 +# video/vnd.motorola.video +# video/vnd.motorola.videop +video/vnd.mpegurl mxu m4u +video/vnd.ms-playready.media.pyv pyv +# video/vnd.nokia.interleaved-multimedia +# video/vnd.nokia.videovoip +# video/vnd.objectvideo +# video/vnd.radgamettools.bink +# video/vnd.radgamettools.smacker +# video/vnd.sealed.mpeg1 +# video/vnd.sealed.mpeg4 +# video/vnd.sealed.swf +# video/vnd.sealedmedia.softseal.mov +video/vnd.uvvu.mp4 uvu uvvu +video/vnd.vivo viv +# video/vp8 +video/webm webm +video/x-f4v f4v +video/x-fli fli +video/x-flv flv +video/x-m4v m4v +video/x-matroska mkv mk3d mks +video/x-mng mng +video/x-ms-asf asf asx +video/x-ms-vob vob +video/x-ms-wm wm +video/x-ms-wmv wmv +video/x-ms-wmx wmx +video/x-ms-wvx wvx +video/x-msvideo avi +video/x-sgi-movie movie +video/x-smv smv +x-conference/x-cooltalk ice \ No newline at end of file diff --git a/net-mime/src/test/java/org/xbib/net/mime/test/MimeMultipartTest.java b/net-mime/src/test/java/org/xbib/net/mime/test/MimeMultipartTest.java new file mode 100644 index 0000000..05388b0 --- /dev/null +++ b/net-mime/src/test/java/org/xbib/net/mime/test/MimeMultipartTest.java @@ -0,0 +1,26 @@ +package org.xbib.net.mime.test; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mime.MimeException; +import org.xbib.net.mime.MimeMultipartParser; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MimeMultipartTest { + + private static final Logger logger = Logger.getLogger(MimeMultipartTest.class.getName()); + + @Test + public void multiPartTest() throws MimeException, IOException { + InputStream inputStream = getClass().getResourceAsStream("/org/xbib/net/mime/test/msg.txt"); + Objects.requireNonNull(inputStream); + MimeMultipartParser parser = new MimeMultipartParser("multipart/mixed; boundary=\"----=_Part_4_910054940.1065629194743\"; charset=\"ISO-8859-1\""); + parser.parse(ByteBuffer.wrap(inputStream.readAllBytes()), + e -> logger.log(Level.INFO, e.getHeaders().toString() + " length = " + e.getLength() + " content = " + e.getCharset().decode(e.getBody()))); + } +} diff --git a/net-mime/src/test/java/org/xbib/net/mime/test/ParsingTest.java b/net-mime/src/test/java/org/xbib/net/mime/test/ParsingTest.java new file mode 100644 index 0000000..44c0eda --- /dev/null +++ b/net-mime/src/test/java/org/xbib/net/mime/test/ParsingTest.java @@ -0,0 +1,254 @@ +package org.xbib.net.mime.test; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mime.MimeException; +import org.xbib.net.mime.MimeMessage; +import org.xbib.net.mime.MimePart; + +import java.io.ByteArrayInputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class ParsingTest { + + @Test + public void testParser() throws Exception { + InputStream in = getClass().getResourceAsStream("msg.txt"); + if (in == null) { + fail("no msg.txt"); + } + String boundary = "----=_Part_4_910054940.1065629194743"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + List parts = mm.getAttachments(); + assertEquals(2, parts.size()); + assertEquals("139912840220.1065629194743.IBM.WEBSERVICES@ibm-7pr28r4m35k", parts.get(0).getContentId()); + assertEquals("1351327060508.1065629194423.IBM.WEBSERVICES@ibm-7pr28r4m35k", parts.get(1).getContentId()); + { + byte[] buf = new byte[8192]; + InputStream part0 = parts.get(0).read(); + int len = part0.read(buf, 0, buf.length); + String str = new String(buf, 0, len); + assertTrue(str.startsWith("")); + part0.close(); + } + { + InputStream part1 = parts.get(1).read(); + assertEquals((byte) part1.read(), (byte) 0xff); + assertEquals((byte) part1.read(), (byte) 0xd8); + part1.close(); + } + } + + @Test + public void testMsg2() throws Exception { + InputStream in = getClass().getResourceAsStream("msg2.txt"); + String boundary = "----=_Part_1_807283631.1066069460327"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + List parts = mm.getAttachments(); + assertEquals(2, parts.size()); + assertEquals("1071294019496.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k", parts.get(0).getContentId()); + assertEquals("871169419176.1066069460266.IBM.WEBSERVICES@ibm-7pr28r4m35k", parts.get(1).getContentId()); + } + + @Test + public void testMessage1() throws Exception { + InputStream in = getClass().getResourceAsStream("message1.txt"); + String boundary = "----=_Part_7_10584188.1123489648993"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + List parts = mm.getAttachments(); + assertEquals(2, parts.size()); + assertEquals("soapPart", parts.get(0).getContentId()); + assertEquals("attachmentPart", parts.get(1).getContentId()); + { + byte[] buf = new byte[18]; + InputStream part0 = parts.get(0).read(); + int len = part0.read(buf, 0, buf.length); + String str = new String(buf, 0, len); + assertTrue(str.startsWith("")); + part0.close(); + } + { + byte[] buf = new byte[8192]; + InputStream part1 = parts.get(1).read(); + int len = part1.read(buf, 0, buf.length); + String str = new String(buf, 0, len); + assertTrue(str.startsWith("\n")); + part1.close(); + } + } + + @Test + public void testReadEOF() throws Exception { + InputStream in = getClass().getResourceAsStream("message1.txt"); + String boundary = "----=_Part_7_10584188.1123489648993"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + List parts = mm.getAttachments(); + for(MimePart part : parts) { + testInputStream(part.read()); + testInputStream(part.readOnce()); + } + } + + @SuppressWarnings("empty-statement") + private void testInputStream(InputStream is) throws IOException { + while (is.read() != -1); + assertEquals(-1, is.read()); + is.close(); + try { + int i = is.read(); + fail("read() after close() should throw IOException"); + } catch (IOException ioe) { + // expected exception + } + } + + @Test + public void testEmptyPart() throws Exception { + InputStream in = getClass().getResourceAsStream("/org/xbib/net/mime/test/emptypart.txt"); + String boundary = "----=_Part_7_10584188.1123489648993"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + List parts = mm.getAttachments(); + assertEquals(2, parts.size()); + assertEquals("soapPart", parts.get(0).getContentId()); + assertEquals("attachmentPart", parts.get(1).getContentId()); + { + try (InputStream is = parts.get(0).read()) { + while (is.read() != -1) { + fail("There should be any bytes since this is empty part"); + } + } + } + { + byte[] buf = new byte[8192]; + InputStream part1 = parts.get(1).read(); + int len = part1.read(buf, 0, buf.length); + String str = new String(buf, 0, len); + assertTrue(str.startsWith("\n")); + part1.close(); + } + } + + @Test + public void testNoHeaders() throws Exception { + InputStream in = getClass().getResourceAsStream("noheaders.txt"); + String boundary = "----=_Part_7_10584188.1123489648993"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + List parts = mm.getAttachments(); + assertEquals(2, parts.size()); + assertEquals("0", parts.get(0).getContentId()); + assertEquals("1", parts.get(1).getContentId()); + } + + @Test + public void testOneByte() throws Exception { + InputStream in = getClass().getResourceAsStream("onebyte.txt"); + String boundary = "boundary"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + List parts = mm.getAttachments(); + assertEquals(2, parts.size()); + assertEquals("0", parts.get(0).getContentId()); + assertEquals("1", parts.get(1).getContentId()); + } + + @Test + public void testBoundaryWhiteSpace() throws Exception { + InputStream in = getClass().getResourceAsStream("boundary-lwsp.txt"); + String boundary = "boundary"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + List parts = mm.getAttachments(); + assertEquals(2, parts.size()); + assertEquals("part1", parts.get(0).getContentId()); + assertEquals("part2", parts.get(1).getContentId()); + } + + @Test + public void testBoundaryInBody() throws Exception { + InputStream in = getClass().getResourceAsStream("boundary-in-body.txt"); + String boundary = "boundary"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + List parts = mm.getAttachments(); + assertEquals(2, parts.size()); + assertEquals("part1", parts.get(0).getContentId()); + assertEquals("part2", parts.get(1).getContentId()); + } + + @Test + public void testNoClosingBoundary() throws Exception { + boolean gotException = false; + try { + String fileName = "msg-no-closing-boundary.txt"; + InputStream in = getClass().getResourceAsStream(fileName); + assertNotNull(in,"Failed to load test data from " + fileName); + String boundary = "----=_Part_4_910054940.1065629194743"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + } catch (MimeException | NoSuchElementException e) { + gotException = true; + String msg = e.getMessage(); + assertNotNull(msg); + assertTrue(msg.contains("no closing MIME boundary")); + } + assertTrue(gotException); + } + + @Test + public void testInvalidClosingBoundary() throws Exception { + boolean gotException = false; + try { + String fileName = "msg-invalid-closing-boundary.txt"; + InputStream in = getClass().getResourceAsStream(fileName); + assertNotNull(in, "Failed to load test data from " + fileName); + String boundary = "----=_Part_4_910054940.1065629194743"; + MimeMessage mm = new MimeMessage(in, boundary); + mm.parseAll(); + } catch (MimeException | NoSuchElementException e) { + gotException = true; + String msg = e.getMessage(); + assertNotNull(msg); + assertTrue(msg.contains("no closing MIME boundary")); + } + assertTrue(gotException); + } + + @Test + public void testInvalidMimeMessage() throws Exception { + String invalidMessage = "--boundary\nContent-Id: part1\n\n1"; + String boundary = "boundary"; + MimeMessage message = new MimeMessage(new ByteArrayInputStream(invalidMessage.getBytes()), boundary); + try { + message.getAttachments(); + fail("Given message is un-parseable. An exception should have been raised"); + } catch (MimeException | NoSuchElementException e) { + MimePart part = message.getPart(0); + assertFalse(part.isClosed(), "Part should not be closed at this point"); + message.close(); + assertTrue(part.isClosed(), "Part should be closed by now"); + } + } +} diff --git a/net-mime/src/test/java/org/xbib/net/mime/test/QuotedPrintableTest.java b/net-mime/src/test/java/org/xbib/net/mime/test/QuotedPrintableTest.java new file mode 100644 index 0000000..aac5b3c --- /dev/null +++ b/net-mime/src/test/java/org/xbib/net/mime/test/QuotedPrintableTest.java @@ -0,0 +1,45 @@ +package org.xbib.net.mime.test; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mime.stream.QuotedPrintableInputStream; +import org.xbib.net.mime.stream.QuotedPrintableOutputStream; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QuotedPrintableTest { + @Test + public void testUTF8RoundTrip() throws Exception { + char[] RUSSIAN_STUFF_UNICODE = { + 0x412, 0x441, 0x435, 0x43C, 0x5F, 0x43F, 0x440, 0x438, 0x432, 0x435, 0x442 + }; + String stringInput = new String(RUSSIAN_STUFF_UNICODE); + byte[] byteInput = stringInput.getBytes(StandardCharsets.UTF_8); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + QuotedPrintableOutputStream outputStream = new QuotedPrintableOutputStream(byteArrayOutputStream); + outputStream.write(byteInput); + String encoded = byteArrayOutputStream.toString(StandardCharsets.UTF_8); + assertEquals("=D0=92=D1=81=D0=B5=D0=BC_=D0=BF=D1=80=D0=B8=D0=B2=D0=B5=D1=82", encoded); + QuotedPrintableInputStream inputStream = new QuotedPrintableInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); + byte[] decodedInput = inputStream.readAllBytes(); + assertArrayEquals(byteInput, decodedInput); + + char[] SWISS_GERMAN_STUFF_UNICODE = { + 0x47, 0x72, 0xFC, 0x65, 0x7A, 0x69, 0x5F, 0x7A, 0xE4, 0x6D, 0xE4 + }; + stringInput = new String(SWISS_GERMAN_STUFF_UNICODE); + byteInput = stringInput.getBytes(StandardCharsets.UTF_8); + byteArrayOutputStream = new ByteArrayOutputStream(); + outputStream = new QuotedPrintableOutputStream(byteArrayOutputStream); + outputStream.write(byteInput); + encoded = byteArrayOutputStream.toString(StandardCharsets.UTF_8); + assertEquals("Gr=C3=BCezi_z=C3=A4m=C3=A4", encoded); + inputStream = new QuotedPrintableInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); + decodedInput = inputStream.readAllBytes(); + assertArrayEquals(byteInput, decodedInput); + } +} diff --git a/net-mime/src/test/resources/logging.properties b/net-mime/src/test/resources/logging.properties new file mode 100644 index 0000000..4416a73 --- /dev/null +++ b/net-mime/src/test/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter +jdk.event.security.level=INFO diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/boundary-in-body.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/boundary-in-body.txt new file mode 100644 index 0000000..3ac967a --- /dev/null +++ b/net-mime/src/test/resources/org/xbib/net/mime/test/boundary-in-body.txt @@ -0,0 +1,10 @@ +--boundary +Content-Id: part1 + +1 --boundary in body +--boundary +Content-Id: part2 + +2 --boundary in body +--boundary starts on a new line +--boundary-- diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/boundary-lwsp.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/boundary-lwsp.txt new file mode 100644 index 0000000..2863d9b --- /dev/null +++ b/net-mime/src/test/resources/org/xbib/net/mime/test/boundary-lwsp.txt @@ -0,0 +1,9 @@ +--boundary +Content-Id: part1 + +1 +--boundary +Content-Id: part2 + +2 +--boundary-- diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/emptypart.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/emptypart.txt new file mode 100644 index 0000000..c517c9a --- /dev/null +++ b/net-mime/src/test/resources/org/xbib/net/mime/test/emptypart.txt @@ -0,0 +1,13 @@ +------=_Part_7_10584188.1123489648993 +Content-Type: text/xml; charset=utf-8 +Content-Id: soapPart + +------=_Part_7_10584188.1123489648993 +Content-Type: text/xml +Content-ID: attachmentPart + + + + + +------=_Part_7_10584188.1123489648993-- diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/message1.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/message1.txt new file mode 100644 index 0000000..047ec4a --- /dev/null +++ b/net-mime/src/test/resources/org/xbib/net/mime/test/message1.txt @@ -0,0 +1,14 @@ +------=_Part_7_10584188.1123489648993 +Content-Type: text/xml; charset=utf-8 +Content-Id: soapPart + +SUNW +------=_Part_7_10584188.1123489648993 +Content-Type: text/xml +Content-ID: attachmentPart + + + + + +------=_Part_7_10584188.1123489648993-- \ No newline at end of file diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/msg-invalid-closing-boundary.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/msg-invalid-closing-boundary.txt new file mode 100644 index 0000000..e75b2d7 Binary files /dev/null and b/net-mime/src/test/resources/org/xbib/net/mime/test/msg-invalid-closing-boundary.txt differ diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/msg-no-closing-boundary.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/msg-no-closing-boundary.txt new file mode 100644 index 0000000..76a163f Binary files /dev/null and b/net-mime/src/test/resources/org/xbib/net/mime/test/msg-no-closing-boundary.txt differ diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/msg.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/msg.txt new file mode 100644 index 0000000..3c3dff8 Binary files /dev/null and b/net-mime/src/test/resources/org/xbib/net/mime/test/msg.txt differ diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/msg2.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/msg2.txt new file mode 100644 index 0000000..81f3d79 --- /dev/null +++ b/net-mime/src/test/resources/org/xbib/net/mime/test/msg2.txt @@ -0,0 +1,53 @@ +------=_Part_1_807283631.1066069460327 +Content-Type: text/xml; charset=UTF-8 +Content-Transfer-Encoding: binary +Content-Id: <1071294019496.1066069460327.IBM.WEBSERVICES@ibm-7pr28r4m35k> + + + + + + TV, Brand1 + 24in, Color, Advanced Velocity Scan Modulation, stereo + 605001 + TV + Brand1 + 299.95 + + + + TV, Brand2 + 32in, Super Slim Flat Panel Plasma + 605002 + TV + Brand2 + 1499.99 + + + + TV, Brand3 + 50in, Plasma Display + 605003 + TV + Brand3 + 5725.98 + + + + Video, Brand1 + S-VHS + 605004 + Video +Brand1199.95Video, Brand2HiFi, S-VHS605005VideoBrand2400.00Video, Brand3s-vhs, mindv605006VideoBrand3949.99DVD, Brand1DVD-Player W/Built-In Dolby Digital Decoder605007DVDBrand1100.00DVD, Brand2Plays DVD-Video discs, CDs, stereo and multi-channel SACDs, and audio CD-Rs & CD-RWs, 27MHz/10-bit video DAC, 605008DVDBrand2200.00DVD, Brand3DVD Player with SmoothSlow forward/reverse; Digital Video Enhancer; DVD/CD Text; Custom Parental Control (20-disc); Digital Cinema Sound modes605009DVDBrand3250.00TV, Brand4Designated invalid product code that is allowed to appear in the catalog, but is unable to be ordered605010TVBrand4149.99 +------=_Part_1_807283631.1066069460327 +Content-Type: image/jpeg +Content-Transfer-Encoding: binary +Content-Id: <871169419176.1066069460266.IBM.WEBSERVICES@ibm-7pr28r4m35k> + + JFIF    C   +  +  +------=_Part_1_807283631.1066069460327-- \ No newline at end of file diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/noheaders.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/noheaders.txt new file mode 100644 index 0000000..b724337 --- /dev/null +++ b/net-mime/src/test/resources/org/xbib/net/mime/test/noheaders.txt @@ -0,0 +1,10 @@ +------=_Part_7_10584188.1123489648993 + +SUNW +------=_Part_7_10584188.1123489648993 + + + + + +------=_Part_7_10584188.1123489648993-- diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/onebyte.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/onebyte.txt new file mode 100644 index 0000000..7ba986d --- /dev/null +++ b/net-mime/src/test/resources/org/xbib/net/mime/test/onebyte.txt @@ -0,0 +1,7 @@ +--boundary + +1 +--boundary + +2 +--boundary-- diff --git a/net-mime/src/test/resources/org/xbib/net/mime/test/quoted.txt b/net-mime/src/test/resources/org/xbib/net/mime/test/quoted.txt new file mode 100644 index 0000000..ccae2d8 --- /dev/null +++ b/net-mime/src/test/resources/org/xbib/net/mime/test/quoted.txt @@ -0,0 +1,770 @@ +------=_Part_16_799571960.1350659465464 +Content-Type: text/xml; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Content-ID: + + + + + + + cid:531309647604 + + + +------=_Part_16_799571960.1350659465464 +Content-Type: text/xml; charset=Cp1252; name=UnXMLCopyPaste.xml +Content-Transfer-Encoding: quoted-printable +Content-ID: <531309647604> +Content-Disposition: attachment; name="UnXMLCopyPaste.xml"; filename="UnXMLCopyPaste.xml" + + + + +/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAASwAA/+4ADkFkb2JlAGTAAAAAAf/= +b +AIQAAwICAgICAwICAwUDAwMFBQQDAwQFBgUFBQUFBggGBwcHBwYICAkKCgoJCAwMDAwMDA4ODg4= +O +EBAQEBAQEBAQEAEDBAQGBgYMCAgMEg4MDhIUEBAQEBQREBAQEBARERAQEBAQEBEQEBAQEBAQEBA= +Q +EBAQEBAQEBAQEBAQEBAQEBAQ/8AAEQgAggElAwERAAIRAQMRAf/EAIQAAAEEAwEBAAAAAAAAAAA= +A +AAAFBgcIAgMEAQkBAQAAAAAAAAAAAAAAAAAAAAAQAAEDAwMCAwUGAwYFAQkAAAECAwQAEQUhEgY= +x +B0FRE2FxIhQIgZGhMkIjUmIVscFygjMW8NGiskOSg5MkNFQlGAkZEQEAAAAAAAAAAAAAAAAAAAA= +A +/9oADAMBAAIRAxEAPwD6p0BQFAUBQeEgdaDwrAoMS6mg89YUHnrUB61AeuPOgPV9tAeqP4qADn8= +1 +BmF+dB4XLUB6ooD1R4UB6tAesjxNB76qPOgxU+hNBqVNbT40GIyDP8QoNqJba+hoNnqo86DBUlC= +e +poNfzzV7bhQZolNq6G9BtStKuhoMqAoCgKAoCgKAoCgxKgKDWt4DqbUHI/kWWepFAmvchYZTZaw= +S +OqjYX+yg4neWRB/5B99BoPMIw/X+NBrc5pGSCSsH7aDnXzuOkXCqDSe4Ef8Aj/soAc+ZV0XQZf7= +4 +a6+p+NBiOfR0myl/eaDejuBDA/1AD76DVJ7jw2xf1R99BwnufD3W9T8aDc13IhudHR94oNyu4UN= +K +bl0ffQcTvc+AhVi8PvoOmH3FhydG3NxsSbeQFyfcB1oGly76mO1/DuKt805HyBmFhn3Vx4c5e4J= +l +ONi6hHFrugfxJuPbQR/i/rn+nbkZ243nuPaXe3pTFriKv/7dKB+NA+sb3dxGZaTJw2RYnsrG5Ls= +V +5DySD43bJoF7H9xmFkBTgv5XoN2T718LwiQjL5uNGcPRpTyS4f8AIm6vwoGplPqd7YRTeRyFmOk= +6 +JdfQ802f860BP40HRjO8/H84n1sJlI2Qb67or7bw/wCgmgcOO7hsukXcH30DrxvL4z9h6g19tA4= +4 +uVYfAsoUHal1tXQ0GdAUBQFAUBQa3HQgf30CZNyjUdJKlC4oGvkuXtN7rL6UDMzHOy3uAXb3UDO= +y +HO1qUbOW+2gRpHOHNSXfxoEyR3Bcb0Dp++g5D3CfWbBw/fQYO83kFN95++g5Fc5fv/qH76D1HOn= +g +dV/jQdKeeqKdXPxoNTvOFk3Dn40GlPOHnVem07vWeiUm5P2DWgwkcjyZdLCwpt3bvLTlm17P4rL= +I +NvbQI45kp3epiQlwIUUrUhYWAodRdJOtBxyO5DsI2LpJ8r0CTK7zSRdtCju8vGgRZfdbJOD1fUK= +R +fW5oHFgu4OTynFM9DZcUXMk0ziQsGxQ3NdCHSPbs0+2gqd9afOhn+5OLw8q4xvF8NDjsREnagOS= +R +6xSgdEgIUhOngmgq7MlY+S4XU45htKv0pW4CB7bGgd3ajtr3I59kU5DhipPG8Ew8Gp3KTKXGgxb= +f +EolxS296gP0JUTQXf4XyKPj+Nq43xzuM9nXmClDmWz84y1qcAN9rbSFtto8kq3kfxE0DR5Zxbnn= +I +pxSnneHm+obqYbygii/S5C22kmgaGS7P8+ZeUycjjXVLGoGSbdQR/iQVCgSW+13dzjEn57A5ONG= +k +t/GlcTJBJP2qCU/jQPPh/wBXPdbto83A7r453IY9K/TORSiz6QOpDiPgct770FquNfUBiM1h4nI= +M +FPTJgykhbToP4EeBFBKnFO+ESShCC+CdPGglHA9yIszaPU6+2gf+LzLMxAKVA3oFUEEXFB7QFB4= +T +bU0GtbthpQIeYyyYyCTpagjXkfKjddl2T76CNs/zJphK1OPBKRe5v0oIK519UHa7izjjWVz7a3U= +E +hUeIFSXAfIhoED7SKCJJ31wcBS8swMXlJ7fg4Gmmx79XCaBNR9Z/DMg7scgT4g/mQ05b7EOXoF3= +G +/UFwnOqCIuVQ04vRLUkKYUf/AHgAP30DjY5zHUQQ8kg6g3FiPZQKjXNYy2yPVB086Dn/ANwqcZc= +m +gkR2lpbcc8lKBVYDxNhc+QoEaX3CxyG1pjPPSZCTtEeNEddN9Oq1bEg9dNfCgSJHcLLmX6SMdK9= +J +CCpa3JLDI37bgEJF9t9OutAlsc1nz3h8xCiIS0kF5ErISlkqTckKKnyNbdAn2UHNzXkDOa438xL= +x +jGIWw6lceViJJYkqWWwDdxoJUE2A8dTYDUk0EPZHuDi5vJPRfjqdafdCWpCpDqyp223couLUojc= +k +pBJNBYXh/MoUzjMZqC0hhcdCW3mWUhCQoJGtk2GvjQEzK+q9vPw6eJoIzy3NZEfLuei9tSFWuLU= +G +TnN1vgMIWF3F1K9tBJ3a7kDa8CXHF7duUhFYB6pSpBt+NBVj6qnnT3Q5Atwm+2CADrZPyjdh9go= +I +o4/x6byJnIKilLUfFxHJ+SlLNg0wlSW7gdSStaUpA6k0E6dt0Tu5nbrB4ZUtOMwfHHGcCw1chUp= ++ +U+t9TqWxoFEOKK1fy0H0d7IdruAce4i3hOMphSJUaN6kiM24w4/r+ZS03vck60DiXgOHuYhaMtw= +a +VyYL3oEeBjnJSrIOpJQUpSfZQV+lcU7A8h5rN49CwruDyzSVOt8Wy7ErDT1JB+IsoWGvUA807qB= +v +8n7M8GfzUePBcyONiSmtiX4eUltvNrJ/MCtbiTY6FJTQQN3f7X5/tq66cblZOUaeQpS0S2hHnJR= +a +6XELa3wpqLddA4PG1A1+Ac25HG489EQlsQGHENh9hJbQFuqKviR+lV1ageGo0oJD4H3WnQsq0xK= +k +FSbjcAqgtfwLuO3MQ2WnvK2utBYzt9zgvbGnF+VBNuLnplMpUDfSgUaAoNa1fFtoOaU56baleQo= +I +T7oc9awra2/UAcN7a0Fau5HfWDgcM/kZ8n022htBGq3HD0QgeKjQUl513q7id3sw5xfCylR4N9z= +7 +SFkNNIJsC8tNis+Sens8aDowXaSBiUplTUNzHwATLmkEFXX9tB+Efd9tA84z5itCOXEtoGiWwkJ= +0 ++wWoEDkeKxuaaKJkFiQnwUtCd3vChZQ++givkfAZmNSuXhiX2E3U5DUdy0jx2H9Q9nWgReH8k5a= +c +21i+NLLiAQqQ06SY7bd9VK/h9ltaCYI2by4ykbHwiuRKluIZjxmrqU44shKUge0mgkjKKXCCMLC= +z +cZl2GhbWUSpxa1Kf3gujT4AkK+FO3U7epoEJmLBeiNfNZRMp1hJcQWy64HFA7vhAFtVDqfCgBCw= +Z +lNvqYkuNoRudcQlSl+oUj1CnUDda9vBKftoFZmLiVtlULGqUVnch55CEekkWQhv4yr/j3UEX93u= +X +xcc29ioUgONtFLJdbILanljcopCQASncbeyghbD8sxMuTFafacCm2vTX8IsS2l9RN7/zg0E2cA5= +7 +KgstvhKXPWdEacgm1nkJtuT5btDb20CtmObTpswBtwNharbR4DxoG9yDJYmYdoY9Ek7ELR8Slq8= +S +qgbysqjDSSA6Ol1X6edqCSuzXM2JmEyDSnLOJyETYnzBW2bfcDQRn9VqVP8AcjKyEgBMiHhpLew= +3 +GxyAwev20EX8c5Jj8Hx/lWInJVuzkFqOw6kX2LZktvi/sISQaCX+yh4bx7jmS4f3QmOsJD0DMSI= +e +PU6mWxEcCXFIWpISkKdQpAO1V0pOttaC9f09fUn9PuJyDXDuF4EYRieyteKQthiLHkpaBKrv3Kl= +K +UEqsVE7tqtb0HH3D+vnjHazmMzC4TELyzGO9P1HIr6Cwlcgbw2CQq5soflv5+NAl8x+s7t73Yah= +Y +Luf2lmNpaWlMOa5KaEqI9uTtdjq2NrQsFQspKh5XoI75v9RfanHswZWJyypKA46WG3gr5qwUBsU= +3 +YqUu6SDYEXoG5le4PMO7TMlntzwWdAbaCTKlTnUNRVJdG1tYhKQ46XCr8vppFz4Ggg3kvbzmHbk= +t +yOWMekZa1seqw4h1r1iPUKFKZW4lKwAfhUd1r+RoOPFz24ikuMD9zzPX30EuduOeZWJk2/XeOzQ= +B +PuoLp9reWiY0w8leul6C2PBMt81Fbuq+goH6g7kg0GVBqWn4t1Ajclnox2KkylmwQkmgox3V5VK= +y +ORlzFuXbSVWv0AHjQfP7vb3Em53JrERSloUssYyOD13Hbvt/Es/hQSBwPtvD4TiY7WRdAeCPmMm= +8 +LErfIuoD2D8o91Ahcv5q1ElfMLdLhSbNJvbakdNPCgaz/cNt0FxK1Ff5iCdLnpQOXDv5PLQPmXZ= +T +UezLcr0nTZZjuLLYWL2FrjxNByymJ09tyPDntNlYKA76jaVa3F03WdaBDxWDn4CI7GxDTSGgoqe= +k +OutoU6rX4ipSvi06UC/wSNMe5ezIlSo4dhtuSIzge3NtuoSdpWpu5FjqBpe1BKSclNs85/U8WyL= +q +UwHPTdVtbG1KtW03IJv5UHj2aZ9P/wC65mGwsoSz6kVpClJQrroRYEgdfL76BIcyGAnIcS1yFDe= +4 +qQgem1cpGqhdSehASD91Alzsri2cS/kpWQ+bQy4ktQ7Da8VXCTZI6a6XoK88g5UzkM+XMqw0+wy= +p +za2t1xtCnFKstQLfgNoSB7KDnad4mHEvRoEVlRSofBJlfquki6vGxoFzA5xhOZLJKEQckAl5KFq= +W +lL6blKviAIO0eFA6OR5MY2Cme+bKWmyli5IUk2KwfHd199A2sHyGKXlvPTEqQLlKVK1ufK9B5mR= +E +lsLltyApSr/DQdHb3NPYXJysVvKHEvxpQTe11Mg3B9xIoFTu4lXIFY/NpuW3YTWOkL6+m7FTsQF= +e +V0WIoISnNLQstOjaoBSFA+drUFjuz3EsL3A5ny7lc1CiJMHHPY5KBuSBMShMgi42qU0UbCk+dBJ= +5 +7I8Z5/3G4l2wgT1NyORzExlvMNlsQYMdsvvu7bg3ARoLgbiKCOshxRzEdx5nD5z7hahPksyydqk= +u +MqS0pZOv5kBB19tBOs/AcPhcRyDmXnR3lKYL65q3UXAas6CkXuLFINqBu8W7VYDiPbDH51vHIHK= +M +yhOVzGTfQFvMrnvKfQ0nd+X02VpFv4ib60CzxL+oYybJcxIJVJhSETZa1pStqOkoL0hK1ahaGyo= +g +jXXSgbf1RvYvj/AsDx9qYZc3NyY+VSp0JQpEPHRnIrZCUWASsu/CbC9jQVhx7slzIFwLt7KCTeI= +u +usPBStSq1iaC1XZrkDjXpNLV4igvN2onGREaN76CgmWObtig20GDuib+VBGnefKKg8UkhJsVpIo= +P +nT365bIxfG5UaMra5NPy5cB1Ac0Vb/LegpZByERzuhh5GRUBEhSA+sKF0/sJK0i3+ICgcnO+5Ux= +2 +Y4jHPlSFHQAK8aBmSMVzXNMfPNwXHW/zLUrTTzF6DpxPBM5MlQ43qNhUxbbaUBYJus2JJ8LCgd/= +G +szjcj3By2Jc9UrlsOwsa2lKDG+VipKQVXO4HcjSwtQdfzbMBpXzLEbdqUBTJGvlcCg50ZWRIf/Z= +Y +adU8UhiI2klS3HPhShKbaknSgmCJxrG8aiN8ZyeDdlTWgl7IP49RbbdkOIJWPUAspLY+HTT4fMm= +g +5OQZLt5xhr18ngMgrSzAkvrCHFW1CAq2gta9tB76BmIlc47gN/O8dxiMDx9Tiktris+o4sDRW1x= +3 +VVrdRYe+g9HajIvpJXIybTpKUiSvY82QdFKKElKttwba0EcZuRncf6uNkyFrbUVpbfBKkObCU7k= +F +V9QfuoGU9xzJZSQ03i2PmBHbO8BSEkDdfooi/WgkLi/FGGO3fIMzyLDtzncA3HksxJLjiW98qaz= +F +JUYrravyrNtbUCQOfcUgo/pkrtri21NKTIbdbn5rYVDorSYSPdQd8/vZx6fjhipfBMYplP5CZeY= +I +Sen/ANSCdPOgSmefcH9MBvtph31DUuqVm1X+6YBQKmG5fxPkEyFgmeGYbHKmSGYxebTl1uJDzgb= +N +g7NWm43aXFB6126y/KM3PyuIz+Exq4rwadOXyrGPdUtKEhRS25qUm/UUCye3nKWFb08z4i8kpKZ= +M +V7O+ol1A/SdrVrjwUDegb83tBJyqlKc5NxuOP0bco66oeQ+GOb0Ej/Rx2vjI+qPinbfnLLGcwPJ= +o +uRLzMeTI9AtssOrS+0tlTKkuJcY2i/gT50F54fbPgHaHu/D5jxHiEqDCjRp8R+Wp96bLbXIjuNt= +r +Wl911aWuhFj4fF4UEXTOxmQ7k82j8khcTlrgupeelZLIsuRWpQSremxYdCwhy523spPW1BKuO7S= +/ +Tw/BBg9tQ3mYZbDglrkzWo7o1utMl1Seo0uCDQR/3xQiBGzrgSG0stMLAToCpbibaDzoGPxGVwG= +H +lI8fuRlYuHwr8OQqXJn3DC9imHS0emq0ghI8TQVF7q9zV9zu4/IecTnSqHKkKZw7NikNQGD6cZt= +K +fABsA28yaDUrBxuLY5vPcpWphT6QuHi0mzpSdQpw9U38E9fO1An4/nuRdmBcF4x0E/CgpCx9tBJ= +X +Hu6PL8S427DkKiykfE0838TTgH8SFXB9oIoPoR9D31MxO6bj/D8wyiHyTFtpdkR0H9uQxcJ9doH= +W +wUQFp/SSPA0F742rKT5ig20GDn5bGghL6hXXFcYkNN6/AoWHuNB8ve/eSRBwEBtbu5xco/BfoPS= +V +bTr1oKy4dt6ZzeDHaZL7k14Rm2goIKlPnYLKVoDrQS1kuz0zGKw7yeO5Zybkp7UARJT8Zpjcv1C= +A +h8k33BFwrbtt7aBS5pxblXGIsFnJYSXGZkyY8QORchDfas4TdG4qbsogaaWoG+1BTgJrS8REW1I= +k +IkJbk5SbAUhopaVqEtvqN9f+WtB1ROStYbjkLBxuOtJmsshtjOFRU8VuK3Fe70ArapXhu6UEdKi= +8 +1kSXnzkFrKyFbSg7bm5Fvh0oHd209TH83iyOd5X5aClmU6lYbBUl0NfBsuBbX/jWgl9zu/xZLyo= +k +XmTzyRZXrEtqFz8FtoTrdWv2UEJd0uaz+UZcOTprrzG75eK4+bqbj7uthoCfzGgk5/n+AxcBnBw= +M +8/GxkVltmM8GQUJa2k7glF7Cw8dfOgTJ3P4sOIUYjk68m7kNrXyygQG2AbKUVHoolVkjyudNKCK= +O +RPTszyJ1ERtRiQU+g2kLSG/U6u7QSAQDYC3lQNyYosLI8D4XI/stQSNxmAmD267hOsJQW52Kxyw= +t +t4OlITl4agFi90nrQRN6M3J8mexcVCnXXXWmGGk9VLWbJSPeTQOl7GR42fj8I4biFcm5E4Qh5bL= +a +n97x6pYRY2Qk6BVtyvzaAgUHRyXhPOuHqSOa4FMdsqKHmok6K/IYVa+1bbLqig+xW2gVeG4KNAz= +/ +AB7IEodhT5cRUWaBtKVh9N23B4KBBHvFqDbhuG/7nHJpceX8vMx09SXPWQgsFpQGu4pUoKFj4Ee= +6 +gSuR8T5pxWV6E5va1tQtE6OliRGWlYJSQ60hSdbdDr7KBW7e8M5zzgvSGnGWMW1uQ9l8kpMeI24= +k +ghKVIQVrUP4W0qPnagkHhfLeO9iPqW7V8rXyRrJRcE4WM5kUxXY8ZqPNW6lah6hUpSAHjc2Gg6U= +H +0E+oOBy/l89eW7UZl9lmQ1FemQce2y446p1KilaVrPxNgAEpSU7wq+7S1BxdmOHd928FJyGS55k= +o +0GMp0O47LfJvlxSUglKGkN/Ak+FlH3igesbkeNjYHKsvupbdcWwZAv0DG9RJ+/Sgqb3r5OeRZ0c= +d +jfB82+3NyJP/AIYscbmkK/mVbeR4D30EI90sBL5r2+5Fk49x8iuOMY2fyqHrBDh+26QPdQQA/wA= +S +5dxhtnJzcWtcaIpLqJrQEiMVDUErb3Jt76DgyGdy/PpTuRybnqLbUEXHTpegXOOcXAWlSkFVA+G= +Y +8mQlGLwcReSn3/ajxk7ykkW+JQ+FP2mgtj9C30n9x8D3YwvenlPIGMKnGl23HoqDKfmMyGlNLaf= +d +ultCTuB+HebgUH1XiqCmEEeVBuoNb35fsNBD3eSKZeKeRa+h0oPln9RXF40XFZIxsM6mVDkpfey= +a +kpJDIUUqClFRXYhYPS1BV5qW1is7AzL0ZExuFIZkuxHUhbbyGlham1JOhCgLUFr8twtHOEYfN8S= +4 +9ExjCJEfJRi1Ok7XY5QpWwojbW033A6WOlqDZO7VoyUtiTmRGadZcQ6hEeLuO5FyAVSlOqKbnUB= +O +vjQb09s8NAU3IaiIlFu5bccS2kJKhY2Q2kJsfbQJGZ4uIGPfd/qSscwkKbU4pSkR20Wtb4iB08L= +X +oIvU3x5xwsMcrO+9gERnDe3tKKDgzeE4whv1s1n1yEpSpLaRHdT16dU+YoG/A4lgpxXksY46WGy= +6 +4yogpStLeosFWNtDegQOSqS5LQlf5NAfdfWgW1cDbhpcTFkqQ2CTsSNoP7W8nr42oNbfCXEvJLc= +p +xB3bfh/xhP8AfQdMPjUJDThdU44tq4VuWoblfvnwI/gFA2OQs+hKU1axSSPuoHfwSOsdv+fuJ09= +X +GwwdTrsyMZen3UDM4k+GedZGWkfustuLjHyeLJQlXvG6491BJParJ/7J+n/uR3Jwj5Y5bNltYeL= +M +bNn4sR5VntiuqSoHbceFBD/G0rDfzjq1LkPqV6qyokm/gaCcMBCZRjCwUbEMPYWc2CLbXpQJUQf= +b +8uk/bQMJ3mLvEeQcmgAKX/VpjyNoNglSVkA++6qBXxveeXAJxMoLXFZjqjzwFfDIj327CjoSNCC= +f +bQa4vcpUfiqOO41ClxcXNmFkN/6hYllLyN9tTY7hQNyNh+b95uUxuNcKwr2VyZbDTWOhILzm0KJ= +3 +ukfA2kbtVLUkCgvRwTl3cP6cu3GJ4LzHOx+Q5jFsmPIRGQpRwkU3UzF+cb3GQloE7rp+C9knaKB= +4 +Y/vlzZ7DofagqV6iErTkX5DTsdYVpuQttRBHvoGfP7iZgJfXJe9dbi1OllsktKcPQrXYFaR5WAv= +4 +UDNhYfK5dMmXNUovT3f33BqsoJuU38Co0HfncEn/AGwePM/sPZEoDRVcNtQ4Kky5b7h1+FCGwke= +a +1AeNBBvG1ZHFNtiDKXHStbscKBuEhtXwpWk6EFBFAuS8Lx+YpcblmKEN5dlHMYpDbbnxD4VqQUl= +K +hboaDXgeyDsdxyTBy73J4D59RO39tbaR4OMo+I280kj2Cgk7g8SFx+Q002whlIIIQlISPwoLk9j= +8 +68tbPp/l0oLcYSQXoaCfEUClQYOfloI87g48yIjgtcEGgo93z4iymRJ+Za9SHkmnIktG2/5klJ0= +8 +yk3HtFB86OY8bl8Xz0zj88ErjKIacIsHWVatuD2KTrQWP+kju/8AM49rsxmEMfMoU45x6Y+SFut= +q ++JUUE3G5JupAtqm48KCwOe4w8d5R6ON3pPrKBV8ZHs3qWdPO1A20ccmNEJjqMi/5ULGxLgtqQnw= +t +52tQJHIeBx8qW5XIOOpyHp/AwX0fMC/kEA2v/NagbeT4LxyDjp62cDDhvIjvltKYbTbqFBtVibJ= +u +PeaCn/OJc1CUsuuX2p/uoJzg9oNkKK47mXI7BZQUs7Wzb1ED2XtrQQhyrHSYGSk4+Yna9FcUy4P= +8 +JtcewjUUEgcRwXIOT8fazMLJtKSoqZkMuISVNuIR6ZBJIOqSCKBWd4ry6AUOuBlQJKgS2oG5Nzo= +F +UCSniPJHnilt9LLbhKrJjlZKiXD4q6fuEUEcchTJn596ExeVIdfLLexO0uLKtgskXtc0EvI4HJ4= +d +295eJMhS1u4yOFM2Rs3IlMkqSQL+FtTQQtx2Mt3PZV5n4XGlslKz0sUG4PvoHNx5nI4aVkmIuNX= +m ++P5shOYw7SiHW3UncCEoueuugoMsdw92Rk3nYmAmRoQX+zFe3spQgjo4+8hISL9Ta9qB+ocl57K= +w +OJ4xaJcx+U3Pz0uO36TCEthDaG20D8qG2m0tNJ6/mUdSaCKu4EOND5VLy0s7Go8h93aQfjcC/hS= +P +eaBj45Gc5Pmo+D4rAfyWVyS0sx4UZpT77zijfYhtAJP2UF3Pp6//AFxrnsjlHfyS8rYkLHEcRIS= +3 +sJ/RPni6EG/VtkqUPFQOlBeHivaHifBsC3xbiqI3FcOvaleO462llBUenzkxxKn3VK6b7j30DL5= +L +wXDsZPJcTehIYVJjlWLctr6qVbjcnUqP5SfG9/GghaD28jOSnYeDkM47Itne1FllacZNKgDseDZ= +u +2s/pcTpfRQPUBpfa4tiZ4wHcXjzfE8gfyyVLkoaWem9t9v1W1p9322oJSb4vxjC8fYykeS3LgLR= +/ +8OYJDintLhtu2pUsny99BHHPc9gnYM/DQnGlT8i2hjIhl0K9GG2oLTDasb7Sr4nV/rV7AKCHMFw= +O +VksDJegtqU4vKO+gm17oaAa0+6gsXw3sXhOUcaTg8yPQyiUKcxkk6EKtuU0u3VKuvs6ig4OHdoY= +v +H8wX0vOMPwXbPRSbLbcSdCSPwI60Eh8j4Hh+eyEx5ZbgZMtkxJyEBIddT+ly1tT5+NAtdioeYwO= +X +ewGbZUzJiq2qB6EX0I9hoLk8ZcJiov5CgX6Dwi4tQIeexwlR1C1+tBXHu3wRGRiSYzqSkL1Ssfp= +U +NUqFvI0FEe9/aOHyKG7HfT8nm8cSIcvYpSVX1LbhA1bV1B/SftoKl5KDl+MZQRMm05j50dQcaWC= +U +qCkm6VtrT5EXCgaCfeAfVTkgyzj+4RXOWyAhrKNqspQHi8lOpV/Mnr4iglmB9Q+Fmpvi2GlJSmw= +e +SU6k21UkXNx7dKDCT3pTJ3BU4gqG1SUaXSfC51+zp7qBNzPdnFpxExhxwMJdYeQXnFD1CVNlIBJ= +6 +9ff7aCl3N8tGlvqSyb7U2/CgvdHwUV/C42fjZTCHlxY6vSbso6tp0A1Av7aCCe9HEY+XmHJx3EN= +Z +VtIQ60r4UyUJ6WV0Cx4X8NKCMOGcryPB8m60oLEWQQJcbooFPRab6XH4igklHOsdPHzDWRQhB6h= +1 +QSse9JIIoETknds4iI9DwcoPSHklG9A+FskW3bvFXlagb3btqLgp7fJskUuTLExELG4NbtCv2rt= +0 +8qCUMzyOLmuA8vBfBIxqnDdJBul9o6k0FfsLlExpE6Qy16rbxRtWVpQCUi2m7rQOKFMmvuJdjpS= +w +7ptUmW2hfsFwaBwt8T5xmUerNkPORtLpM5Fj/wCkXoJB4Q85wWKXmcE2EH/UkCUneSRa9tmv2mg= +j +V/tNy76gO72Q43w9IbgY9xT2cyzm4RILbjqrFduqz0SgaqPsBID6KfTr9M/afsVhPncdCVIyUpI= +b +lZJ+xly021Clj8jR6+k3YH9ZXQStnsyh2ElEFaRH2n9lACUJCdAnaLACg4eLZ45GG4lz40pSUuI= +V +qFIJ2lJoOflOPdzWNERpZXlcWDLwspR+N9pH5mVK8VAfD7dD50Ebct41/WMbF5fxptO9aSuRGJK= +Q +TclxBIBsd1/cffQQ3i+2vMO+GbnZ3k2an4vjEZ35TE4ph9TfqIj/AAKcJBI3KXcnTQWFAp817Yu= +8 +CgwOLcby2Rx+I5K+3Fnu/MKXICmQpQaQ6SC2h5KzvCbFWzyNA5uEfR127WynKIkTYEmx/fjPBL2= +/ +zXuSoEexV6CRsB2849xDIJ4xikKfj4eE24ZEiynnX5Mh15biykAblEk6AAdBQOaChuHlGJ23/TV= +c +kdetv7CaDZy7jpXPey0H4ZBtvsNHEWukH/nQMTMT3FxC4gltxlQsQbEG9BKfCHshlWoaHZDbOUg= +K +jplT32i6p3HvG6XAApN19U3NwDqQelBZ/CxTHZSn3UCvQFBrdbC0kHoetAzOWcXayDC7ouSKCq/= +e +TtNLmx3n8egImISoNLI0WP4Vf3GgqDy3tmMxvg8jjtSC2pQ9J1OxSV+Nj1FvZQRXlvpdya1rk8f= +y +Bjtk/DHfG+3uVobeVA3l/T93QiLPoOx1FPQhakn+yg6YvY/u8+EgTWEFX6A48tQPuAFAtM/Sn3O= +y +SEuZjOtR2l9SltROvtWTQOviX0ncNxb/AK/IpDuTko1Tc/Bu9t9Le21BJ2L7L45LhMf5h5pH5mk= +v +LQkeAuQeg8zQc2f7S4qY8mMZxlLOhYYustk+BUbjQ+F9aBl5n6aZeSUY5eVYfkLidqk38iP7zQc= +s +f6OmV/FOzHoote4WkfeCaDtw/wBKPDkSgXJD0gJP/wAyVD09PIq0oH5j/p24RFUFFPrgeIX6hv7= +d +osPvoHVH7TcLjRH4CMS28iW2WpMZ4oWh1HWyhY6XFxcUCK99Nfa+cSf9vMpGhWmOi6vaLqCUAfb= +Q +dh+mDg0aHvxWEjJUhJ9NDzbDhJPibIOv20Fb+7Dmc4dl8tHwP9MhY3EoSVtHGQ1vhwISpSQ4tpS= +j +qQOtAwsFO+o/nWK/qfGoSJOKutK5EeBimkDYbHcotoVpQXq7P8HHajsBwrHZCOGM3yyUM5yV4pA= +d +dclO/tpcV47WQkAdBc0EvOv4yUtWTgNqhx2HPSdQypW238RRfb9woEzPiTgpIlKdDsSYn9t4H4V= ++ +OtvH2+PvoEvimYIZzDaF7VDVsDrZwm/9tA+XFOTIyUx3PTeTZyM8OqHE9PsPQ+yg4cIERM5Ix6k= +h +uNmAuU2z4MzEW9ZA9h0UKDmzPrYyO5NhoSfQUHFNW/QFfHYD+W9Am93eP/13gMyVGRvehejkohT= +1 +3xVhzT3ouPtoHxxFQcjNm9/WabWCNL3SDeg5/wCnOozecnOC5eDSE21+FtGg/Gg49vW/tFA5dqp= +m +IQ+NVekR/mRQRNzGMIUmcwRYuWWPvoJJ4s83Hb4NnVf6OQbdw2Qt0KVgqbJ9oUnSgs9w2U9KwbS= +Z +QIfilUZ1R/UW9Ar7U2NAu0BQFBqeZS4kpIuDQNLknEY2RaVdAN/ZQV17o9gkZZ1eRxoEaaBYO7b= +h +xPkq2vuIoK/cj4ByLjLpORgrG2+2U3+4g/5yFbftAoG6S98OxoKt+bepSiftAFB1R8nkYSVIQAF= +O +/mKEJSLW81a0GL0qcAPmHisK/Mm5NyfDTWgzZyjcZsNHaSdQ0lJcdT7rWA+2g2OZp8tqjJdWlHi= +E +kn3ApRYf20Czx3KGESqUpLKbXbWoAuA+YCLfiaDonciYlSQ9LkOyVJFkbFejoOl7bifdoKDWy1n= +M +okoxeHceCujwaddUftOlAp47tL3EyI3JgrjoVY/vqS308gBegcUXsPzd4AyJyGyeoHqLt99qBYi= +f +TpkFJ3Tcq6VdbIQAP+omg6x2TmRTt/qkpYHTVIt+FAj8l7e5zj+NfycfNeg1GSXHVTFIbaSkeJc= +N +gn3nSgoJ9RKcVH5JJKsq0tnLNKEkNOJe2OfkJJaKgdUg0EP9sW52Y55xfiz7ryYsvJQoThjvOIH= +p +SZKUK0Gh0UaD6w/UQ61FyuMhREhEfHhgNIToEpbNgB7gKBQ4Yll6CsEBSXlELB6Gg2yoMFgu8az= +2 +5eEyJIYkJ/1Ibx6LQfYfCgjLC8e5Bw7uBk+G8oWF+s2zJxORQLNTI28p3p9ouNw8DQTExDEdDQQ= +b +hIFj5mgwn43e/HyDQspDiFA/wupuAfcoEpPvoFPJYdl6Ml9pPwvAhYP8woObi8ETONIxz4K/TL0= +V +RV4pSopAP+UigUMFAGPdjxUj4WUJaA66ISE/3UHejHrdRMeCf9YuKF/IjYn8BQJzmCUIn5bL/Ub= +a +6nr91Ar8fxy14xbCrXZcI9903oI87h8Qy0/LNt42MuQ7IbAabbSVFRTYEaeNBNnbDsvMTwvBwua= +o +LDuPebmpiJPxBxBKgFkdOuooJraabZSENJCQLCwHkLUGdAUBQFBipAULGgS5+HYlJIUka+FAz8v= +w +GHL3D0xc+ygj7NfT3xXMOFcrHI3nUutAtLv70WvQN6T9KHE3xZsy2f8AA9u/70qoOdf0hYJwWRk= +5 +qL+fpKH/AGCg1f8A4f4VsfFm5iR5ekyP7qDZD+kjizToMrJzHk/wpLLf4hBNA7cT9NHbGFtK8Wq= +W +oeMl51y/vG4J/Cgd2P7T8QxQH9Nw8WN/M2w2lX32vQKrXFYTOiW0gCg3p4/EGgQPuoM/6FHGu38= +K +AOGatbZ+FBwyeONr1CCKCIvqc47KidkOUTod0qjoiOOqHgymawXf+i96D5lZjnXLDl5wGQLn7zo= +C +ihpRsFkAXKTpQe8e5pnRyvjip0hK2U5bGFy7LQKUmY1uIISCNKC9n1KQgxyZUdJ3WCbn7DQdfb0= +F +eJaXbVWt/soHXOw6MnDchuj84+BX8KvA0EddxW5jvb9OfUT/AFHgsxqSpXVRgyCI0lHuAUlf+Wg= +e +3DZo5HgY8pGrgOxwfzCgdTWKMht2KQTuTe9uhHjQdkXGmREUCj4uvT9VviA+0UCbxOOq2RZCCPT= +l +/Am3Xe2m/wCIoFBEVYyqWGUlSidwQBc9NaBabxGTebLbcVwlWtwg9KBRx/B87LWlL8JSW19Suw0= +o +F/B9sHozrhyLyEMqsQ20SVEi/U6CgeuMwWKxTYTCYSCP/IRdV/eaBQoCgKAoCgKAoPCL9aDBbKV= +U +Gow0HwoPPkkmgPkUedBgqD5Gg0qx1z0H3CgzRAtQbPkhQYjHo8RQbUwWgNRQHyTdBkIbQoAw2j4= +U +CXybiOF5dxzKcVzjAkY7LxnoU1k/qafQUKHvsdKClX/8ju0wdK2+e8jaQb2QgwRa/QXLB6UFb/r= +L ++irjv0143imZ4XzSdk5uQlvKfjZkMKUhEQNuNuNCOhs6LNlE36igs/3Wko5phsHzaEQ+1l4UKWH= +E +ap/eZCydPaaBZ7W4abLxcRmOwt1VlGyEKVpf2CgliF275I/tX8p6Q6guEJ/DrQc2R7BS8w1mocy= +W +0xGzsN+DKQElwpDySncBoLg69aBU7fdhMRwiAYa57+QKtpUVpS2m6UhOgFz4edBIUPiWJjJ2txk= +6 +9bi96BQj4WA1omMhIBv+VPWg6E4rGJVv+Ua3fxemm/8AZQbEQoTSt7UdtCjoVJQkH8BQbglI6AD= +3 +Cg9oPLX0NB7QFAUBQFAUBQFAUBQFAUBQFAUBQFAUBQFAUBQFAUEad8OwvEu/OJx+I5U/IjIx7jq= +0 +OxSkLW28jY42d4ULEhKr9QUigUOI9luA8P4rjOIRoPz8HEsJiRTPIfX6SFFSQrQJNug06UDziwI= +U +FlMeEw3HaQLIbaQlCQB4AJAFBu2p8qAKEmg8DaRQZAAdKD2gKAoCgKAoCgKAoCgKAoCgKAoCgKA= +o +CgKAoCgKAoCgKAoCgKAoCgKAoCgKAoCgKAoCgKAoCgKAoCgKD//Z=09 + + +R0lGODlhCQAIAOcPAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP// +AAAA//8A/wD//////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAA +mQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBmZgBmmQBmzABm/wCZAACZ +MwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/mQD/zAD/ +/zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNm +mTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/ +MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz +/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbM +mWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkz +M5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZZpmZmZmZzJmZ +/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA +mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZ +M8yZZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz/ +//8AAP8AM/8AZv8Amf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9m +mf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///MAP/MM//MZv/Mmf/MzP/M////AP// +M///Zv//mf//zP///yH5BAEAAPwALAAAAAAJAAgAAAgyAP/5g4MChYpt//71I1jQ +IEJiDRve+GcnYsEY20BZRBHjn0aLOAQyLKjiVsKBJE3+CwgAOw=3D=3D + + + + + + + + + Esta es una frase t=C3=ADpica + + + + + + Este es un ejemplo de un p=C3=A1rrafo compuesto por varios= + fragmentos, + + + =20 + =C3=81lgunos de ellos con un fondo distinto, y varias T=C3= +=8Dldes. + + + =20 + =C3=93tras c=C3=93sas =C3=8Dnteresantes, caray. + + + + + + + + + + + + + + + + + + + + + + + + + Celda que ocupa dos filas + + + + + + + + + + + + + + + A + + + + + + + + + + + + + + + + B + + + + + + + + + C + + + + + + + + + + D + + + + + =20 + + + + Esta es otra frase t=C3=ADpica + + + + + +------=_Part_16_799571960.1350659465464-- \ No newline at end of file diff --git a/net-oauth/build.gradle b/net-oauth/build.gradle new file mode 100644 index 0000000..5929d8a --- /dev/null +++ b/net-oauth/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':net-http') +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/AbstractOAuthConsumer.java b/net-oauth/src/main/java/org/xbib/net/oauth/AbstractOAuthConsumer.java new file mode 100644 index 0000000..fb90280 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/AbstractOAuthConsumer.java @@ -0,0 +1,237 @@ +package org.xbib.net.oauth; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import java.security.SecureRandom; +import java.util.Random; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; +import org.xbib.net.http.UrlStringRequestAdapter; +import org.xbib.net.oauth.sign.AuthorizationHeaderSigningStrategy; +import org.xbib.net.oauth.sign.HmacSha1MessageSigner; +import org.xbib.net.oauth.sign.OAuthMessageSigner; +import org.xbib.net.oauth.sign.QueryStringSigningStrategy; +import org.xbib.net.oauth.sign.SigningStrategy; + +/** + * ABC for consumer implementations. If you're developing a custom consumer you + * will probably inherit from this class to save you a lot of work. + * + */ +public abstract class AbstractOAuthConsumer implements OAuthConsumer { + + private String consumerKey, consumerSecret; + + private String token; + + private OAuthMessageSigner messageSigner; + + private SigningStrategy signingStrategy; + + // these are params that may be passed to the consumer directly (i.e. + // without going through the request object) + private HttpParameters additionalParameters; + + // these are the params which will be passed to the message signer + private HttpParameters requestParameters; + + private boolean sendEmptyTokens; + + private final Random random = new SecureRandom(); + + public AbstractOAuthConsumer(String consumerKey, String consumerSecret) { + this.consumerKey = consumerKey; + this.consumerSecret = consumerSecret; + setMessageSigner(new HmacSha1MessageSigner()); + setSigningStrategy(new AuthorizationHeaderSigningStrategy()); + } + + public void setMessageSigner(OAuthMessageSigner messageSigner) { + this.messageSigner = messageSigner; + messageSigner.setConsumerSecret(consumerSecret); + } + + public void setSigningStrategy(SigningStrategy signingStrategy) { + this.signingStrategy = signingStrategy; + } + + public void setAdditionalParameters(HttpParameters additionalParameters) { + this.additionalParameters = additionalParameters; + } + + public synchronized HttpRequest sign(HttpRequest request) throws OAuthMessageSignerException, + OAuthExpectationFailedException, OAuthCommunicationException { + if (consumerKey == null) { + throw new OAuthExpectationFailedException("consumer key not set"); + } + if (consumerSecret == null) { + throw new OAuthExpectationFailedException("consumer secret not set"); + } + requestParameters = new HttpParameters(); + try { + if (additionalParameters != null) { + requestParameters.putAll(additionalParameters, false); + } + collectHeaderParameters(request, requestParameters); + collectQueryParameters(request, requestParameters); + collectBodyParameters(request, requestParameters); + // add any OAuth params that haven't already been set + completeOAuthParameters(requestParameters); + requestParameters.remove(OAuth.OAUTH_SIGNATURE); + } catch (IOException e) { + throw new OAuthCommunicationException(e); + } + String signature = messageSigner.sign(request, requestParameters); + try { + signingStrategy.writeSignature(signature, request, requestParameters); + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new OAuthMessageSignerException(e); + } + return request; + } + + public synchronized HttpRequest sign(Object request) throws OAuthMessageSignerException, + OAuthExpectationFailedException, OAuthCommunicationException { + return sign(wrap(request)); + } + + public synchronized String sign(String url) throws OAuthMessageSignerException, + OAuthExpectationFailedException, OAuthCommunicationException { + HttpRequest request = new UrlStringRequestAdapter(url); + // switch to URL signing + SigningStrategy oldStrategy = this.signingStrategy; + this.signingStrategy = new QueryStringSigningStrategy(); + sign(request); + // revert to old strategy + this.signingStrategy = oldStrategy; + return request.getRequestUrl(); + } + + /** + * Adapts the given request object to a Signpost {@link HttpRequest}. How + * this is done depends on the consumer implementation. + * + * @param request + * the native HTTP request instance + * @return the adapted request + */ + protected abstract HttpRequest wrap(Object request); + + public void setTokenWithSecret(String token, String tokenSecret) { + this.token = token; + messageSigner.setTokenSecret(tokenSecret); + } + + public String getToken() { + return token; + } + + public String getTokenSecret() { + return messageSigner.getTokenSecret(); + } + + public String getConsumerKey() { + return this.consumerKey; + } + + public String getConsumerSecret() { + return this.consumerSecret; + } + + /** + *

+ * Helper method that adds any OAuth parameters to the given request + * parameters which are missing from the current request but required for + * signing. A good example is the oauth_nonce parameter, which is typically + * not provided by the client in advance. + *

+ *

+ * It's probably not a very good idea to override this method. If you want + * to generate different nonces or timestamps, override + * {@link #generateNonce()} or {@link #generateTimestamp()} instead. + *

+ * + * @param out + * the request parameter which should be completed + */ + protected void completeOAuthParameters(HttpParameters out) + throws MalformedInputException, UnmappableCharacterException { + if (!out.containsKey(OAuth.OAUTH_CONSUMER_KEY)) { + out.put(OAuth.OAUTH_CONSUMER_KEY, consumerKey, true); + } + if (!out.containsKey(OAuth.OAUTH_SIGNATURE_METHOD)) { + out.put(OAuth.OAUTH_SIGNATURE_METHOD, messageSigner.getSignatureMethod(), true); + } + if (!out.containsKey(OAuth.OAUTH_TIMESTAMP)) { + out.put(OAuth.OAUTH_TIMESTAMP, generateTimestamp(), true); + } + if (!out.containsKey(OAuth.OAUTH_NONCE)) { + out.put(OAuth.OAUTH_NONCE, generateNonce(), true); + } + if (!out.containsKey(OAuth.OAUTH_VERSION)) { + out.put(OAuth.OAUTH_VERSION, OAuth.VERSION_1_0, true); + } + if (!out.containsKey(OAuth.OAUTH_TOKEN)) { + if (token != null && !token.equals("") || sendEmptyTokens) { + out.put(OAuth.OAUTH_TOKEN, token, true); + } + } + } + + public HttpParameters getRequestParameters() { + return requestParameters; + } + + public void setSendEmptyTokens(boolean enable) { + this.sendEmptyTokens = enable; + } + + /** + * Collects OAuth Authorization header parameters as per OAuth Core 1.0 spec + * section 9.1.1 + */ + protected void collectHeaderParameters(HttpRequest request, HttpParameters out) + throws MalformedInputException, UnmappableCharacterException { + HttpParameters headerParams = OAuth.oauthHeaderToParamsMap(request.getHeader(OAuth.HTTP_AUTHORIZATION_HEADER)); + out.putAll(headerParams, false); + } + + /** + * Collects x-www-form-urlencoded body parameters as per OAuth Core 1.0 spec + * section 9.1.1 + */ + protected void collectBodyParameters(HttpRequest request, HttpParameters out) + throws IOException { + // collect x-www-form-urlencoded body params + String contentType = request.getContentType(); + if (contentType != null && contentType.startsWith(OAuth.FORM_ENCODED)) { + InputStream payload = request.getMessagePayload(); + out.putAll(OAuth.decodeForm(payload), true); + } + } + + /** + * Collects HTTP GET query string parameters as per OAuth Core 1.0 spec + * section 9.1.1 + */ + protected void collectQueryParameters(HttpRequest request, HttpParameters out) + throws MalformedInputException, UnmappableCharacterException { + String url = request.getRequestUrl(); + int q = url.indexOf('?'); + if (q >= 0) { + // Combine the URL query string with the other parameters: + out.putAll(OAuth.decodeForm(url.substring(q + 1)), true); + } + } + + protected String generateTimestamp() { + return Long.toString(System.currentTimeMillis() / 1000L); + } + + protected String generateNonce() { + return Long.toString(random.nextLong()); + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/AbstractOAuthProvider.java b/net-oauth/src/main/java/org/xbib/net/oauth/AbstractOAuthProvider.java new file mode 100644 index 0000000..29d309e --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/AbstractOAuthProvider.java @@ -0,0 +1,299 @@ +package org.xbib.net.oauth; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; +import org.xbib.net.http.HttpResponse; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import java.util.HashMap; +import java.util.Map; + +/** + * For all provider implementations. If you're writing a custom provider, + * you will probably inherit from this class, since it takes a lot of work from + * you. + */ +public abstract class AbstractOAuthProvider implements OAuthProvider { + + private final String requestTokenEndpointUrl; + + private final String accessTokenEndpointUrl; + + private final String authorizationWebsiteUrl; + + private HttpParameters responseParameters; + + private Map defaultHeaders; + + private boolean isOAuth10a; + + private OAuthProviderListener listener; + + public AbstractOAuthProvider(String requestTokenEndpointUrl, String accessTokenEndpointUrl, + String authorizationWebsiteUrl) { + this.requestTokenEndpointUrl = requestTokenEndpointUrl; + this.accessTokenEndpointUrl = accessTokenEndpointUrl; + this.authorizationWebsiteUrl = authorizationWebsiteUrl; + this.responseParameters = new HttpParameters(); + this.defaultHeaders = new HashMap(); + } + + public synchronized String retrieveRequestToken(OAuthConsumer consumer, String callbackUrl, + String... customOAuthParams) throws OAuthMessageSignerException, + OAuthNotAuthorizedException, OAuthExpectationFailedException, + OAuthCommunicationException { + // invalidate current credentials, if any + consumer.setTokenWithSecret(null, null); + // 1.0a expects the callback to be sent while getting the request token. + // 1.0 service providers would simply ignore this parameter. + HttpParameters params = new HttpParameters(); + try { + params.putAll(customOAuthParams, true); + params.put(OAuth.OAUTH_CALLBACK, callbackUrl, true); + retrieveToken(consumer, requestTokenEndpointUrl, params); + String callbackConfirmed = responseParameters.getFirst(OAuth.OAUTH_CALLBACK_CONFIRMED); + responseParameters.remove(OAuth.OAUTH_CALLBACK_CONFIRMED); + isOAuth10a = Boolean.TRUE.toString().equals(callbackConfirmed); + // 1.0 service providers expect the callback as part of the auth URL, + // Do not send when 1.0a. + if (isOAuth10a) { + return OAuth.addQueryParameters(authorizationWebsiteUrl, OAuth.OAUTH_TOKEN, + consumer.getToken()); + } else { + return OAuth.addQueryParameters(authorizationWebsiteUrl, OAuth.OAUTH_TOKEN, + consumer.getToken(), OAuth.OAUTH_CALLBACK, callbackUrl); + } + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new OAuthMessageSignerException(e); + } + } + + public synchronized void retrieveAccessToken(OAuthConsumer consumer, String oauthVerifier, + String... customOAuthParams) throws OAuthMessageSignerException, + OAuthNotAuthorizedException, OAuthExpectationFailedException, + OAuthCommunicationException { + if (consumer.getToken() == null || consumer.getTokenSecret() == null) { + throw new OAuthExpectationFailedException( + "Authorized request token or token secret not set. " + + "Did you retrieve an authorized request token before?"); + } + HttpParameters params = new HttpParameters(); + try { + params.putAll(customOAuthParams, true); + if (isOAuth10a && oauthVerifier != null) { + params.put(OAuth.OAUTH_VERIFIER, oauthVerifier, true); + } + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new OAuthMessageSignerException(e); + } + retrieveToken(consumer, accessTokenEndpointUrl, params); + } + + /** + * Implemented by subclasses. The responsibility of this method is to + * contact the service provider at the given endpoint URL and fetch a + * request or access token. What kind of token is retrieved solely depends + * on the URL being used. + * Correct implementations of this method must guarantee the following + * post-conditions: + *
    + *
  • the {@link OAuthConsumer} passed to this method must have a valid + * {@link OAuth#OAUTH_TOKEN} and {@link OAuth#OAUTH_TOKEN_SECRET} set by + * calling {@link OAuthConsumer#setTokenWithSecret(String, String)}
  • + *
  • {@link #getResponseParameters()} must return the set of query + * parameters served by the service provider in the token response, with all + * OAuth specific parameters being removed
  • + *
+ * + * @param consumer the {@link OAuthConsumer} that should be used to sign the request + * @param endpointUrl the URL at which the service provider serves the OAuth token that + * is to be fetched + * @param customOAuthParams you can pass custom OAuth parameters here (such as oauth_callback + * or oauth_verifier) which will go directly into the signer, i.e. + * you don't have to put them into the request first. + * @throws OAuthMessageSignerException if signing the token request fails + * @throws OAuthCommunicationException if a network communication error occurs + * @throws OAuthNotAuthorizedException if the server replies 401 - Unauthorized + * @throws OAuthExpectationFailedException if an expectation has failed, e.g. because the server didn't + * reply in the expected format + */ + protected void retrieveToken(OAuthConsumer consumer, String endpointUrl, + HttpParameters customOAuthParams) throws OAuthMessageSignerException, + OAuthCommunicationException, OAuthNotAuthorizedException, + OAuthExpectationFailedException { + Map defaultHeaders = getRequestHeaders(); + if (consumer.getConsumerKey() == null || consumer.getConsumerSecret() == null) { + throw new OAuthExpectationFailedException("Consumer key or secret not set"); + } + HttpRequest request = null; + HttpResponse response = null; + try { + request = createRequest(endpointUrl); + for (String header : defaultHeaders.keySet()) { + request.setHeader(header, defaultHeaders.get(header)); + } + if (customOAuthParams != null && !customOAuthParams.isEmpty()) { + consumer.setAdditionalParameters(customOAuthParams); + } + if (this.listener != null) { + this.listener.prepareRequest(request); + } + consumer.sign(request); + if (this.listener != null) { + this.listener.prepareSubmission(request); + } + response = sendRequest(request); + int statusCode = response.getStatusCode(); + boolean requestHandled = false; + if (this.listener != null) { + requestHandled = this.listener.onResponseReceived(request, response); + } + if (requestHandled) { + return; + } + if (statusCode >= 300) { + handleUnexpectedResponse(statusCode, response); + } + HttpParameters responseParams = OAuth.decodeForm(response.getContent()); + String token = responseParams.getFirst(OAuth.OAUTH_TOKEN); + String secret = responseParams.getFirst(OAuth.OAUTH_TOKEN_SECRET); + responseParams.remove(OAuth.OAUTH_TOKEN); + responseParams.remove(OAuth.OAUTH_TOKEN_SECRET); + setResponseParameters(responseParams); + if (token == null || secret == null) { + throw new OAuthExpectationFailedException( + "Request token or token secret not set in server reply. " + + "The service provider you use is probably buggy."); + } + consumer.setTokenWithSecret(token, secret); + } catch (OAuthNotAuthorizedException | OAuthExpectationFailedException e) { + throw e; + } catch (Exception e) { + throw new OAuthCommunicationException(e); + } finally { + try { + closeConnection(request, response); + } catch (Exception e) { + throw new OAuthCommunicationException(e); + } + } + } + + protected void handleUnexpectedResponse(int statusCode, HttpResponse response) throws Exception { + if (response == null) { + return; + } + BufferedReader reader = new BufferedReader(new InputStreamReader(response.getContent())); + StringBuilder responseBody = new StringBuilder(); + String line = reader.readLine(); + while (line != null) { + responseBody.append(line); + line = reader.readLine(); + } + if (statusCode == 401) { + throw new OAuthNotAuthorizedException(responseBody.toString()); + } + throw new OAuthCommunicationException("Service provider responded in error: " + + statusCode + " (" + response.getReasonPhrase() + ")", responseBody.toString()); + } + + /** + * Overrride this method if you want to customize the logic for building a + * request object for the given endpoint URL. + * + * @param endpointUrl + * the URL to which the request will go + * @return the request object + * @throws Exception + * if something breaks + */ + protected abstract HttpRequest createRequest(String endpointUrl) throws Exception; + + /** + * Override this method if you want to customize the logic for how the given + * request is sent to the server. + * + * @param request + * the request to send + * @return the response to the request + * @throws Exception + * if something breaks + */ + protected abstract HttpResponse sendRequest(HttpRequest request) throws Exception; + + /** + * Called when the connection is being finalized after receiving the + * response. Use this to do any cleanup / resource freeing. + * + * @param request + * the request that has been sent + * @param response + * the response that has been received + * @throws Exception + * if something breaks + */ + protected void closeConnection(HttpRequest request, HttpResponse response) throws Exception { + // NOP + } + + public HttpParameters getResponseParameters() { + return responseParameters; + } + + /** + * Returns a single query parameter as served by the service provider in a + * token reply. You must call {@link #setResponseParameters} with the set of + * parameters before using this method. + * + * @param key + * the parameter name + * @return the parameter value + */ + protected String getResponseParameter(String key) + throws MalformedInputException, UnmappableCharacterException { + return responseParameters.getFirst(key); + } + + public void setResponseParameters(HttpParameters parameters) { + this.responseParameters = parameters; + } + + public void setOAuth10a(boolean isOAuth10aProvider) { + this.isOAuth10a = isOAuth10aProvider; + } + + public boolean isOAuth10a() { + return isOAuth10a; + } + + public String getRequestTokenEndpointUrl() { + return this.requestTokenEndpointUrl; + } + + public String getAccessTokenEndpointUrl() { + return this.accessTokenEndpointUrl; + } + + public String getAuthorizationWebsiteUrl() { + return this.authorizationWebsiteUrl; + } + + public void setRequestHeader(String header, String value) { + defaultHeaders.put(header, value); + } + + public Map getRequestHeaders() { + return defaultHeaders; + } + + public void setListener(OAuthProviderListener listener) { + this.listener = listener; + } + + public void removeListener(OAuthProviderListener listener) { + this.listener = null; + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/DefaultOAuthConsumer.java b/net-oauth/src/main/java/org/xbib/net/oauth/DefaultOAuthConsumer.java new file mode 100644 index 0000000..896eac5 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/DefaultOAuthConsumer.java @@ -0,0 +1,26 @@ +package org.xbib.net.oauth; + +import org.xbib.net.http.HttpRequest; + +import java.net.HttpURLConnection; + +/** + * The default implementation for an OAuth consumer. Only supports signing + * {@link HttpURLConnection} type requests. + */ +public class DefaultOAuthConsumer extends AbstractOAuthConsumer { + + public DefaultOAuthConsumer(String consumerKey, String consumerSecret) { + super(consumerKey, consumerSecret); + } + + @Override + protected HttpRequest wrap(Object request) { + if (!(request instanceof HttpURLConnection)) { + throw new IllegalArgumentException( + "The default consumer expects requests of type java.net.HttpURLConnection"); + } + return new HttpURLConnectionRequestAdapter((HttpURLConnection) request); + } + +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/DefaultOAuthProvider.java b/net-oauth/src/main/java/org/xbib/net/oauth/DefaultOAuthProvider.java new file mode 100644 index 0000000..8f19ccc --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/DefaultOAuthProvider.java @@ -0,0 +1,42 @@ +package org.xbib.net.oauth; + +import org.xbib.net.http.HttpRequest; +import org.xbib.net.http.HttpResponse; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * This default implementation uses {@link HttpURLConnection} type GET + * requests to receive elements from a service provider. + */ +public class DefaultOAuthProvider extends AbstractOAuthProvider { + + public DefaultOAuthProvider(String requestTokenEndpointUrl, String accessTokenEndpointUrl, + String authorizationWebsiteUrl) { + super(requestTokenEndpointUrl, accessTokenEndpointUrl, authorizationWebsiteUrl); + } + + protected HttpRequest createRequest(String endpointUrl) throws IOException { + HttpURLConnection connection = (HttpURLConnection) new URL(endpointUrl).openConnection(); + connection.setRequestMethod("POST"); + connection.setAllowUserInteraction(false); + connection.setRequestProperty("Content-Length", "0"); + return new HttpURLConnectionRequestAdapter(connection); + } + + protected HttpResponse sendRequest(HttpRequest request) throws IOException { + HttpURLConnection connection = (HttpURLConnection) request.unwrap(); + connection.connect(); + return new HttpURLConnectionResponseAdapter(connection); + } + + @Override + protected void closeConnection(HttpRequest request, HttpResponse response) { + HttpURLConnection connection = (HttpURLConnection) request.unwrap(); + if (connection != null) { + connection.disconnect(); + } + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/HttpURLConnectionRequestAdapter.java b/net-oauth/src/main/java/org/xbib/net/oauth/HttpURLConnectionRequestAdapter.java new file mode 100644 index 0000000..c2df3a6 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/HttpURLConnectionRequestAdapter.java @@ -0,0 +1,61 @@ +package org.xbib.net.oauth; + +import org.xbib.net.http.HttpRequest; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class HttpURLConnectionRequestAdapter implements HttpRequest { + + protected HttpURLConnection connection; + + public HttpURLConnectionRequestAdapter(HttpURLConnection connection) { + this.connection = connection; + } + + public String getMethod() { + return connection.getRequestMethod(); + } + + public String getRequestUrl() { + return connection.getURL().toExternalForm(); + } + + public void setRequestUrl(String url) { + } + + public void setHeader(String name, String value) { + connection.setRequestProperty(name, value); + } + + public String getHeader(String name) { + return connection.getRequestProperty(name); + } + + public Map getAllHeaders() { + Map> origHeaders = connection.getRequestProperties(); + Map headers = new HashMap(origHeaders.size()); + for (String name : origHeaders.keySet()) { + List values = origHeaders.get(name); + if (!values.isEmpty()) { + headers.put(name, values.get(0)); + } + } + return headers; + } + + public InputStream getMessagePayload() { + return null; + } + + public String getContentType() { + return connection.getRequestProperty("Content-Type"); + } + + public HttpURLConnection unwrap() { + return connection; + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/HttpURLConnectionResponseAdapter.java b/net-oauth/src/main/java/org/xbib/net/oauth/HttpURLConnectionResponseAdapter.java new file mode 100644 index 0000000..6990419 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/HttpURLConnectionResponseAdapter.java @@ -0,0 +1,36 @@ +package org.xbib.net.oauth; + +import org.xbib.net.http.HttpResponse; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; + +public class HttpURLConnectionResponseAdapter implements HttpResponse { + + private HttpURLConnection connection; + + public HttpURLConnectionResponseAdapter(HttpURLConnection connection) { + this.connection = connection; + } + + public InputStream getContent() { + try { + return connection.getInputStream(); + } catch (IOException e) { + return connection.getErrorStream(); + } + } + + public int getStatusCode() throws IOException { + return connection.getResponseCode(); + } + + public String getReasonPhrase() throws Exception { + return connection.getResponseMessage(); + } + + public Object unwrap() { + return connection; + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/OAuth.java b/net-oauth/src/main/java/org/xbib/net/oauth/OAuth.java new file mode 100644 index 0000000..94f5db2 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/OAuth.java @@ -0,0 +1,285 @@ +package org.xbib.net.oauth; + +import org.xbib.net.PercentDecoder; +import org.xbib.net.PercentEncoder; +import org.xbib.net.PercentEncoders; +import org.xbib.net.http.HttpParameters; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class OAuth { + + public static final String VERSION_1_0 = "1.0"; + + public static final String ENCODING = "UTF-8"; + + public static final String FORM_ENCODED = "application/x-www-form-urlencoded"; + + public static final String HTTP_AUTHORIZATION_HEADER = "Authorization"; + + public static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key"; + + public static final String OAUTH_TOKEN = "oauth_token"; + + public static final String OAUTH_TOKEN_SECRET = "oauth_token_secret"; + + public static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method"; + + public static final String OAUTH_SIGNATURE = "oauth_signature"; + + public static final String OAUTH_TIMESTAMP = "oauth_timestamp"; + + public static final String OAUTH_NONCE = "oauth_nonce"; + + public static final String OAUTH_VERSION = "oauth_version"; + + public static final String OAUTH_CALLBACK = "oauth_callback"; + + public static final String OAUTH_CALLBACK_CONFIRMED = "oauth_callback_confirmed"; + + public static final String OAUTH_VERIFIER = "oauth_verifier"; + + /** + * Pass this value as the callback "url" upon retrieving a request token if + * your application cannot receive callbacks (e.g. because it's a desktop + * app). This will tell the service provider that verification happens + * out-of-band, which basically means that it will generate a PIN code (the + * OAuth verifier) and display that to your user. You must obtain this code + * from your user and pass it to + * {@link OAuthProvider#retrieveAccessToken(OAuthConsumer, String, String...)} in order + * to complete the token handshake. + */ + public static final String OUT_OF_BAND = "oob"; + + public static final PercentEncoder percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); + + public static final PercentDecoder percentDecoder = new PercentDecoder(); + + /** + * Construct a x-www-form-urlencoded document containing the given sequence + * of name/value pairs. Use OAuth percent encoding (not exactly the encoding + * mandated by x-www-form-urlencoded). + */ + public static > void formEncode(Collection parameters, + OutputStream into) throws IOException { + if (parameters != null) { + boolean first = true; + for (Map.Entry entry : parameters) { + if (first) { + first = false; + } else { + into.write('&'); + } + into.write(percentEncoder.encode(safeToString(entry.getKey())).getBytes()); + into.write('='); + into.write(percentEncoder.encode(safeToString(entry.getValue())).getBytes()); + } + } + } + + /** + * Construct a x-www-form-urlencoded document containing the given sequence + * of name/value pairs. Use OAuth percent encoding (not exactly the encoding + * mandated by x-www-form-urlencoded). + */ + public static > String formEncode(Collection parameters) + throws IOException { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + formEncode(parameters, b); + return new String(b.toByteArray()); + } + + /** + * Parse a form-urlencoded document. + */ + public static HttpParameters decodeForm(String form) + throws MalformedInputException, UnmappableCharacterException { + HttpParameters params = new HttpParameters(); + if (isEmpty(form)) { + return params; + } + for (String nvp : form.split("\\&")) { + int equals = nvp.indexOf('='); + String name; + String value; + if (equals < 0) { + name = percentDecoder.decode(nvp); + value = null; + } else { + name = percentDecoder.decode(nvp.substring(0, equals)); + value = percentDecoder.decode(nvp.substring(equals + 1)); + } + + params.put(name, value); + } + return params; + } + + public static HttpParameters decodeForm(InputStream content) + throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader( + content)); + StringBuilder sb = new StringBuilder(); + String line = reader.readLine(); + while (line != null) { + sb.append(line); + line = reader.readLine(); + } + + return decodeForm(sb.toString()); + } + + /** + * Construct a Map containing a copy of the given parameters. If several + * parameters have the same name, the Map will contain the first value, + * only. + */ + public static > Map toMap(Collection from) { + HashMap map = new HashMap(); + if (from != null) { + for (Map.Entry entry : from) { + String key = entry.getKey(); + if (!map.containsKey(key)) { + map.put(key, entry.getValue()); + } + } + } + return map; + } + + public static final String safeToString(Object from) { + return (from == null) ? null : from.toString(); + } + + public static boolean isEmpty(String str) { + return (str == null) || (str.length() == 0); + } + + /** + * Appends a list of key/value pairs to the given URL, e.g.: + * + *
+     * String url = OAuth.addQueryParameters("http://example.com?a=1", b, 2, c, 3);
+     * 
+ * + * which yields: + * + *
+     * http://example.com?a=1&b=2&c=3
+     * 
+ * + * All parameters will be encoded according to OAuth's percent encoding + * rules. + * + * @param url + * the URL + * @param kvPairs + * the list of key/value pairs + * @return string + */ + public static String addQueryParameters(String url, String... kvPairs) + throws MalformedInputException, UnmappableCharacterException { + String queryDelim = url.contains("?") ? "&" : "?"; + StringBuilder sb = new StringBuilder(url + queryDelim); + for (int i = 0; i < kvPairs.length; i += 2) { + if (i > 0) { + sb.append("&"); + } + sb.append(percentEncoder.encode(kvPairs[i])).append("=") + .append(percentEncoder.encode(kvPairs[i + 1])); + } + return sb.toString(); + } + + public static String addQueryParameters(String url, Map params) + throws MalformedInputException, UnmappableCharacterException { + String[] kvPairs = new String[params.size() * 2]; + int idx = 0; + for (String key : params.keySet()) { + kvPairs[idx] = key; + kvPairs[idx + 1] = params.get(key); + idx += 2; + } + return addQueryParameters(url, kvPairs); + } + + public static String addQueryString(String url, String queryString) { + String queryDelim = url.contains("?") ? "&" : "?"; + return url + queryDelim + queryString; + } + + /** + * Builds an OAuth header from the given list of header fields. All + * parameters starting in 'oauth_*' will be percent encoded. + * + *
+     * String authHeader = OAuth.prepareOAuthHeader("realm", "http://example.com", "oauth_token", "x%y");
+     * 
+ * + * which yields: + * + *
+     * OAuth realm="http://example.com", oauth_token="x%25y"
+     * 
+ * + * @param kvPairs + * the list of key/value pairs + * @return a string eligible to be used as an OAuth HTTP Authorization + * header. + */ + public static String prepareOAuthHeader(String... kvPairs) + throws MalformedInputException, UnmappableCharacterException { + StringBuilder sb = new StringBuilder("OAuth "); + for (int i = 0; i < kvPairs.length; i += 2) { + if (i > 0) { + sb.append(", "); + } + boolean isOAuthElem = kvPairs[i].startsWith("oauth_") + || kvPairs[i].startsWith("x_oauth_"); + String value = isOAuthElem ? percentEncoder.encode(kvPairs[i + 1]) : kvPairs[i + 1]; + sb.append(percentEncoder.encode(kvPairs[i])).append("=\"").append(value).append("\""); + } + return sb.toString(); + } + + public static HttpParameters oauthHeaderToParamsMap(String oauthHeader) + throws MalformedInputException, UnmappableCharacterException { + HttpParameters params = new HttpParameters(); + if (oauthHeader == null || !oauthHeader.startsWith("OAuth ")) { + return params; + } + String[] elements = oauthHeader.substring("OAuth ".length()).split(","); + for (String keyValuePair : elements) { + String[] keyValue = keyValuePair.split("="); + params.put(keyValue[0].trim(), keyValue[1].replace("\"", "").trim()); + } + return params; + } + + /** + * Helper method to concatenate a parameter and its value to a pair that can + * be used in an HTTP header. This method percent encodes both parts before + * joining them. + * + * @param name + * the OAuth parameter name, e.g. oauth_token + * @param value + * the OAuth parameter value, e.g. 'hello oauth' + * @return a name/value pair, e.g. oauth_token="hello%20oauth" + */ + public static String toHeaderElement(String name, String value) + throws MalformedInputException, UnmappableCharacterException { + return percentEncoder.encode(name) + "=\"" + percentEncoder.encode(value) + "\""; + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/OAuthCommunicationException.java b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthCommunicationException.java new file mode 100644 index 0000000..d93c15c --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthCommunicationException.java @@ -0,0 +1,21 @@ +package org.xbib.net.oauth; + +@SuppressWarnings("serial") +public class OAuthCommunicationException extends OAuthException { + + private String responseBody; + + public OAuthCommunicationException(Exception cause) { + super("Communication with the service provider failed: " + + cause.getLocalizedMessage(), cause); + } + + public OAuthCommunicationException(String message, String responseBody) { + super(message); + this.responseBody = responseBody; + } + + public String getResponseBody() { + return responseBody; + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/OAuthConsumer.java b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthConsumer.java new file mode 100644 index 0000000..da5a15b --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthConsumer.java @@ -0,0 +1,157 @@ +package org.xbib.net.oauth; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; +import org.xbib.net.oauth.sign.OAuthMessageSigner; +import org.xbib.net.oauth.sign.SigningStrategy; + +/** + *

+ * Exposes a simple interface to sign HTTP requests using a given OAuth token + * and secret. Refer to {@link OAuthProvider} how to retrieve a valid token and + * token secret. + *

+ * HTTP messages are signed as follows: + * + *
+ * // exchange the arguments with the actual token/secret pair
+ * OAuthConsumer consumer = new DefaultOAuthConsumer("1234", "5678");
+ * URL url = new URL("http://example.com/protected.xml");
+ * HttpURLConnection request = (HttpURLConnection) url.openConnection();
+ * consumer.sign(request);
+ * request.connect();
+ * 
+ * + */ +public interface OAuthConsumer { + + /** + * Sets the message signer that should be used to generate the OAuth + * signature. + * + * @param messageSigner + * the signer + */ + void setMessageSigner(OAuthMessageSigner messageSigner); + + /** + * Allows you to add parameters (typically OAuth parameters such as + * oauth_callback or oauth_verifier) which will go directly into the signer, + * i.e. you don't have to put them into the request first. The consumer's + * signing strategy will then take care of writing them to the + * correct part of the request before it is sent. This is useful if you want + * to pre-set custom OAuth parameters. Note that these parameters are + * expected to already be percent encoded -- they will be simply merged + * as-is. BE CAREFUL WITH THIS METHOD! Your service provider may decide + * to ignore any non-standard OAuth params when computing the signature. + * + * @param additionalParameters + * the parameters + */ + void setAdditionalParameters(HttpParameters additionalParameters); + + /** + * Defines which strategy should be used to write a signature to an HTTP + * request. + * + * @param signingStrategy + * the strategy + */ + void setSigningStrategy(SigningStrategy signingStrategy); + + /** + *

+ * Causes the consumer to always include the oauth_token parameter to be + * sent, even if blank. If you're seeing 401s during calls to + * {@link OAuthProvider#retrieveRequestToken}, try setting this to true. + *

+ * + * @param enable + * true or false + */ + void setSendEmptyTokens(boolean enable); + + /** + * Signs the given HTTP request by writing an OAuth signature (and other + * required OAuth parameters) to it. Where these parameters are written + * depends on the current {@link SigningStrategy}. + * + * @param request + * the request to sign + * @return the request object passed as an argument + * @throws OAuthMessageSignerException + * @throws OAuthExpectationFailedException + * @throws OAuthCommunicationException + */ + HttpRequest sign(HttpRequest request) throws OAuthMessageSignerException, + OAuthExpectationFailedException, OAuthCommunicationException; + + /** + *

+ * Signs the given HTTP request by writing an OAuth signature (and other + * required OAuth parameters) to it. Where these parameters are written + * depends on the current {@link SigningStrategy}. + *

+ * This method accepts HTTP library specific request objects; the consumer + * implementation must ensure that only those request types are passed which + * it supports. + * + * @param request + * the request to sign + * @return the request object passed as an argument + * @throws OAuthMessageSignerException + * @throws OAuthExpectationFailedException + * @throws OAuthCommunicationException + */ + HttpRequest sign(Object request) throws OAuthMessageSignerException, + OAuthExpectationFailedException, OAuthCommunicationException; + + /** + * "Signs" the given URL by appending all OAuth parameters to it which are + * required for message signing. The assumed HTTP method is GET. + * Essentially, this is equivalent to signing an HTTP GET request, but it + * can be useful if your application requires clickable links to protected + * resources, i.e. when your application does not have access to the actual + * request that is being sent. + * + * @param url + * the input URL. May have query parameters. + * @return the input URL, with all necessary OAuth parameters attached as a + * query string. Existing query parameters are preserved. + * @throws OAuthMessageSignerException + * @throws OAuthExpectationFailedException + * @throws OAuthCommunicationException + */ + String sign(String url) throws OAuthMessageSignerException, + OAuthExpectationFailedException, OAuthCommunicationException; + + /** + * Sets the OAuth token and token secret used for message signing. + * + * @param token + * the token + * @param tokenSecret + * the token secret + */ + void setTokenWithSecret(String token, String tokenSecret); + + String getToken(); + + String getTokenSecret(); + + String getConsumerKey(); + + String getConsumerSecret(); + + /** + * Returns all parameters collected from the HTTP request during message + * signing (this means the return value may be NULL before a call to + * {@link #sign}), plus all required OAuth parameters that were added + * because the request didn't contain them beforehand. In other words, this + * is the exact set of parameters that were used for creating the message + * signature. + * + * @return the request parameters used for message signing + */ + HttpParameters getRequestParameters(); +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/OAuthException.java b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthException.java new file mode 100644 index 0000000..b15d139 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthException.java @@ -0,0 +1,17 @@ +package org.xbib.net.oauth; + +@SuppressWarnings("serial") +public abstract class OAuthException extends Exception { + + public OAuthException(String message) { + super(message); + } + + public OAuthException(Throwable cause) { + super(cause); + } + + public OAuthException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/OAuthExpectationFailedException.java b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthExpectationFailedException.java new file mode 100644 index 0000000..f9d53ff --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthExpectationFailedException.java @@ -0,0 +1,9 @@ +package org.xbib.net.oauth; + +@SuppressWarnings("serial") +public class OAuthExpectationFailedException extends OAuthException { + + public OAuthExpectationFailedException(String message) { + super(message); + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/OAuthMessageSignerException.java b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthMessageSignerException.java new file mode 100644 index 0000000..82797da --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthMessageSignerException.java @@ -0,0 +1,14 @@ +package org.xbib.net.oauth; + +@SuppressWarnings("serial") +public class OAuthMessageSignerException extends OAuthException { + + public OAuthMessageSignerException(String message) { + super(message); + } + + public OAuthMessageSignerException(Exception cause) { + super(cause); + } + +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/OAuthNotAuthorizedException.java b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthNotAuthorizedException.java new file mode 100644 index 0000000..9d46d02 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthNotAuthorizedException.java @@ -0,0 +1,24 @@ +package org.xbib.net.oauth; + +@SuppressWarnings("serial") +public class OAuthNotAuthorizedException extends OAuthException { + + private static final String ERROR = "Authorization failed (server replied with a 401). " + + "This can happen if the consumer key was not correct or " + + "the signatures did not match."; + + private String responseBody; + + public OAuthNotAuthorizedException() { + super(ERROR); + } + + public OAuthNotAuthorizedException(String responseBody) { + super(ERROR); + this.responseBody = responseBody; + } + + public String getResponseBody() { + return responseBody; + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/OAuthProvider.java b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthProvider.java new file mode 100644 index 0000000..4627ed0 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthProvider.java @@ -0,0 +1,206 @@ +package org.xbib.net.oauth; + +import org.xbib.net.http.HttpParameters; + +/** + *

+ * Supplies an interface that can be used to retrieve request and access elements + * from an OAuth 1.0(a) service provider. A provider object requires an + * {@link OAuthConsumer} to sign the token request message; after a token has + * been retrieved, the consumer is automatically updated with the token and the + * corresponding secret. + *

+ *

+ * To initiate the token exchange, create a new provider instance and configure + * it with the URLs the service provider exposes for requesting elements and + * resource authorization, e.g.: + *

+ * + *
+ * OAuthProvider provider = new DefaultOAuthProvider("http://twitter.com/oauth/request_token",
+ *         "http://twitter.com/oauth/access_token", "http://twitter.com/oauth/authorize");
+ * 
+ *

+ * Depending on the HTTP library you use, you may need a different provider + * type, refer to the website documentation for how to do that. + *

+ *

+ * To receive a request token which the user must authorize, you invoke it using + * a consumer instance and a callback URL: + *

+ *

+ * + *

+ * String url = provider.retrieveRequestToken(consumer, "http://www.example.com/callback");
+ * 
+ * + *

+ *

+ * That url must be opened in a Web browser, where the user can grant access to + * the resources in question. If that succeeds, the service provider will + * redirect to the callback URL and append the blessed request token. + *

+ *

+ * That token must now be exchanged for an access token, as such: + *

+ *

+ * + *

+ * provider.retrieveAccessToken(consumer, nullOrVerifierCode);
+ * 
+ * + *

+ *

+ * where nullOrVerifierCode is either null if your provided a callback URL in + * the previous step, or the pin code issued by the service provider to the user + * if the request was out-of-band (cf. {@link OAuth#OUT_OF_BAND}. + *

+ *

+ * The consumer used during token handshakes is now ready for signing. + *

+ * + * @see OAuthProviderListener + */ +public interface OAuthProvider { + + /** + * Queries the service provider for a request token. + *

+ * Pre-conditions: the given {@link OAuthConsumer} must have a valid + * consumer key and consumer secret already set. + *

+ *

+ * Post-conditions: the given {@link OAuthConsumer} will have an + * unauthorized request token and token secret set. + *

+ * + * @param consumer + * the {@link OAuthConsumer} that should be used to sign the request + * @param callbackUrl + * Pass an actual URL if your app can receive callbacks and you want + * to get informed about the result of the authorization process. + * Pass OUT_OF_BAND if the service provider implements + * OAuth 1.0a and your app cannot receive callbacks. Pass null if the + * service provider implements OAuth 1.0 and your app cannot receive + * callbacks. Please note that some services (among them Twitter) + * will fail authorization if you pass a callback URL but register + * your application as a desktop app (which would only be able to + * handle OOB requests). + * @param customOAuthParams + * you can pass custom OAuth parameters here which will go directly + * into the signer, i.e. you don't have to put them into the request + * first. This is useful for pre-setting OAuth params for signing. + * Pass them sequentially in key/value order. + * @return The URL to which the user must be sent in order to authorize the + * consumer. It includes the unauthorized request token (and in the + * case of OAuth 1.0, the callback URL -- 1.0a clients send along + * with the token request). + * @throws OAuthMessageSignerException + * if signing the request failed + * @throws OAuthNotAuthorizedException + * if the service provider rejected the consumer + * @throws OAuthExpectationFailedException + * if required parameters were not correctly set by the consumer or + * service provider + * @throws OAuthCommunicationException + * if server communication failed + */ + String retrieveRequestToken(OAuthConsumer consumer, String callbackUrl, + String... customOAuthParams) throws OAuthMessageSignerException, + OAuthNotAuthorizedException, OAuthExpectationFailedException, + OAuthCommunicationException; + + /** + * Queries the service provider for an access token. + *

+ * Pre-conditions: the given {@link OAuthConsumer} must have a valid + * consumer key, consumer secret, authorized request token and token secret + * already set. + *

+ *

+ * Post-conditions: the given {@link OAuthConsumer} will have an + * access token and token secret set. + *

+ * + * @param consumer + * the {@link OAuthConsumer} that should be used to sign the request + * @param oauthVerifier + * NOTE: Only applies to service providers implementing OAuth + * 1.0a. Set to null if the service provider is still using OAuth + * 1.0. The verification code issued by the service provider + * after the the user has granted the consumer authorization. If the + * callback method provided in the previous step was + * OUT_OF_BAND, then you must ask the user for this + * value. If your app has received a callback, the verfication code + * was passed as part of that request instead. + * @param customOAuthParams + * you can pass custom OAuth parameters here which will go directly + * into the signer, i.e. you don't have to put them into the request + * first. This is useful for pre-setting OAuth params for signing. + * Pass them sequentially in key/value order. + * @throws OAuthMessageSignerException + * if signing the request failed + * @throws OAuthNotAuthorizedException + * if the service provider rejected the consumer + * @throws OAuthExpectationFailedException + * if required parameters were not correctly set by the consumer or + * service provider + * @throws OAuthCommunicationException + * if server communication failed + */ + void retrieveAccessToken(OAuthConsumer consumer, String oauthVerifier, + String... customOAuthParams) throws OAuthMessageSignerException, + OAuthNotAuthorizedException, OAuthExpectationFailedException, + OAuthCommunicationException; + + /** + * Any additional non-OAuth parameters returned in the response body of a + * token request can be obtained through this method. These parameters will + * be preserved until the next token request is issued. The return value is + * never null. + */ + HttpParameters getResponseParameters(); + + /** + * Subclasses must use this setter to preserve any non-OAuth query + * parameters contained in the server response. It's the caller's + * responsibility that any OAuth parameters be removed beforehand. + * + * @param parameters + * the map of query parameters served by the service provider in the + * token response + */ + void setResponseParameters(HttpParameters parameters); + + /** + * @param isOAuth10aProvider + * set to true if the service provider supports OAuth 1.0a. Note that + * you need only call this method if you reconstruct a provider + * object in between calls to retrieveRequestToken() and + * retrieveAccessToken() (i.e. if the object state isn't preserved). + * If instead those two methods are called on the same provider + * instance, this flag will be deducted automatically based on the + * server response during retrieveRequestToken(), so you can simply + * ignore this method. + */ + void setOAuth10a(boolean isOAuth10aProvider); + + /** + * @return true if the service provider supports OAuth 1.0a. Note that the + * value returned here is only meaningful after you have already + * performed the token handshake, otherwise there is no way to + * determine what version of the OAuth protocol the service provider + * implements. + */ + boolean isOAuth10a(); + + String getRequestTokenEndpointUrl(); + + String getAccessTokenEndpointUrl(); + + String getAuthorizationWebsiteUrl(); + + void setListener(OAuthProviderListener listener); + + void removeListener(OAuthProviderListener listener); +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/OAuthProviderListener.java b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthProviderListener.java new file mode 100644 index 0000000..26d3f61 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/OAuthProviderListener.java @@ -0,0 +1,47 @@ +package org.xbib.net.oauth; + +import org.xbib.net.http.HttpRequest; +import org.xbib.net.http.HttpResponse; + +/** + * Provides hooks into the token request handling procedure executed by + * {@link OAuthProvider}. + * + */ +public interface OAuthProviderListener { + + /** + * Called after the request has been created and default headers added, but + * before the request has been signed. + * + * @param request + * the request to be sent + * @throws Exception + */ + void prepareRequest(HttpRequest request) throws Exception; + + /** + * Called after the request has been signed, but before it's being sent. + * + * @param request + * the request to be sent + * @throws Exception + */ + void prepareSubmission(HttpRequest request) throws Exception; + + /** + * Called when the server response has been received. You can implement this + * to manually handle the response data. + * + * @param request + * the request that was sent + * @param response + * the response that was received + * @return returning true means you have handled the response, and the + * provider will return immediately. Return false to let the event + * propagate and let the provider execute its default response + * handling. + * @throws Exception + */ + boolean onResponseReceived(HttpRequest request, HttpResponse response) throws Exception; +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/sign/AuthorizationHeaderSigningStrategy.java b/net-oauth/src/main/java/org/xbib/net/oauth/sign/AuthorizationHeaderSigningStrategy.java new file mode 100644 index 0000000..49e146b --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/sign/AuthorizationHeaderSigningStrategy.java @@ -0,0 +1,42 @@ +package org.xbib.net.oauth.sign; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; +import org.xbib.net.oauth.OAuth; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import java.util.Iterator; + +/** + * Writes to the HTTP Authorization header field. + */ +public class AuthorizationHeaderSigningStrategy implements SigningStrategy { + + @Override + public String writeSignature(String signature, HttpRequest request, + HttpParameters requestParameters) throws MalformedInputException, UnmappableCharacterException { + StringBuilder sb = new StringBuilder(); + sb.append("OAuth "); + // add the realm parameter, if any + if (requestParameters.containsKey("realm")) { + sb.append(requestParameters.getAsHeaderElement("realm")); + sb.append(", "); + } + // add all (x_)oauth parameters + HttpParameters oauthParams = requestParameters.getOAuthParameters(); + oauthParams.put(OAuth.OAUTH_SIGNATURE, signature, true); + Iterator iterator = oauthParams.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + sb.append(oauthParams.getAsHeaderElement(key)); + if (iterator.hasNext()) { + sb.append(", "); + } + } + String header = sb.toString(); + request.setHeader(OAuth.HTTP_AUTHORIZATION_HEADER, header); + return header; + } + +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/sign/HmacSha1MessageSigner.java b/net-oauth/src/main/java/org/xbib/net/oauth/sign/HmacSha1MessageSigner.java new file mode 100644 index 0000000..b386559 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/sign/HmacSha1MessageSigner.java @@ -0,0 +1,45 @@ +package org.xbib.net.oauth.sign; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; +import org.xbib.net.oauth.OAuth; +import org.xbib.net.oauth.OAuthMessageSignerException; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import java.security.GeneralSecurityException; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +@SuppressWarnings("serial") +public class HmacSha1MessageSigner extends OAuthMessageSigner { + + private static final String MAC_NAME = "HmacSHA1"; + + @Override + public String getSignatureMethod() { + return "HMAC-SHA1"; + } + + @Override + public String sign(HttpRequest request, HttpParameters requestParams) + throws OAuthMessageSignerException { + try { + String keyString = OAuth.percentEncoder.encode(getConsumerSecret()) + '&' + + OAuth.percentEncoder.encode(getTokenSecret()); + byte[] keyBytes = keyString.getBytes(OAuth.ENCODING); + SecretKey key = new SecretKeySpec(keyBytes, MAC_NAME); + Mac mac = Mac.getInstance(MAC_NAME); + mac.init(key); + String sbs = new SignatureBaseString(request, requestParams).generate(); + byte[] text = sbs.getBytes(OAuth.ENCODING); + return base64Encode(mac.doFinal(text)).trim(); + } catch (GeneralSecurityException | UnsupportedEncodingException | + MalformedInputException | UnmappableCharacterException e) { + throw new OAuthMessageSignerException(e); + } + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/sign/HmacSha256MessageSigner.java b/net-oauth/src/main/java/org/xbib/net/oauth/sign/HmacSha256MessageSigner.java new file mode 100644 index 0000000..2d756f4 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/sign/HmacSha256MessageSigner.java @@ -0,0 +1,45 @@ +package org.xbib.net.oauth.sign; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; +import org.xbib.net.oauth.OAuth; +import org.xbib.net.oauth.OAuthMessageSignerException; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import java.security.GeneralSecurityException; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +@SuppressWarnings("serial") +public class HmacSha256MessageSigner extends OAuthMessageSigner { + + private static final String MAC_NAME = "HmacSHA256"; + + @Override + public String getSignatureMethod() { + return "HMAC-SHA256"; + } + + @Override + public String sign(HttpRequest request, HttpParameters requestParams) + throws OAuthMessageSignerException { + try { + String keyString = OAuth.percentEncoder.encode(getConsumerSecret()) + '&' + + OAuth.percentEncoder.encode(getTokenSecret()); + byte[] keyBytes = keyString.getBytes(OAuth.ENCODING); + SecretKey key = new SecretKeySpec(keyBytes, MAC_NAME); + Mac mac = Mac.getInstance(MAC_NAME); + mac.init(key); + String sbs = new SignatureBaseString(request, requestParams).generate(); + byte[] text = sbs.getBytes(OAuth.ENCODING); + return base64Encode(mac.doFinal(text)).trim(); + } catch (GeneralSecurityException | UnsupportedEncodingException | + MalformedInputException| UnmappableCharacterException e) { + throw new OAuthMessageSignerException(e); + } + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/sign/OAuthMessageSigner.java b/net-oauth/src/main/java/org/xbib/net/oauth/sign/OAuthMessageSigner.java new file mode 100644 index 0000000..35f8445 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/sign/OAuthMessageSigner.java @@ -0,0 +1,54 @@ +package org.xbib.net.oauth.sign; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; +import org.xbib.net.oauth.OAuthMessageSignerException; + +import java.util.Base64; + + +public abstract class OAuthMessageSigner { + + private Base64.Encoder base64Encoder; + + private Base64.Decoder base64Decoder; + + private String consumerSecret; + + private String tokenSecret; + + public OAuthMessageSigner() { + this.base64Encoder = Base64.getEncoder(); + this.base64Decoder = Base64.getDecoder(); + } + + public abstract String sign(HttpRequest request, HttpParameters requestParameters) + throws OAuthMessageSignerException; + + public abstract String getSignatureMethod(); + + public String getConsumerSecret() { + return consumerSecret; + } + + public String getTokenSecret() { + return tokenSecret; + } + + public void setConsumerSecret(String consumerSecret) { + this.consumerSecret = consumerSecret; + } + + public void setTokenSecret(String tokenSecret) { + this.tokenSecret = tokenSecret; + } + + protected byte[] decodeBase64(String s) { + return base64Decoder.decode(s.getBytes()); + } + + protected String base64Encode(byte[] b) { + return new String(base64Encoder.encode(b)); + } + +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/sign/PlainTextMessageSigner.java b/net-oauth/src/main/java/org/xbib/net/oauth/sign/PlainTextMessageSigner.java new file mode 100644 index 0000000..ef280c3 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/sign/PlainTextMessageSigner.java @@ -0,0 +1,29 @@ +package org.xbib.net.oauth.sign; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; +import org.xbib.net.oauth.OAuth; +import org.xbib.net.oauth.OAuthMessageSignerException; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; + +@SuppressWarnings("serial") +public class PlainTextMessageSigner extends OAuthMessageSigner { + + @Override + public String getSignatureMethod() { + return "PLAINTEXT"; + } + + @Override + public String sign(HttpRequest request, HttpParameters requestParams) + throws OAuthMessageSignerException { + try { + return OAuth.percentEncoder.encode(getConsumerSecret()) + '&' + + OAuth.percentEncoder.encode(getTokenSecret()); + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new OAuthMessageSignerException(e); + } + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/sign/QueryStringSigningStrategy.java b/net-oauth/src/main/java/org/xbib/net/oauth/sign/QueryStringSigningStrategy.java new file mode 100644 index 0000000..ddf0de2 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/sign/QueryStringSigningStrategy.java @@ -0,0 +1,41 @@ +package org.xbib.net.oauth.sign; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; +import org.xbib.net.oauth.OAuth; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import java.util.Iterator; + +/** + * Writes to a URL query string. Note that this currently ONLY works + * when signing a URL directly, not with HTTP request objects. That's + * because most HTTP request implementations do not allow the client to change + * the URL once the request has been instantiated, so there is no way to append + * parameters to it. + */ +public class QueryStringSigningStrategy implements SigningStrategy { + + @Override + public String writeSignature(String signature, HttpRequest request, + HttpParameters requestParameters) + throws MalformedInputException, UnmappableCharacterException { + // add all (x_)oauth parameters + HttpParameters oauthParams = requestParameters.getOAuthParameters(); + oauthParams.put(OAuth.OAUTH_SIGNATURE, signature, true); + Iterator iterator = oauthParams.keySet().iterator(); + // add the first query parameter (we always have at least the signature) + String firstKey = iterator.next(); + StringBuilder sb = new StringBuilder(OAuth.addQueryString(request.getRequestUrl(), + oauthParams.getAsQueryString(firstKey))); + while (iterator.hasNext()) { + sb.append("&"); + String key = iterator.next(); + sb.append(oauthParams.getAsQueryString(key)); + } + String signedUrl = sb.toString(); + request.setRequestUrl(signedUrl); + return signedUrl; + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/sign/SignatureBaseString.java b/net-oauth/src/main/java/org/xbib/net/oauth/sign/SignatureBaseString.java new file mode 100644 index 0000000..fa64c3b --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/sign/SignatureBaseString.java @@ -0,0 +1,96 @@ +package org.xbib.net.oauth.sign; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; +import org.xbib.net.oauth.OAuth; +import org.xbib.net.oauth.OAuthMessageSignerException; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; + +public class SignatureBaseString { + + private final HttpRequest request; + + private final HttpParameters requestParameters; + + /** + * Constructs a new instance that will operate on the given request + * object and parameter set. + * + * @param request the HTTP request + * @param requestParameters the set of request parameters from the Authorization header, query + * string and form body + */ + public SignatureBaseString(HttpRequest request, HttpParameters requestParameters) { + this.request = request; + this.requestParameters = requestParameters; + } + + /** + * Builds the signature base string from the data this instance was + * configured with. + * + * @return the signature base string + * @throws OAuthMessageSignerException + */ + public String generate() throws OAuthMessageSignerException { + try { + String normalizedUrl = normalizeRequestUrl(); + String normalizedParams = normalizeRequestParameters(); + return request.getMethod() + '&' + OAuth.percentEncoder.encode(normalizedUrl) + '&' + + OAuth.percentEncoder.encode(normalizedParams); + } catch (URISyntaxException | IOException e) { + throw new OAuthMessageSignerException(e); + } + } + + public String normalizeRequestUrl() throws URISyntaxException { + URI uri = new URI(request.getRequestUrl()); + String scheme = uri.getScheme().toLowerCase(); + String authority = uri.getAuthority().toLowerCase(); + boolean dropPort = (scheme.equals("http") && uri.getPort() == 80) + || (scheme.equals("https") && uri.getPort() == 443); + if (dropPort) { + // find the last : in the authority + int index = authority.lastIndexOf(":"); + if (index >= 0) { + authority = authority.substring(0, index); + } + } + String path = uri.getRawPath(); + if (path == null || path.length() <= 0) { + path = "/"; // conforms to RFC 2616 section 3.2.2 + } + // we know that there is no query and no fragment here. + return scheme + "://" + authority + path; + } + + /** + * Normalizes the set of request parameters this instance was configured + * with, as per OAuth spec section 9.1.1. + * + * @return the normalized params string + * @throws IOException + */ + public String normalizeRequestParameters() throws IOException { + if (requestParameters == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + Iterator iter = requestParameters.keySet().iterator(); + for (int i = 0; iter.hasNext(); i++) { + String param = iter.next(); + if (OAuth.OAUTH_SIGNATURE.equals(param) || "realm".equals(param)) { + continue; + } + if (i > 0) { + sb.append("&"); + } + sb.append(requestParameters.getAsQueryString(param, false)); + } + return sb.toString(); + } +} diff --git a/net-oauth/src/main/java/org/xbib/net/oauth/sign/SigningStrategy.java b/net-oauth/src/main/java/org/xbib/net/oauth/sign/SigningStrategy.java new file mode 100644 index 0000000..60e39b0 --- /dev/null +++ b/net-oauth/src/main/java/org/xbib/net/oauth/sign/SigningStrategy.java @@ -0,0 +1,37 @@ +package org.xbib.net.oauth.sign; + +import org.xbib.net.http.HttpParameters; +import org.xbib.net.http.HttpRequest; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; + +/** + *

+ * Defines how an OAuth signature string is written to a request. + *

+ *

+ * Unlike {@link OAuthMessageSigner}, which is concerned with how to + * generate a signature, this class is concered with where to write it + * (e.g. HTTP header or query string). + *

+ */ +public interface SigningStrategy { + + /** + * Writes an OAuth signature and all remaining required parameters to an + * HTTP message. + * + * @param signature + * the signature to write + * @param request + * the request to sign + * @param requestParameters + * the request parameters + * @return whatever has been written to the request, e.g. an Authorization + * header field + */ + String writeSignature(String signature, HttpRequest request, HttpParameters requestParameters) + throws MalformedInputException, UnmappableCharacterException; + +} diff --git a/net-path/build.gradle b/net-path/build.gradle new file mode 100644 index 0000000..3de5ef5 --- /dev/null +++ b/net-path/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':net') +} diff --git a/net-path/src/main/java/module-info.java b/net-path/src/main/java/module-info.java new file mode 100644 index 0000000..8d954a2 --- /dev/null +++ b/net-path/src/main/java/module-info.java @@ -0,0 +1,10 @@ +module org.xbib.net.path { + exports org.xbib.net.path; + exports org.xbib.net.path.simple; + exports org.xbib.net.path.spring; + exports org.xbib.net.path.spring.element; + exports org.xbib.net.path.spring.util; + exports org.xbib.net.path.structure; + requires transitive org.xbib.net; + requires java.logging; +} diff --git a/net-path/src/main/java/org/xbib/net/path/PathDecoder.java b/net-path/src/main/java/org/xbib/net/path/PathDecoder.java new file mode 100644 index 0000000..ed563a1 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/PathDecoder.java @@ -0,0 +1,48 @@ +package org.xbib.net.path; + +import org.xbib.net.Parameter; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.PathNormalizer; + +public class PathDecoder { + + private final String path; + + private final String query; + + private final ParameterBuilder params; + + public PathDecoder(String pathAndQuery) { + this(pathAndQuery, null); + } + + public PathDecoder(String pathAndQuery, String queryString) { + int pos = pathAndQuery.indexOf('?'); + String path = pos > 0 ? pathAndQuery.substring(0, pos) : pathAndQuery; + this.query = pos > 0 ? pathAndQuery.substring(pos + 1) : null; + this.path = PathNormalizer.normalize(path); + this.params = Parameter.builder().enablePercentDeccoding(); + if (query != null) { + parse(query); + } + if (queryString != null) { + parse(queryString); + } + } + + public void parse(String queryString) { + this.params.add(queryString); + } + + public String path() { + return path; + } + + public String query() { + return query; + } + + public Parameter getParameter() { + return params.build(); + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/PathResolver.java b/net-path/src/main/java/org/xbib/net/path/PathResolver.java new file mode 100644 index 0000000..b6a9aa5 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/PathResolver.java @@ -0,0 +1,31 @@ +package org.xbib.net.path; + +import org.xbib.net.Parameter; + +public interface PathResolver { + + void resolve(String method, String path, ResultListener listener); + + interface Builder { + + Builder add(String method, String path, T value); + + PathResolver build(); + } + + interface Result { + + T getValue(); + + Parameter getParameter(); + + String getMethod(); + } + + @FunctionalInterface + interface ResultListener { + + void onResult(Result result); + + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/package-info.java b/net-path/src/main/java/org/xbib/net/path/package-info.java new file mode 100644 index 0000000..ee10fe8 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL path normalizing, decoding, matching, and routing. + */ +package org.xbib.net.path; diff --git a/net-path/src/main/java/org/xbib/net/path/simple/Path.java b/net-path/src/main/java/org/xbib/net/path/simple/Path.java new file mode 100644 index 0000000..f24a7ea --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/simple/Path.java @@ -0,0 +1,124 @@ +package org.xbib.net.path.simple; + +import java.util.Objects; +import java.util.regex.Pattern; + +public class Path { + + static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?}"); + + static final String CATCH_ALL = "**"; + + private final String pattern; + + private int uriVars; + + private int singleWildcards; + + private int doubleWildcards; + + private boolean catchAllPattern; + + private boolean prefixPattern; + + private Integer length; + + public Path(String pattern) { + this.pattern = pattern; + if (this.pattern != null) { + initCounters(); + this.catchAllPattern = this.pattern.equals(CATCH_ALL); + this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith(CATCH_ALL); + } + if (this.uriVars == 0) { + this.length = this.pattern != null ? this.pattern.length() : 0; + } + } + + public static Path of(String string) { + return new Path(string); + } + + public String getPattern() { + return pattern; + } + + public int getUriVars() { + return uriVars; + } + + public int getSingleWildcards() { + return singleWildcards; + } + + public int getDoubleWildcards() { + return doubleWildcards; + } + + public boolean isLeastSpecific() { + return this.pattern == null || this.catchAllPattern; + } + + public boolean isPrefixPattern() { + return this.prefixPattern; + } + + public int getTotalCount() { + return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); + } + + public int getLength() { + if (length == null) { + length = VARIABLE_PATTERN.matcher(pattern).replaceAll("#").length(); + } + return length; + } + + public boolean isCatchAllPattern() { + return catchAllPattern; + } + + public boolean isWildCard() { + return singleWildcards > 0 || doubleWildcards > 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Path that = (Path) o; + return uriVars == that.uriVars && singleWildcards == that.singleWildcards && doubleWildcards == that.doubleWildcards && catchAllPattern == that.catchAllPattern && prefixPattern == that.prefixPattern && Objects.equals(pattern, that.pattern) && Objects.equals(length, that.length); + } + + @Override + public int hashCode() { + return Objects.hash(pattern, uriVars, singleWildcards, doubleWildcards, catchAllPattern, prefixPattern, length); + } + + private void initCounters() { + int pos = 0; + int len = this.pattern.length(); + while (pos < len) { + if (this.pattern.charAt(pos) == '{') { + this.uriVars++; + pos++; + } else if (this.pattern.charAt(pos) == '*') { + if (pos + 1 < len && this.pattern.charAt(pos + 1) == '*') { + this.doubleWildcards++; + pos += 2; + } else if (pos > 0 && !(this.pattern.charAt(pos - 1) == '.' && this.pattern.charAt(pos) == '*')) { + this.singleWildcards++; + pos++; + } else { + pos++; + } + } else { + pos++; + } + } + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/simple/PathComparator.java b/net-path/src/main/java/org/xbib/net/path/simple/PathComparator.java new file mode 100644 index 0000000..93b704e --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/simple/PathComparator.java @@ -0,0 +1,68 @@ +package org.xbib.net.path.simple; + +import java.util.Comparator; + +/** + * Path pattern comparator. + */ +@SuppressWarnings("serial") +public class PathComparator implements Comparator { + + private final String path; + + public PathComparator(String path) { + this.path = path; + } + + @Override + public int compare(Path path1, Path path2) { + if (path1 == null) { + if (path2 == null) { + return 0; + } else { + return 1; + } + } + if (path2 == null) { + return -1; + } + if (path1.isLeastSpecific() && path2.isLeastSpecific()) { + return 0; + } else if (path1.isLeastSpecific()) { + return 1; + } else if (path2.isLeastSpecific()) { + return -1; + } + boolean pattern1EqualsPath = path1.getPattern().equals(path); + boolean pattern2EqualsPath = path2.getPattern().equals(path); + if (pattern1EqualsPath && pattern2EqualsPath) { + return 0; + } else if (pattern1EqualsPath) { + return -1; + } else if (pattern2EqualsPath) { + return 1; + } + if (path1.isPrefixPattern() && path2.getDoubleWildcards() == 0) { + return 1; + } else if (path2.isPrefixPattern() && path1.getDoubleWildcards() == 0) { + return -1; + } + if (path1.getTotalCount() != path2.getTotalCount()) { + return path1.getTotalCount() - path2.getTotalCount(); + } + if (path1.getLength() != path2.getLength()) { + return path2.getLength() - path1.getLength(); + } + if (path1.getSingleWildcards() < path2.getSingleWildcards()) { + return -1; + } else if (path2.getSingleWildcards() < path1.getSingleWildcards()) { + return 1; + } + if (path1.getUriVars() < path2.getUriVars()) { + return -1; + } else if (path2.getUriVars() < path1.getUriVars()) { + return 1; + } + return 0; + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/simple/PathMatcher.java b/net-path/src/main/java/org/xbib/net/path/simple/PathMatcher.java new file mode 100644 index 0000000..a95fd26 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/simple/PathMatcher.java @@ -0,0 +1,290 @@ +package org.xbib.net.path.simple; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; +import org.xbib.net.Parameter; +import org.xbib.net.ParameterBuilder; + +/** + * Path matcher. The methods of this class are threadsafe. + * + * This is based on org.springframework.util.AntPathMatcher + */ +public class PathMatcher { + + private static final String DEFAULT_PATH_SEPARATOR = "/"; + + private String pathSeparator; + + private String endsOnWildCard; + + private String endsOnDoubleWildCard; + + private boolean caseSensitive; + + private boolean trimTokens; + + public PathMatcher() { + this(DEFAULT_PATH_SEPARATOR); + } + + public PathMatcher(String pathSeparator) { + setPathSeparator(pathSeparator); + } + + public void setPathSeparator(String pathSeparator) { + this.pathSeparator = pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR; + this.endsOnWildCard = this.pathSeparator + "*"; + this.endsOnDoubleWildCard = this.pathSeparator + "**"; + this.caseSensitive = true; + this.trimTokens = true; + } + + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + public void setTrimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + } + + public Parameter extractUriTemplateVariables(String pattern, String path) { + ParameterBuilder queryParameters = Parameter.builder(); + if (!doMatch(pattern, path, true, queryParameters)) { + throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); + } + return queryParameters.build(); + } + + public PathComparator getPatternComparator(String path) { + return new PathComparator(path); + } + + public boolean match(String pattern, String path) { + return doMatch(pattern, path, true, null); + } + + public boolean matchStart(String pattern, String path) { + return doMatch(pattern, path, false, null); + } + + public String extractPathWithinPattern(String pattern, String path) { + List patternParts = tokenize(pattern, pathSeparator, trimTokens); + List pathParts = tokenize(path, pathSeparator, trimTokens); + StringBuilder sb = new StringBuilder(); + boolean pathStarted = false; + for (int segment = 0; segment < patternParts.size(); segment++) { + String patternPart = patternParts.get(segment); + if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { + while (segment < pathParts.size()) { + if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { + sb.append(pathSeparator); + } + sb.append(pathParts.get(segment)); + pathStarted = true; + segment++; + } + } + } + return sb.toString(); + } + + public String combine(String pattern1, String pattern2) { + if (hasNotText(pattern1) && hasNotText(pattern2)) { + return ""; + } + if (hasNotText(pattern1)) { + return pattern2; + } + if (hasNotText(pattern2)) { + return pattern1; + } + boolean pattern1ContainsUriVar = pattern1.indexOf('{') != -1; + if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { + return pattern2; + } + if (pattern1.endsWith(endsOnWildCard)) { + return concat(pattern1.substring(0, pattern1.length() - 2), pattern2); + } + if (pattern1.endsWith(endsOnDoubleWildCard)) { + return concat(pattern1, pattern2); + } + int starDotPos1 = pattern1.indexOf("*."); + if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { + return concat(pattern1, pattern2); + } + String ext1 = pattern1.substring(starDotPos1 + 1); + int dotPos2 = pattern2.indexOf('.'); + String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); + String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); + boolean ext1All = (ext1.equals(".*") || ext1.equals("")); + boolean ext2All = (ext2.equals(".*") || ext2.equals("")); + if (!ext1All && !ext2All) { + throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2); + } + String ext = ext1All ? ext2 : ext1; + return file2 + ext; + } + + private static boolean hasNotText(CharSequence str) { + if (str == null || str.length() == 0) { + return true; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return false; + } + } + return true; + } + + private String concat(String path1, String path2) { + boolean path1EndsWithSeparator = path1.endsWith(pathSeparator); + boolean path2StartsWithSeparator = path2.startsWith(pathSeparator); + if (path1EndsWithSeparator && path2StartsWithSeparator) { + return path1 + path2.substring(1); + } else if (path1EndsWithSeparator || path2StartsWithSeparator) { + return path1 + path2; + } else { + return path1 + pathSeparator + path2; + } + } + + private boolean doMatch(String pattern, String path, boolean fullMatch, ParameterBuilder queryParameters) { + if (path.startsWith(pathSeparator) != pattern.startsWith(pathSeparator)) { + return false; + } + List patternElements = tokenize(pattern, pathSeparator, trimTokens); + List pathElements = tokenize(path, pathSeparator, trimTokens); + int pattIdxStart = 0; + int pattIdxEnd = patternElements.size() - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathElements.size() - 1; + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String patternString = patternElements.get(pattIdxStart); + if ("**".equals(patternString)) { + break; + } + if (!matchStrings(patternString, pathElements.get(pathIdxStart), queryParameters)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + if (pathIdxStart > pathIdxEnd) { + if (pattIdxStart > pattIdxEnd) { + return pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd + && patternElements.get(pattIdxStart).equals("*") + && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!patternElements.get(i).equals("**")) { + return false; + } + } + return true; + } else if (pattIdxStart > pattIdxEnd) { + return false; + } else if (!fullMatch && "**".equals(patternElements.get(pattIdxStart))) { + return true; + } + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = patternElements.get(pattIdxEnd); + if (pattDir.equals("**")) { + break; + } + if (!matchStrings(pattDir, pathElements.get(pathIdxEnd), queryParameters)) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!patternElements.get(i).equals("**")) { + return false; + } + } + return true; + } + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (patternElements.get(i).equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + pattIdxStart++; + continue; + } + int patLength = patIdxTmp - pattIdxStart - 1; + int strLength = pathIdxEnd - pathIdxStart + 1; + int foundIdx = -1; + boolean strLoop = true; + while (strLoop) { + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = patternElements.get(pattIdxStart + j + 1); + String subStr = pathElements.get(pathIdxStart + i + j); + if (matchStrings(subPat, subStr, queryParameters)) { + strLoop = false; + break; + } + } + if (strLoop) { + foundIdx = pathIdxStart + i; + } else { + break; + } + } + } + if (foundIdx == -1) { + return false; + } + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!patternElements.get(i).equals("**")) { + return false; + } + } + return true; + } + + public List tokenize(String string) { + return tokenize(string, pathSeparator, trimTokens); + } + + private static List tokenize(String string, String delimiters, boolean trimTokens) { + List tokens = new ArrayList<>(); + if (string == null) { + return tokens; + } + StringTokenizer st = new StringTokenizer(string, delimiters); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (token.length() > 0) { + tokens.add(token); + } + } + return tokens; + } + + private boolean matchStrings(String patternString, String str, ParameterBuilder queryParameters) { + return new PathStringMatcher(patternString, caseSensitive).match(str, queryParameters); + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/simple/PathResolver.java b/net-path/src/main/java/org/xbib/net/path/simple/PathResolver.java new file mode 100644 index 0000000..5224553 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/simple/PathResolver.java @@ -0,0 +1,257 @@ +package org.xbib.net.path.simple; + +import org.xbib.net.Parameter; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.util.CharMatcher; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A simple resolver that maps pairs of method names and parameterized paths to arbitrary data. Each + * node in the tree is a path segment. For example, given a path "discovery/v1/apis", the data would + * be stored in the node path represented by "discovery" -> "v1" -> "apis". A path is + * considered parameterized if one or more segments is of the form "{name}". When a parameterized + * path is resolved, a map from parameter names to raw String values is returned as part of the + * result. Null values are not acceptable values. Parameter names can only contain + * alphanumeric characters or underscores, and cannot start with a numeric. + * @param type + */ +public class PathResolver implements org.xbib.net.path.PathResolver { + + private static final String PARAMETER_PATH_SEGMENT = "{}"; + + private static final Pattern PARAMETER_NAME_PATTERN = Pattern.compile("[a-zA-Z_][a-zA-Z_\\d]*"); + + private static final CharMatcher RESERVED_URL_CHARS = CharMatcher.anyOf(":/?#[]{}"); + + private final Builder builder; + + private final Map> children; + + private PathResolver(Builder builder) { + this.builder = builder; + this.children = new LinkedHashMap<>(); + for (Entry> entry : builder.subBuilders.entrySet()) { + children.put(entry.getKey(), new PathResolver<>(entry.getValue())); + } + } + + /** + * Attempts to resolve a path. Resolution prefers literal paths over path parameters. The result + * includes the object to which the path mapped, as well a map from parameter names to + * values. If the path cannot be resolved, null is returned. + * @param method method + * @param path path + * @param resultListener result listener + */ + @Override + public void resolve(String method, String path, ResultListener resultListener) { + Objects.requireNonNull(method, "method"); + Objects.requireNonNull(path, "path"); + resolve(method, builder.pathMatcher.tokenize(path), 0, new ArrayList<>(), resultListener); + } + + @Override + public String toString() { + return "PathResolver[" + "builder=" + builder + ", children=" + children + ']'; + } + + private void resolve(String method, + List pathSegments, + int index, + List parameters, + ResultListener resultListener) { + if (index < pathSegments.size()) { + String segment = pathSegments.get(index); + PathResolver child = children.get(segment); + if (child != null) { + child.resolve(method, pathSegments, index + 1, parameters, resultListener); + } + child = children.get(PARAMETER_PATH_SEGMENT); + if (child != null) { + parameters.add(segment); + child.resolve(method, pathSegments, index + 1, parameters, resultListener); + parameters.remove(parameters.size() - 1); + } + } else if (builder.infoMap.containsKey(method)) { + Info info = builder.infoMap.get(method); + ParameterBuilder parameterBuilder = Parameter.builder().domain("PATH").enableSort(); + for (int i = 0; i < info.parameterNames.size(); i++) { + parameterBuilder.add(info.parameterNames.get(i), parameters.get(i)); + } + resultListener.onResult(new Result<>(info.value, parameterBuilder.build(), info.method)); + } + } + + /** + * The resulting information for a successful path resolution, which includes the value to which + * the path maps, as well as the raw string values of all path parameters. + * @param type + */ + public static class Result implements org.xbib.net.path.PathResolver.Result { + + private final T value; + + private final Parameter parameter; + + private final String method; + + Result(T value, Parameter parameter, String method) { + this.value = value; + this.parameter = parameter; + this.method = method; + } + + @Override + public T getValue() { + return value; + } + + @Override + public Parameter getParameter() { + return parameter; + } + + @Override + public String getMethod() { + return method; + } + } + + /** + * Returns a new, path conflict validating {@link Builder}. + * + * @param the type that the resolver will be storing + * @return the builder + */ + public static Builder builder() { + return new Builder<>(true); + } + + /** + * Returns a new {@link Builder}. + * + * @param throwOnConflict whether or not to throw an exception on path conflicts + * @param the type that the resolver will be storing + * @return the builder + */ + public static Builder builder(boolean throwOnConflict) { + return new Builder<>(throwOnConflict); + } + + /** + * A builder for creating a {@link PathResolver}. + * @param type + */ + public static class Builder implements org.xbib.net.path.PathResolver.Builder { + + private final Map> subBuilders; + + private final Map> infoMap; + + private final boolean throwOnConflict; + + private final PathMatcher pathMatcher; + + Builder(boolean throwOnConflict) { + this.throwOnConflict = throwOnConflict; + this.subBuilders = new LinkedHashMap<>(); + this.infoMap = new LinkedHashMap<>(); + this.pathMatcher = new PathMatcher(); + } + + /** + * Adds a path. + * + * @param method the method + * @param path the path + * @param value the value + * @return the builder + * @throws IllegalArgumentException if the path cannot be added + * @throws NullPointerException if either path or value are null + */ + @Override + public Builder add(String method, String path, T value) { + Objects.requireNonNull(method, "method"); + Objects.requireNonNull(path, "path"); + Objects.requireNonNull(value, "value"); + internalAdd(method, path, pathMatcher.tokenize(path).iterator(), value, new ArrayList<>()); + return this; + } + + @Override + public PathResolver build() { + return new PathResolver<>(this); + } + + private void internalAdd(String method, + String path, + Iterator pathSegments, + T value, + List parameterNames) { + if (pathSegments.hasNext()) { + String segment = pathSegments.next(); + if (segment.startsWith("{")) { + if (segment.endsWith("}")) { + parameterNames.add(getAndCheckParameterName(segment)); + getOrCreateSubBuilder(PARAMETER_PATH_SEGMENT) + .internalAdd(method, path, pathSegments, value, parameterNames); + } else { + throw new IllegalArgumentException("missing closed brace } in " + segment); + } + } else if (segment.contains(Path.CATCH_ALL)) { + getOrCreateSubBuilder(Path.CATCH_ALL).internalAdd(method, path, pathSegments, value, parameterNames); + } else { + if (RESERVED_URL_CHARS.matchesAnyOf(segment)) { + throw new IllegalArgumentException("contains reserved URL character in " + segment); + } + getOrCreateSubBuilder(segment).internalAdd(method, path, pathSegments, value, parameterNames); + } + } else { + boolean pathExists = infoMap.containsKey(method); + if (pathExists && throwOnConflict) { + throw new IllegalArgumentException("path '" + path + "' is already mapped"); + } + infoMap.put(method, new Info<>(parameterNames, value, method)); + } + } + + private String getAndCheckParameterName(String segment) { + String name = segment.substring(1, segment.length() - 1); + Matcher matcher = PARAMETER_NAME_PATTERN.matcher(name); + if (!matcher.matches()) { + throw new IllegalArgumentException(String.format("'%s' not a valid path parameter name", name)); + } + return name; + } + + private Builder getOrCreateSubBuilder(String segment) { + Builder subBuilder = subBuilders.get(segment); + if (subBuilder == null) { + subBuilder = builder(throwOnConflict); + subBuilders.put(segment, subBuilder); + } + return subBuilder; + } + } + + private static class Info { + private final T value; + private final List parameterNames; + private final String method; + + Info(List parameterNames, T value, String method) { + this.parameterNames = parameterNames; + this.value = value; + this.method = method; + } + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/simple/PathStringMatcher.java b/net-path/src/main/java/org/xbib/net/path/simple/PathStringMatcher.java new file mode 100644 index 0000000..7c18c83 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/simple/PathStringMatcher.java @@ -0,0 +1,89 @@ +package org.xbib.net.path.simple; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.xbib.net.ParameterBuilder; + +public class PathStringMatcher { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; + + private final String patternString; + + private final List variableNames = new ArrayList<>(); + + private final Pattern pattern; + + public PathStringMatcher(String patternString, boolean caseSensitive) { + this.patternString = patternString; + StringBuilder sb = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(patternString); + int start = 0; + while (matcher.find()) { + sb.append(quote(patternString, start, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + sb.append('.'); + } else if ("*".equals(match)) { + sb.append(".*"); + } else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + sb.append(DEFAULT_VARIABLE_PATTERN); + this.variableNames.add(matcher.group(1)); + } else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + sb.append('(').append(variablePattern).append(')'); + String variableName = match.substring(1, colonIdx); + this.variableNames.add(variableName); + } + } + start = matcher.end(); + } + sb.append(quote(patternString, start, patternString.length())); + this.pattern = caseSensitive ? Pattern.compile(sb.toString()) : + Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE); + } + + public String getPatternString() { + return patternString; + } + + public List getVariableNames() { + return variableNames; + } + + public Pattern getPattern() { + return pattern; + } + + public boolean match(String string, ParameterBuilder queryParameters) { + Matcher matcher = pattern.matcher(string); + if (matcher.matches()) { + if (queryParameters != null) { + if (variableNames.size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = variableNames.get(i - 1); + String value = matcher.group(i); + queryParameters.add(name, value); + } + } + return true; + } else { + return false; + } + } + + private static String quote(String s, int start, int end) { + return start == end ? "" : Pattern.quote(s.substring(start, end)); + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/DefaultPathContainer.java b/net-path/src/main/java/org/xbib/net/path/spring/DefaultPathContainer.java new file mode 100644 index 0000000..9c461ce --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/DefaultPathContainer.java @@ -0,0 +1,386 @@ +package org.xbib.net.path.spring; + +import org.xbib.net.path.spring.util.LinkedMultiValueMap; +import org.xbib.net.path.spring.util.MultiValueMap; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Default implementation of {@link PathContainer}. + */ +final class DefaultPathContainer implements PathContainer { + + private static final PathContainer EMPTY_PATH = new DefaultPathContainer("", Collections.emptyList()); + + private static final Map SEPARATORS = new HashMap<>(2); + + static { + SEPARATORS.put('/', new DefaultSeparator('/', "%2F")); + SEPARATORS.put('.', new DefaultSeparator('.', "%2E")); + } + + private final String path; + + private final List elements; + + + private DefaultPathContainer(String path, List elements) { + this.path = path; + this.elements = Collections.unmodifiableList(elements); + } + + + @Override + public String value() { + return this.path; + } + + @Override + public List elements() { + return this.elements; + } + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PathContainer)) { + return false; + } + return value().equals(((PathContainer) other).value()); + } + + @Override + public int hashCode() { + return this.path.hashCode(); + } + + @Override + public String toString() { + return value(); + } + + + static PathContainer createFromUrlPath(String path, Options options) { + if (path.isEmpty()) { + return EMPTY_PATH; + } + char separator = options.separator(); + DefaultSeparator separatorElement = SEPARATORS.get(separator); + if (separatorElement == null) { + throw new IllegalArgumentException("Unexpected separator: '" + separator + "'"); + } + List elements = new ArrayList<>(); + int begin; + if (path.charAt(0) == separator) { + begin = 1; + elements.add(separatorElement); + } + else { + begin = 0; + } + while (begin < path.length()) { + int end = path.indexOf(separator, begin); + String segment = (end != -1 ? path.substring(begin, end) : path.substring(begin)); + if (!segment.isEmpty()) { + elements.add(options.shouldDecodeAndParseSegments() ? + decodeAndParsePathSegment(segment) : + DefaultPathSegment.from(segment, separatorElement)); + } + if (end == -1) { + break; + } + elements.add(separatorElement); + begin = end + 1; + } + return new DefaultPathContainer(path, elements); + } + + private static PathSegment decodeAndParsePathSegment(String segment) { + Charset charset = StandardCharsets.UTF_8; + int index = segment.indexOf(';'); + if (index == -1) { + String valueToMatch = uriDecode(segment, charset); + return DefaultPathSegment.from(segment, valueToMatch); + } + else { + String valueToMatch = uriDecode(segment.substring(0, index), charset); + String pathParameterContent = segment.substring(index); + MultiValueMap parameters = parsePathParams(pathParameterContent, charset); + return DefaultPathSegment.from(segment, valueToMatch, parameters); + } + } + + private static MultiValueMap parsePathParams(String input, Charset charset) { + MultiValueMap result = new LinkedMultiValueMap<>(); + int begin = 1; + while (begin < input.length()) { + int end = input.indexOf(';', begin); + String param = (end != -1 ? input.substring(begin, end) : input.substring(begin)); + parsePathParamValues(param, charset, result); + if (end == -1) { + break; + } + begin = end + 1; + } + return result; + } + + private static void parsePathParamValues(String input, Charset charset, MultiValueMap output) { + if (hasText(input)) { + int index = input.indexOf('='); + if (index != -1) { + String name = input.substring(0, index); + name = uriDecode(name, charset); + if (hasText(name)) { + String value = input.substring(index + 1); + for (String v : commaDelimitedListToStringArray(value)) { + output.add(name, uriDecode(v, charset)); + } + } + } + else { + String name = uriDecode(input, charset); + if (hasText(name)) { + output.add(input, ""); + } + } + } + } + + public static boolean hasText(String str) { + return (str != null && !str.isEmpty() && containsText(str)); + } + + private static boolean containsText(CharSequence str) { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + public static List commaDelimitedListToStringArray(String str) { + return delimitedListToStringArray(str, ","); + } + + private static List delimitedListToStringArray(String str, String delimiter) { + return delimitedListToStringArray(str, delimiter, null); + } + + private static List delimitedListToStringArray(String str, String delimiter, String charsToDelete) { + + if (str == null) { + return List.of(); + } + if (delimiter == null) { + return List.of(str); + } + + List result = new ArrayList<>(); + if (delimiter.isEmpty()) { + for (int i = 0; i < str.length(); i++) { + result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); + } + } + else { + int pos = 0; + int delPos; + while ((delPos = str.indexOf(delimiter, pos)) != -1) { + result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); + pos = delPos + delimiter.length(); + } + if (str.length() > 0 && pos <= str.length()) { + // Add rest of String, but not in case of empty input. + result.add(deleteAny(str.substring(pos), charsToDelete)); + } + } + return result; + } + + private static String deleteAny(String inString, String charsToDelete) { + if (!hasLength(inString) || !hasLength(charsToDelete)) { + return inString; + } + + int lastCharIndex = 0; + char[] result = new char[inString.length()]; + for (int i = 0; i < inString.length(); i++) { + char c = inString.charAt(i); + if (charsToDelete.indexOf(c) == -1) { + result[lastCharIndex++] = c; + } + } + if (lastCharIndex == inString.length()) { + return inString; + } + return new String(result, 0, lastCharIndex); + } + + private static boolean hasLength(String str) { + return (str != null && !str.isEmpty()); + } + + /** + * Decode the given encoded URI component value. Based on the following rules: + *
    + *
  • Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, + * and {@code "0"} through {@code "9"} stay the same.
  • + *
  • Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
  • + *
  • A sequence "{@code %xy}" is interpreted as a hexadecimal representation of the character.
  • + *
+ * @param source the encoded String + * @param charset the character set + * @return the decoded value + * @throws IllegalArgumentException when the given source contains invalid encoded sequences + * @see java.net.URLDecoder#decode(String, String) + */ + private static String uriDecode(String source, Charset charset) { + int length = source.length(); + if (length == 0) { + return source; + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(length); + boolean changed = false; + for (int i = 0; i < length; i++) { + int ch = source.charAt(i); + if (ch == '%') { + if (i + 2 < length) { + char hex1 = source.charAt(i + 1); + char hex2 = source.charAt(i + 2); + int u = Character.digit(hex1, 16); + int l = Character.digit(hex2, 16); + if (u == -1 || l == -1) { + throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); + } + baos.write((char) ((u << 4) + l)); + i += 2; + changed = true; + } + else { + throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); + } + } + else { + baos.write(ch); + } + } + return changed ? baos.toString(charset) : source; + } + + static PathContainer subPath(PathContainer container, int fromIndex, int toIndex) { + List elements = container.elements(); + if (fromIndex == 0 && toIndex == elements.size()) { + return container; + } + if (fromIndex == toIndex) { + return EMPTY_PATH; + } + + //Assert.isTrue(fromIndex >= 0 && fromIndex < elements.size(), () -> "Invalid fromIndex: " + fromIndex); + //Assert.isTrue(toIndex >= 0 && toIndex <= elements.size(), () -> "Invalid toIndex: " + toIndex); + //Assert.isTrue(fromIndex < toIndex, () -> "fromIndex: " + fromIndex + " should be < toIndex " + toIndex); + + List subList = elements.subList(fromIndex, toIndex); + String path = subList.stream().map(Element::value).collect(Collectors.joining("")); + return new DefaultPathContainer(path, subList); + } + + + private static final class DefaultPathSegment implements PathSegment { + + private static final MultiValueMap EMPTY_PARAMS = + new LinkedMultiValueMap<>(); + + private final String value; + + private final String valueToMatch; + + private final MultiValueMap parameters; + + /** + * Factory for segments without decoding and parsing. + */ + static DefaultPathSegment from(String value, DefaultSeparator separator) { + String valueToMatch = value.contains(separator.encodedSequence()) ? + value.replaceAll(separator.encodedSequence(), separator.value()) : value; + return from(value, valueToMatch); + } + + /** + * Factory for decoded and parsed segments. + */ + static DefaultPathSegment from(String value, String valueToMatch) { + return new DefaultPathSegment(value, valueToMatch, EMPTY_PARAMS); + } + + /** + * Factory for decoded and parsed segments. + */ + static DefaultPathSegment from(String value, String valueToMatch, MultiValueMap params) { + return new DefaultPathSegment(value, valueToMatch, params); + } + + private DefaultPathSegment(String value, String valueToMatch, MultiValueMap params) { + this.value = value; + this.valueToMatch = valueToMatch; + this.parameters = params; + } + + + @Override + public String value() { + return this.value; + } + + @Override + public String valueToMatch() { + return this.valueToMatch; + } + + @Override + public char[] valueToMatchAsChars() { + return this.valueToMatch.toCharArray(); + } + + @Override + public MultiValueMap parameters() { + return this.parameters; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PathSegment)) { + return false; + } + return value().equals(((PathSegment) other).value()); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return "[value='" + this.value + "']"; + } + } + +} + diff --git a/net-path/src/main/java/org/xbib/net/path/spring/DefaultSeparator.java b/net-path/src/main/java/org/xbib/net/path/spring/DefaultSeparator.java new file mode 100644 index 0000000..4ce6494 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/DefaultSeparator.java @@ -0,0 +1,22 @@ +package org.xbib.net.path.spring; + +class DefaultSeparator implements PathContainer.Separator { + + private final String separator; + + private final String encodedSequence; + + DefaultSeparator(char separator, String encodedSequence) { + this.separator = String.valueOf(separator); + this.encodedSequence = encodedSequence; + } + + @Override + public String value() { + return this.separator; + } + + public String encodedSequence() { + return this.encodedSequence; + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/MatchingContext.java b/net-path/src/main/java/org/xbib/net/path/spring/MatchingContext.java new file mode 100644 index 0000000..d2f7f77 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/MatchingContext.java @@ -0,0 +1,98 @@ +package org.xbib.net.path.spring; + +import org.xbib.net.path.spring.util.MultiValueMap; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Encapsulates context when attempting a match. Includes some fixed state like the + * candidate currently being considered for a match but also some accumulators for + * extracted variables. + */ +public class MatchingContext { + + private final PathPattern pathPattern; + + public final PathContainer candidate; + + public final List pathElements; + + public final int pathLength; + + private Map extractedUriVariables; + + private Map> extractedMatrixVariables; + + public boolean extractingVariables; + + public boolean determineRemainingPath = false; + + // if determineRemaining is true, this is set to the position in + // the candidate where the pattern finished matching - i.e. it + // points to the remaining path that wasn't consumed + public int remainingPathIndex; + + public MatchingContext(PathPattern pathPattern, PathContainer pathContainer, boolean extractVariables) { + this.pathPattern = pathPattern; + this.candidate = pathContainer; + this.pathElements = pathContainer.elements(); + this.pathLength = this.pathElements.size(); + this.extractingVariables = extractVariables; + } + + public void setMatchAllowExtraPath() { + this.determineRemainingPath = true; + } + + public boolean isMatchOptionalTrailingSeparator() { + return pathPattern.matchOptionalTrailingSeparator; + } + + public void set(String key, String value, MultiValueMap parameters) { + if (this.extractedUriVariables == null) { + this.extractedUriVariables = new HashMap<>(); + } + this.extractedUriVariables.put(key, value); + if (!parameters.isEmpty()) { + if (this.extractedMatrixVariables == null) { + this.extractedMatrixVariables = new HashMap<>(); + } + this.extractedMatrixVariables.put(key, parameters); + } + } + + public PathMatchInfo getPathMatchResult() { + if (this.extractedUriVariables == null) { + return PathMatchInfo.EMPTY; + } else { + return new PathMatchInfo(this.extractedUriVariables, this.extractedMatrixVariables); + } + } + + /** + * Return if element at specified index is a separator. + * + * @param pathIndex possible index of a separator + * @return {@code true} if element is a separator + */ + public boolean isSeparator(int pathIndex) { + return this.pathElements.get(pathIndex) instanceof PathContainer.Separator; + } + + /** + * Return the decoded value of the specified element. + * + * @param pathIndex path element index + * @return the decoded value + */ + public String pathElementValue(int pathIndex) { + PathContainer.Element element = (pathIndex < this.pathLength) ? this.pathElements.get(pathIndex) : null; + if (element instanceof PathContainer.PathSegment) { + PathContainer.PathSegment pathSegment = (PathContainer.PathSegment) element; + return pathSegment.valueToMatch(); + } + return ""; + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/PathContainer.java b/net-path/src/main/java/org/xbib/net/path/spring/PathContainer.java new file mode 100644 index 0000000..6c2a9b1 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/PathContainer.java @@ -0,0 +1,157 @@ +package org.xbib.net.path.spring; + +import org.xbib.net.path.spring.util.MultiValueMap; + +import java.util.List; + +/** + * Structured representation of a URI path parsed via {@link #parsePath(String)} + * into a sequence of {@link Separator} and {@link PathSegment} elements. + * + * Each {@link PathSegment} exposes its content in decoded form and with path + * parameters removed. This makes it safe to match one path segment at a time + * without the risk of decoded reserved characters altering the structure of + * the path. + * + */ +public interface PathContainer { + + /** + * The original path from which this instance was parsed. + */ + String value(); + + /** + * The contained path elements, either {@link Separator} or {@link PathSegment}. + */ + List elements(); + + /** + * Extract a sub-path from the given offset into the elements list. + * @param index the start element index (inclusive) + * @return the sub-path + */ + default PathContainer subPath(int index) { + return subPath(index, elements().size()); + } + + /** + * Extract a sub-path from the given start offset into the element list + * (inclusive) and to the end offset (exclusive). + * @param startIndex the start element index (inclusive) + * @param endIndex the end element index (exclusive) + * @return the sub-path + */ + default PathContainer subPath(int startIndex, int endIndex) { + return DefaultPathContainer.subPath(this, startIndex, endIndex); + } + + /** + * Parse the path value into a sequence of {@code "/"} {@link Separator Separator} + * and {@link PathSegment PathSegment} elements. + * @param path the encoded, raw path value to parse + * @return the parsed path + */ + static PathContainer parsePath(String path) { + return DefaultPathContainer.createFromUrlPath(path, Options.HTTP_PATH); + } + + /** + * Parse the path value into a sequence of {@link Separator Separator} and + * {@link PathSegment PathSegment} elements. + * @param path the encoded, raw path value to parse + * @param options to customize parsing + * @return the parsed path + */ + static PathContainer parsePath(String path, Options options) { + return DefaultPathContainer.createFromUrlPath(path, options); + } + + /** + * A path element, either separator or path segment. + */ + interface Element { + + /** + * The unmodified, original value of this element. + */ + String value(); + } + + /** + * Path separator element. + */ + interface Separator extends Element { + } + + /** + * Path segment element. + */ + interface PathSegment extends Element { + + /** + * Return the path segment value, decoded and sanitized, for path matching. + */ + String valueToMatch(); + + /** + * Expose {@link #valueToMatch()} as a character array. + */ + char[] valueToMatchAsChars(); + + /** + * Path parameters associated with this path segment. + * @return an unmodifiable map containing the parameters + */ + MultiValueMap parameters(); + } + + /** + * Options to customize parsing based on the type of input path. + */ + class Options { + + /** + * Options for HTTP URL paths. + * Separator '/' with URL decoding and parsing of path parameters. + */ + public final static Options HTTP_PATH = Options.create('/', true); + + /** + * Options for a message route. + * Separator '.' with neither URL decoding nor parsing of path parameters. + * Escape sequences for the separator character in segment values are still + * decoded. + */ + public final static Options MESSAGE_ROUTE = Options.create('.', false); + + private final char separator; + + private final boolean decodeAndParseSegments; + + private Options(char separator, boolean decodeAndParseSegments) { + this.separator = separator; + this.decodeAndParseSegments = decodeAndParseSegments; + } + + public char separator() { + return this.separator; + } + + public boolean shouldDecodeAndParseSegments() { + return this.decodeAndParseSegments; + } + + /** + * Create an {@link Options} instance with the given settings. + * @param separator the separator for parsing the path into segments; + * currently this must be slash or dot. + * @param decodeAndParseSegments whether to URL decode path segment + * values and parse path parameters. If set to false, only escape + * sequences for the separator char are decoded. + */ + public static Options create(char separator, boolean decodeAndParseSegments) { + return new Options(separator, decodeAndParseSegments); + } + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/PathMatchInfo.java b/net-path/src/main/java/org/xbib/net/path/spring/PathMatchInfo.java new file mode 100644 index 0000000..6a36036 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/PathMatchInfo.java @@ -0,0 +1,46 @@ +package org.xbib.net.path.spring; + +import org.xbib.net.path.spring.util.MultiValueMap; + +import java.util.Collections; +import java.util.Map; + +/** + * Holder for URI variables and path parameters (matrix variables) extracted + * based on the pattern for a given matched path. + */ +public class PathMatchInfo { + + static final PathMatchInfo EMPTY = new PathMatchInfo(Collections.emptyMap(), Collections.emptyMap()); + + private final Map uriVariables; + + private final Map> matrixVariables; + + public PathMatchInfo(Map uriVars, Map> matrixVars) { + this.uriVariables = Collections.unmodifiableMap(uriVars); + this.matrixVariables = (matrixVars != null ? + Collections.unmodifiableMap(matrixVars) : Collections.emptyMap()); + } + + /** + * Return the extracted URI variables. + */ + public Map getUriVariables() { + return this.uriVariables; + } + + /** + * Return maps of matrix variables per path segment, keyed off by URI + * variable name. + */ + public Map> getMatrixVariables() { + return this.matrixVariables; + } + + @Override + public String toString() { + return "PathMatchInfo[uriVariables=" + this.uriVariables + ", " + + "matrixVariables=" + this.matrixVariables + "]"; + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/PathPattern.java b/net-path/src/main/java/org/xbib/net/path/spring/PathPattern.java new file mode 100644 index 0000000..7301273 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/PathPattern.java @@ -0,0 +1,517 @@ +package org.xbib.net.path.spring; + +import org.xbib.net.path.spring.element.CaptureTheRestPathElement; +import org.xbib.net.path.spring.element.PathElement; +import org.xbib.net.path.spring.element.SeparatorPathElement; +import org.xbib.net.path.spring.element.WildcardPathElement; +import org.xbib.net.path.spring.element.WildcardTheRestPathElement; + +import java.util.Comparator; +import java.util.List; +import java.util.StringJoiner; + +/** + * Representation of a parsed path pattern. Includes a chain of path elements + * for fast matching and accumulates computed state for quick comparison of + * patterns. + * + *

{@code PathPattern} matches URL paths using the following rules:
+ *

    + *
  • {@code ?} matches one character
  • + *
  • {@code *} matches zero or more characters within a path segment
  • + *
  • {@code **} matches zero or more path segments until the end of the path
  • + *
  • {spring} matches a path segment and captures it as a variable named "spring"
  • + *
  • {spring:[a-z]+} matches the regexp {@code [a-z]+} as a path variable named "spring"
  • + *
  • {*spring} matches zero or more path segments until the end of the path + * and captures it as a variable named "spring"
  • + *
+ * + *

Note: In contrast to + * {@code org.springframework.util.AntPathMatcher}, {@code **} is supported only + * at the end of a pattern. For example {@code /pages/{**}} is valid but + * {@code /pages/{**}/details} is not. The same applies also to the capturing + * variant {*spring}. The aim is to eliminate ambiguity when + * comparing patterns for specificity. + * + *

Examples

+ *
    + *
  • {@code /pages/t?st.html} — matches {@code /pages/test.html} as well as + * {@code /pages/tXst.html} but not {@code /pages/toast.html}
  • + *
  • {@code /resources/*.png} — matches all {@code .png} files in the + * {@code resources} directory
  • + *
  • /resources/** — matches all files + * underneath the {@code /resources/} path, including {@code /resources/image.png} + * and {@code /resources/css/spring.css}
  • + *
  • /resources/{*path} — matches all files + * underneath the {@code /resources/}, as well as {@code /resources}, and captures + * their relative path in a variable named "path"; {@code /resources/image.png} + * will match with "path" → "/image.png", and {@code /resources/css/spring.css} + * will match with "path" → "/css/spring.css"
  • + *
  • /resources/{filename:\\w+}.dat will match {@code /resources/spring.dat} + * and assign the value {@code "spring"} to the {@code filename} variable
  • + *
+ * + * @see PathContainer + */ +public class PathPattern implements Comparable { + + private static final PathContainer EMPTY_PATH = PathContainer.parsePath(""); + + /** + * Comparator that sorts patterns by specificity as follows: + *
    + *
  1. Null instances are last. + *
  2. Catch-all patterns are last. + *
  3. If both patterns are catch-all, consider the length (longer wins). + *
  4. Compare wildcard and captured variable count (lower wins). + *
  5. Consider length (longer wins) + *
+ */ + public static final Comparator SPECIFICITY_COMPARATOR = + Comparator.nullsLast( + Comparator. + comparingInt(p -> p.isCatchAll() ? 1 : 0) + .thenComparingInt(p -> p.isCatchAll() ? scoreByNormalizedLength(p) : 0) + .thenComparingInt(PathPattern::getScore) + .thenComparingInt(PathPattern::scoreByNormalizedLength) + ); + + + /** The text of the parsed pattern. */ + private final String patternString; + + /** The parser used to construct this pattern. */ + private final PathPatternParser parser; + + /** The options to use to parse a pattern. */ + private final PathContainer.Options pathOptions; + + /** If this pattern has no trailing slash, allow candidates to include one and still match successfully. */ + final boolean matchOptionalTrailingSeparator; + + /** Will this match candidates in a case sensitive way? (case sensitivity at parse time). */ + private final boolean caseSensitive; + + /** First path element in the parsed chain of path elements for this pattern. */ + + private final PathElement head; + + /** How many variables are captured in this pattern. */ + private int capturedVariableCount; + + /** + * The normalized length is trying to measure the 'active' part of the pattern. It is computed + * by assuming all captured variables have a normalized length of 1. Effectively this means changing + * your variable name lengths isn't going to change the length of the active part of the pattern. + * Useful when comparing two patterns. + */ + private int normalizedLength; + + /** + * Does the pattern end with '<separator>'. + */ + private boolean endsWithSeparatorWildcard = false; + + /** + * Score is used to quickly compare patterns. Different pattern components are given different + * weights. A 'lower score' is more specific. Current weights: + *
    + *
  • Captured variables are worth 1 + *
  • Wildcard is worth 100 + *
+ */ + private int score; + + /** Does the pattern end with {*...}. */ + private boolean catchAll = false; + + public PathPattern(String patternText, PathPatternParser parser, PathElement head) { + this.patternString = patternText; + this.parser = parser; + this.pathOptions = parser.getPathOptions(); + this.matchOptionalTrailingSeparator = parser.isMatchOptionalTrailingSeparator(); + this.caseSensitive = parser.isCaseSensitive(); + this.head = head; + // Compute fields for fast comparison + PathElement elem = head; + while (elem != null) { + this.capturedVariableCount += elem.getCaptureCount(); + this.normalizedLength += elem.getNormalizedLength(); + this.score += elem.getScore(); + if (elem instanceof CaptureTheRestPathElement || elem instanceof WildcardTheRestPathElement) { + this.catchAll = true; + } + if (elem instanceof SeparatorPathElement && elem.next instanceof WildcardPathElement && elem.next.next == null) { + this.endsWithSeparatorWildcard = true; + } + elem = elem.next; + } + } + + /** + * Return the original String that was parsed to create this PathPattern. + */ + public String getPatternString() { + return this.patternString; + } + + /** + * Whether the pattern string contains pattern syntax that would require + * use of {@link #matches(PathContainer)}, or if it is a regular String that + * could be compared directly to others. + */ + public boolean hasPatternSyntax() { + return (this.score > 0 || this.catchAll || this.patternString.indexOf('?') != -1); + } + + /** + * Whether this pattern matches the given path. + * @param pathContainer the candidate path to attempt to match against + * @return {@code true} if the path matches this pattern + */ + public boolean matches(PathContainer pathContainer) { + if (this.head == null) { + return !hasLength(pathContainer) || + (this.matchOptionalTrailingSeparator && pathContainerIsJustSeparator(pathContainer)); + } + else if (!hasLength(pathContainer)) { + if (this.head instanceof WildcardTheRestPathElement || this.head instanceof CaptureTheRestPathElement) { + pathContainer = EMPTY_PATH; // Will allow CaptureTheRest to bind the variable to empty + } + else { + return false; + } + } + MatchingContext matchingContext = new MatchingContext(this, pathContainer, false); + return this.head.matches(0, matchingContext); + } + + /** + * Match this pattern to the given URI path and return extracted URI template + * variables as well as path parameters (matrix variables). + * @param pathContainer the candidate path to attempt to match against + * @return info object with the extracted variables, or {@code null} for no match + */ + public PathMatchInfo matchAndExtract(PathContainer pathContainer) { + if (this.head == null) { + return (hasLength(pathContainer) && + !(this.matchOptionalTrailingSeparator && pathContainerIsJustSeparator(pathContainer)) ? + null : PathMatchInfo.EMPTY); + } + else if (!hasLength(pathContainer)) { + if (this.head instanceof WildcardTheRestPathElement || this.head instanceof CaptureTheRestPathElement) { + pathContainer = EMPTY_PATH; // Will allow CaptureTheRest to bind the variable to empty + } + else { + return null; + } + } + MatchingContext matchingContext = new MatchingContext(this, pathContainer, true); + return this.head.matches(0, matchingContext) ? matchingContext.getPathMatchResult() : null; + } + + /** + * Match the beginning of the given path and return the remaining portion + * not covered by this pattern. This is useful for matching nested routes + * where the path is matched incrementally at each level. + * @param pathContainer the candidate path to attempt to match against + * @return info object with the match result or {@code null} for no match + */ + + public PathRemainingMatchInfo matchStartOfPath(PathContainer pathContainer) { + if (this.head == null) { + return new PathRemainingMatchInfo(EMPTY_PATH, pathContainer); + } + else if (!hasLength(pathContainer)) { + return null; + } + + MatchingContext matchingContext = new MatchingContext(this, pathContainer, true); + matchingContext.setMatchAllowExtraPath(); + boolean matches = this.head.matches(0, matchingContext); + if (!matches) { + return null; + } + else { + PathContainer pathMatched; + PathContainer pathRemaining; + if (matchingContext.remainingPathIndex == pathContainer.elements().size()) { + pathMatched = pathContainer; + pathRemaining = EMPTY_PATH; + } + else { + pathMatched = pathContainer.subPath(0, matchingContext.remainingPathIndex); + pathRemaining = pathContainer.subPath(matchingContext.remainingPathIndex); + } + return new PathRemainingMatchInfo(pathMatched, pathRemaining, matchingContext.getPathMatchResult()); + } + } + + /** + * Determine the pattern-mapped part for the given path. + *

For example:

    + *
  • '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} → ''
  • + *
  • '{@code /docs/*}' and '{@code /docs/cvs/commit}' → '{@code cvs/commit}'
  • + *
  • '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} → '{@code commit.html}'
  • + *
  • '{@code /docs/**}' and '{@code /docs/cvs/commit} → '{@code cvs/commit}'
  • + *
+ *

Notes: + *

    + *
  • Assumes that {@link #matches} returns {@code true} for + * the same path but does not enforce this. + *
  • Duplicate occurrences of separators within the returned result are removed + *
  • Leading and trailing separators are removed from the returned result + *
+ * @param path a path that matches this pattern + * @return the subset of the path that is matched by pattern or "" if none + * of it is matched by pattern elements + */ + public PathContainer extractPathWithinPattern(PathContainer path) { + List pathElements = path.elements(); + int pathElementsCount = pathElements.size(); + + int startIndex = 0; + // Find first path element that is not a separator or a literal (i.e. the first pattern based element) + PathElement elem = this.head; + while (elem != null) { + if (elem.getWildcardCount() != 0 || elem.getCaptureCount() != 0) { + break; + } + elem = elem.next; + startIndex++; + } + if (elem == null) { + // There is no pattern piece + return PathContainer.parsePath(""); + } + + // Skip leading separators that would be in the result + while (startIndex < pathElementsCount && (pathElements.get(startIndex) instanceof PathContainer.Separator)) { + startIndex++; + } + int endIndex = pathElements.size(); + // Skip trailing separators that would be in the result + while (endIndex > 0 && (pathElements.get(endIndex - 1) instanceof PathContainer.Separator)) { + endIndex--; + } + boolean multipleAdjacentSeparators = false; + for (int i = startIndex; i < (endIndex - 1); i++) { + if ((pathElements.get(i) instanceof PathContainer.Separator) && (pathElements.get(i+1) instanceof PathContainer.Separator)) { + multipleAdjacentSeparators=true; + break; + } + } + PathContainer resultPath = null; + if (multipleAdjacentSeparators) { + // Need to rebuild the path without the duplicate adjacent separators + StringBuilder sb = new StringBuilder(); + int i = startIndex; + while (i < endIndex) { + PathContainer.Element e = pathElements.get(i++); + sb.append(e.value()); + if (e instanceof PathContainer.Separator) { + while (i < endIndex && (pathElements.get(i) instanceof PathContainer.Separator)) { + i++; + } + } + } + resultPath = PathContainer.parsePath(sb.toString(), this.pathOptions); + } + else if (startIndex >= endIndex) { + resultPath = PathContainer.parsePath(""); + } + else { + resultPath = path.subPath(startIndex, endIndex); + } + return resultPath; + } + + /** + * Compare this pattern with a supplied pattern: return -1,0,+1 if this pattern + * is more specific, the same or less specific than the supplied pattern. + * The aim is to sort more specific patterns first. + */ + @Override + public int compareTo( PathPattern otherPattern) { + int result = SPECIFICITY_COMPARATOR.compare(this, otherPattern); + return (result == 0 && otherPattern != null ? + this.patternString.compareTo(otherPattern.patternString) : result); + } + + /** + * Combine this pattern with another. + */ + public PathPattern combine(PathPattern pattern2string) { + // If one of them is empty the result is the other. If both empty the result is "" + if (!hasLength(this.patternString)) { + if (!hasLength(pattern2string.patternString)) { + return this.parser.parse(""); + } + else { + return pattern2string; + } + } + else if (!hasLength(pattern2string.patternString)) { + return this; + } + + // /* + /hotel => /hotel + // /*.* + /*.html => /*.html + // However: + // /usr + /user => /usr/user + // /{foo} + /bar => /{foo}/bar + if (!this.patternString.equals(pattern2string.patternString) && this.capturedVariableCount == 0 && + matches(PathContainer.parsePath(pattern2string.patternString))) { + return pattern2string; + } + + // /hotels/* + /booking => /hotels/booking + // /hotels/* + booking => /hotels/booking + if (this.endsWithSeparatorWildcard) { + return this.parser.parse(concat( + this.patternString.substring(0, this.patternString.length() - 2), + pattern2string.patternString)); + } + + // /hotels + /booking => /hotels/booking + // /hotels + booking => /hotels/booking + int starDotPos1 = this.patternString.indexOf("*."); // Are there any file prefix/suffix things to consider? + if (this.capturedVariableCount != 0 || starDotPos1 == -1 || getSeparator() == '.') { + return this.parser.parse(concat(this.patternString, pattern2string.patternString)); + } + + // /*.html + /hotel => /hotel.html + // /*.html + /hotel.* => /hotel.html + String firstExtension = this.patternString.substring(starDotPos1 + 1); // looking for the first extension + String p2string = pattern2string.patternString; + int dotPos2 = p2string.indexOf('.'); + String file2 = (dotPos2 == -1 ? p2string : p2string.substring(0, dotPos2)); + String secondExtension = (dotPos2 == -1 ? "" : p2string.substring(dotPos2)); + boolean firstExtensionWild = (firstExtension.equals(".*") || firstExtension.isEmpty()); + boolean secondExtensionWild = (secondExtension.equals(".*") || secondExtension.isEmpty()); + if (!firstExtensionWild && !secondExtensionWild) { + throw new IllegalArgumentException( + "Cannot combine patterns: " + this.patternString + " and " + pattern2string); + } + return this.parser.parse(file2 + (firstExtensionWild ? secondExtension : firstExtension)); + } + + public static boolean hasLength(String str) { + return (str != null && !str.isEmpty()); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof PathPattern)) { + return false; + } + PathPattern otherPattern = (PathPattern) other; + return (this.patternString.equals(otherPattern.getPatternString()) && + getSeparator() == otherPattern.getSeparator() && + this.caseSensitive == otherPattern.caseSensitive); + } + + @Override + public int hashCode() { + return (this.patternString.hashCode() + getSeparator()) * 17 + (this.caseSensitive ? 1 : 0); + } + + @Override + public String toString() { + return this.patternString; + } + + int getScore() { + return this.score; + } + + boolean isCatchAll() { + return this.catchAll; + } + + /** + * The normalized length is trying to measure the 'active' part of the pattern. It is computed + * by assuming all capture variables have a normalized length of 1. Effectively this means changing + * your variable name lengths isn't going to change the length of the active part of the pattern. + * Useful when comparing two patterns. + */ + int getNormalizedLength() { + return this.normalizedLength; + } + + char getSeparator() { + return this.pathOptions.separator(); + } + + int getCapturedVariableCount() { + return this.capturedVariableCount; + } + + String toChainString() { + StringJoiner stringJoiner = new StringJoiner(" "); + PathElement pe = this.head; + while (pe != null) { + stringJoiner.add(pe.toString()); + pe = pe.next; + } + return stringJoiner.toString(); + } + + /** + * Return the string form of the pattern built from walking the path element chain. + * @return the string form of the pattern + */ + String computePatternString() { + StringBuilder sb = new StringBuilder(); + PathElement pe = this.head; + while (pe != null) { + sb.append(pe.getChars()); + pe = pe.next; + } + return sb.toString(); + } + + PathElement getHeadSection() { + return this.head; + } + + /** + * Join two paths together including a separator if necessary. + * Extraneous separators are removed (if the first path + * ends with one and the second path starts with one). + * @param path1 first path + * @param path2 second path + * @return joined path that may include separator if necessary + */ + private String concat(String path1, String path2) { + boolean path1EndsWithSeparator = (path1.charAt(path1.length() - 1) == getSeparator()); + boolean path2StartsWithSeparator = (path2.charAt(0) == getSeparator()); + if (path1EndsWithSeparator && path2StartsWithSeparator) { + return path1 + path2.substring(1); + } + else if (path1EndsWithSeparator || path2StartsWithSeparator) { + return path1 + path2; + } + else { + return path1 + getSeparator() + path2; + } + } + + /** + * Return if the container is not null and has more than zero elements. + * @param container a path container + * @return {@code true} has more than zero elements + */ + private boolean hasLength( PathContainer container) { + return container != null && container.elements().size() > 0; + } + + private static int scoreByNormalizedLength(PathPattern pattern) { + return -pattern.getNormalizedLength(); + } + + private boolean pathContainerIsJustSeparator(PathContainer pathContainer) { + return pathContainer.value().length() == 1 && + pathContainer.value().charAt(0) == getSeparator(); + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/PathPatternParser.java b/net-path/src/main/java/org/xbib/net/path/spring/PathPatternParser.java new file mode 100644 index 0000000..bc29975 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/PathPatternParser.java @@ -0,0 +1,505 @@ +package org.xbib.net.path.spring; + +import org.xbib.net.path.spring.element.CaptureTheRestPathElement; +import org.xbib.net.path.spring.element.CaptureVariablePathElement; +import org.xbib.net.path.spring.element.LiteralPathElement; +import org.xbib.net.path.spring.element.PathElement; +import org.xbib.net.path.spring.element.RegexPathElement; +import org.xbib.net.path.spring.element.SeparatorPathElement; +import org.xbib.net.path.spring.element.SingleCharWildcardedPathElement; +import org.xbib.net.path.spring.element.WildcardPathElement; +import org.xbib.net.path.spring.element.WildcardTheRestPathElement; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.PatternSyntaxException; + +/** + * Parser for URI path patterns producing {@link PathPattern} instances that can + * then be matched to requests. + * + *

The {@link PathPatternParser} and {@link PathPattern} are specifically + * designed for use with HTTP URL paths in web applications where a large number + * of URI path patterns, continuously matched against incoming requests, + * motivates the need for efficient matching. + * + *

For details of the path pattern syntax see {@link PathPattern}. + * + */ +public class PathPatternParser { + + /** + * Shared, read-only instance of {@code PathPatternParser}. Uses default settings: + *

    + *
  • {@code matchOptionalTrailingSeparator=true} + *
  • {@code caseSensitivetrue} + *
  • {@code pathOptions=PathContainer.Options.HTTP_PATH} + *
+ */ + public final static PathPatternParser defaultInstance = new PathPatternParser() { + + @Override + public void setMatchOptionalTrailingSeparator(boolean matchOptionalTrailingSeparator) { + raiseError(); + } + + @Override + public void setCaseSensitive(boolean caseSensitive) { + raiseError(); + } + + @Override + public void setPathOptions(PathContainer.Options pathOptions) { + raiseError(); + } + + private void raiseError() { + throw new UnsupportedOperationException("This is a read-only, shared instance that cannot be modified"); + } + }; + + private boolean matchOptionalTrailingSeparator = true; + + private boolean caseSensitive = true; + + private PathContainer.Options pathOptions = PathContainer.Options.HTTP_PATH; + + // The input data for parsing + private char[] pathPatternData = new char[0]; + + // The length of the input data + private int pathPatternLength; + + // Current parsing position + int pos; + + // How many ? characters in a particular path element + private int singleCharWildcardCount; + + // Is the path pattern using * characters in a particular path element + private boolean wildcard = false; + + // Is the construct {*...} being used in a particular path element + private boolean isCaptureTheRestVariable = false; + + // Has the parser entered a {...} variable capture block in a particular + // path element + private boolean insideVariableCapture = false; + + // How many variable captures are occurring in a particular path element + private int variableCaptureCount = 0; + + // Start of the most recent path element in a particular path element + private int pathElementStart; + + // Start of the most recent variable capture in a particular path element + private int variableCaptureStart; + + // Variables captures in this path pattern + private List capturedVariableNames; + + // The head of the path element chain currently being built + private PathElement headPE; + + // The most recently constructed path element in the chain + private PathElement currentPE; + + public PathPatternParser() { + } + + public boolean match(String pattern, String string) { + return parse(pattern).matches(PathContainer.parsePath(string)); + } + + /** + * Whether a {@link PathPattern} produced by this parser should + * automatically match request paths with a trailing slash. + * + * If set to {@code true} a {@code PathPattern} without a trailing slash + * will also match request paths with a trailing slash. If set to + * {@code false} a {@code PathPattern} will only match request paths with + * a trailing slash. + * + * The default is {@code true}. + */ + public void setMatchOptionalTrailingSeparator(boolean matchOptionalTrailingSeparator) { + this.matchOptionalTrailingSeparator = matchOptionalTrailingSeparator; + } + + /** + * Whether optional trailing slashing match is enabled. + */ + public boolean isMatchOptionalTrailingSeparator() { + return this.matchOptionalTrailingSeparator; + } + + /** + * Whether path pattern matching should be case-sensitive. + * + * The default is {@code true}. + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + /** + * Whether case-sensitive pattern matching is enabled. + */ + public boolean isCaseSensitive() { + return this.caseSensitive; + } + + /** + * Set options for parsing patterns. These should be the same as the + * options used to parse input paths. + * {@code org.springframework.http.server.PathContainer.Options#HTTP_PATH} + * is used by default. + */ + public void setPathOptions(PathContainer.Options pathOptions) { + this.pathOptions = pathOptions; + } + + /** + * Return the {@link #setPathOptions configured} pattern parsing options. + */ + public PathContainer.Options getPathOptions() { + return this.pathOptions; + } + + /** + * Process the path pattern content, a character at a time, breaking it into + * path elements around separator boundaries and verifying the structure at each + * stage. Produces a PathPattern object that can be used for fast matching + * against paths. + * @param pathPattern the input path pattern, e.g. /project/{name} + * @return a PathPattern for quickly matching paths against request paths + * @throws PatternParseException in case of parse errors + */ + public PathPattern parse(String pathPattern) throws PatternParseException { + this.pathPatternData = pathPattern.toCharArray(); + this.pathPatternLength = this.pathPatternData.length; + this.headPE = null; + this.currentPE = null; + this.capturedVariableNames = null; + this.pathElementStart = -1; + this.pos = 0; + resetPathElementState(); + + while (this.pos < this.pathPatternLength) { + char ch = this.pathPatternData[this.pos]; + char separator = this.getPathOptions().separator(); + if (ch == separator) { + if (this.pathElementStart != -1) { + pushPathElement(createPathElement()); + } + if (peekDoubleWildcard()) { + pushPathElement(new WildcardTheRestPathElement(this.pos, separator)); + this.pos += 2; + } + else { + pushPathElement(new SeparatorPathElement(this.pos, separator)); + } + } + else { + if (this.pathElementStart == -1) { + this.pathElementStart = this.pos; + } + if (ch == '?') { + this.singleCharWildcardCount++; + } + else if (ch == '{') { + if (this.insideVariableCapture) { + throw new PatternParseException(this.pos, this.pathPatternData, + PatternParseException.PatternMessage.ILLEGAL_NESTED_CAPTURE); + } + // If we enforced that adjacent captures weren't allowed, + // this would do it (this would be an error: /foo/{bar}{boo}/) + // } else if (pos > 0 && pathPatternData[pos - 1] == '}') { + // throw new PatternParseException(pos, pathPatternData, + // PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES); + this.insideVariableCapture = true; + this.variableCaptureStart = this.pos; + } + else if (ch == '}') { + if (!this.insideVariableCapture) { + throw new PatternParseException(this.pos, this.pathPatternData, + PatternParseException.PatternMessage.MISSING_OPEN_CAPTURE); + } + this.insideVariableCapture = false; + if (this.isCaptureTheRestVariable && (this.pos + 1) < this.pathPatternLength) { + throw new PatternParseException(this.pos + 1, this.pathPatternData, + PatternParseException.PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); + } + this.variableCaptureCount++; + } + else if (ch == ':') { + if (this.insideVariableCapture && !this.isCaptureTheRestVariable) { + skipCaptureRegex(); + this.insideVariableCapture = false; + this.variableCaptureCount++; + } + } + else if (ch == '*') { + if (this.insideVariableCapture && this.variableCaptureStart == this.pos - 1) { + this.isCaptureTheRestVariable = true; + } + this.wildcard = true; + } + // Check that the characters used for captured variable names are like java identifiers + if (this.insideVariableCapture) { + if ((this.variableCaptureStart + 1 + (this.isCaptureTheRestVariable ? 1 : 0)) == this.pos && + !Character.isJavaIdentifierStart(ch)) { + throw new PatternParseException(this.pos, this.pathPatternData, + PatternParseException.PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, + Character.toString(ch)); + + } + else if ((this.pos > (this.variableCaptureStart + 1 + (this.isCaptureTheRestVariable ? 1 : 0)) && + !Character.isJavaIdentifierPart(ch) && ch != '-')) { + throw new PatternParseException(this.pos, this.pathPatternData, + PatternParseException.PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, + Character.toString(ch)); + } + } + } + this.pos++; + } + if (this.pathElementStart != -1) { + pushPathElement(createPathElement()); + } + return new PathPattern(pathPattern, this, this.headPE); + } + + /** + * Just hit a ':' and want to jump over the regex specification for this + * variable. pos will be pointing at the ':', we want to skip until the }. + * + * Nested {...} pairs don't have to be escaped: /abc/{var:x{1,2}}/def + * + * An escaped } will not be treated as the end of the regex: /abc/{var:x\\{y:}/def + * + * A separator that should not indicate the end of the regex can be escaped: + */ + private void skipCaptureRegex() { + this.pos++; + int regexStart = this.pos; + int curlyBracketDepth = 0; + boolean previousBackslash = false; + while (this.pos < this.pathPatternLength) { + char ch = this.pathPatternData[this.pos]; + if (ch == '\\' && !previousBackslash) { + this.pos++; + previousBackslash = true; + continue; + } + if (ch == '{' && !previousBackslash) { + curlyBracketDepth++; + } + else if (ch == '}' && !previousBackslash) { + if (curlyBracketDepth == 0) { + if (regexStart == this.pos) { + throw new PatternParseException(regexStart, this.pathPatternData, + PatternParseException.PatternMessage.MISSING_REGEX_CONSTRAINT); + } + return; + } + curlyBracketDepth--; + } + if (ch == this.getPathOptions().separator() && !previousBackslash) { + throw new PatternParseException(this.pos, this.pathPatternData, + PatternParseException.PatternMessage.MISSING_CLOSE_CAPTURE); + } + this.pos++; + previousBackslash = false; + } + + throw new PatternParseException(this.pos - 1, this.pathPatternData, + PatternParseException.PatternMessage.MISSING_CLOSE_CAPTURE); + } + + /** + * After processing a separator, a quick peek whether it is followed by + * a double wildcard (and only as the last path element). + */ + private boolean peekDoubleWildcard() { + if ((this.pos + 2) >= this.pathPatternLength) { + return false; + } + if (this.pathPatternData[this.pos + 1] != '*' || this.pathPatternData[this.pos + 2] != '*') { + return false; + } + char separator = this.getPathOptions().separator(); + if ((this.pos + 3) < this.pathPatternLength && this.pathPatternData[this.pos + 3] == separator) { + throw new PatternParseException(this.pos, this.pathPatternData, + PatternParseException.PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); + } + return (this.pos + 3 == this.pathPatternLength); + } + + /** + * Push a path element to the chain being build. + * @param newPathElement the new path element to add + */ + private void pushPathElement(PathElement newPathElement) { + if (newPathElement instanceof CaptureTheRestPathElement) { + // There must be a separator ahead of this thing + // currentPE SHOULD be a SeparatorPathElement + if (this.currentPE == null) { + this.headPE = newPathElement; + this.currentPE = newPathElement; + } + else if (this.currentPE instanceof SeparatorPathElement) { + PathElement peBeforeSeparator = this.currentPE.prev; + if (peBeforeSeparator == null) { + // /{*foobar} is at the start + this.headPE = newPathElement; + newPathElement.prev = null; + } + else { + peBeforeSeparator.next = newPathElement; + newPathElement.prev = peBeforeSeparator; + } + this.currentPE = newPathElement; + } + else { + throw new IllegalStateException("Expected SeparatorPathElement but was " + this.currentPE); + } + } + else { + if (this.headPE == null) { + this.headPE = newPathElement; + this.currentPE = newPathElement; + } + else if (this.currentPE != null) { + this.currentPE.next = newPathElement; + newPathElement.prev = this.currentPE; + this.currentPE = newPathElement; + } + } + resetPathElementState(); + } + + private char[] getPathElementText() { + int len = Math.min(this.pathPatternLength, this.pos - this.pathElementStart); + char[] pathElementText = new char[len]; + System.arraycopy(this.pathPatternData, this.pathElementStart, pathElementText, 0, len); + return pathElementText; + } + + /** + * Used the knowledge built up whilst processing since the last path element to determine what kind of path + * element to create. + * @return the new path element + */ + private PathElement createPathElement() { + if (this.insideVariableCapture) { + throw new PatternParseException(this.pos, this.pathPatternData, PatternParseException.PatternMessage.MISSING_CLOSE_CAPTURE); + } + PathElement newPE = null; + char separator = this.getPathOptions().separator(); + if (this.variableCaptureCount > 0) { + if (this.variableCaptureCount == 1 && this.pathElementStart == this.variableCaptureStart && + this.pathPatternData[this.pos - 1] == '}') { + if (this.isCaptureTheRestVariable) { + // It is {*....} + newPE = new CaptureTheRestPathElement( + this.pathElementStart, getPathElementText(), separator); + } + else { + // It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/ + try { + newPE = new CaptureVariablePathElement(this.pathElementStart, getPathElementText(), + this.isCaseSensitive(), separator); + } + catch (PatternSyntaxException pse) { + throw new PatternParseException(pse, + findRegexStart(this.pathPatternData, this.pathElementStart) + pse.getIndex(), + this.pathPatternData, PatternParseException.PatternMessage.REGEX_PATTERN_SYNTAX_EXCEPTION); + } + recordCapturedVariable(this.pathElementStart, + ((CaptureVariablePathElement) newPE).getVariableName()); + } + } + else { + if (this.isCaptureTheRestVariable) { + throw new PatternParseException(this.pathElementStart, this.pathPatternData, + PatternParseException.PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT); + } + RegexPathElement newRegexSection = new RegexPathElement(this.pathElementStart, + getPathElementText(), this.isCaseSensitive(), + this.pathPatternData, separator); + for (String variableName : newRegexSection.getVariableNames()) { + recordCapturedVariable(this.pathElementStart, variableName); + } + newPE = newRegexSection; + } + } + else { + if (this.wildcard) { + if (this.pos - 1 == this.pathElementStart) { + newPE = new WildcardPathElement(this.pathElementStart, separator); + } + else { + newPE = new RegexPathElement(this.pathElementStart, getPathElementText(), + this.isCaseSensitive(), this.pathPatternData, separator); + } + } + else if (this.singleCharWildcardCount != 0) { + newPE = new SingleCharWildcardedPathElement(this.pathElementStart, getPathElementText(), + this.singleCharWildcardCount, this.isCaseSensitive(), separator); + } + else { + newPE = new LiteralPathElement(this.pathElementStart, getPathElementText(), + this.isCaseSensitive(), separator); + } + } + return newPE; + } + + /** + * For a path element representing a captured variable, locate the constraint pattern. + * Assumes there is a constraint pattern. + * @param data a complete path expression, e.g. /aaa/bbb/{ccc:...} + * @param offset the start of the capture pattern of interest + * @return the index of the character after the ':' within + * the pattern expression relative to the start of the whole expression + */ + private int findRegexStart(char[] data, int offset) { + int pos = offset; + while (pos < data.length) { + if (data[pos] == ':') { + return pos + 1; + } + pos++; + } + return -1; + } + + /** + * Reset all the flags and position markers computed during path element processing. + */ + private void resetPathElementState() { + this.pathElementStart = -1; + this.singleCharWildcardCount = 0; + this.insideVariableCapture = false; + this.variableCaptureCount = 0; + this.wildcard = false; + this.isCaptureTheRestVariable = false; + this.variableCaptureStart = -1; + } + + /** + * Record a new captured variable. If it clashes with an existing one then report an error. + */ + private void recordCapturedVariable(int pos, String variableName) { + if (this.capturedVariableNames == null) { + this.capturedVariableNames = new ArrayList<>(); + } + if (this.capturedVariableNames.contains(variableName)) { + throw new PatternParseException(pos, this.pathPatternData, + PatternParseException.PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName); + } + this.capturedVariableNames.add(variableName); + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/PathRemainingMatchInfo.java b/net-path/src/main/java/org/xbib/net/path/spring/PathRemainingMatchInfo.java new file mode 100644 index 0000000..fbad5a8 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/PathRemainingMatchInfo.java @@ -0,0 +1,59 @@ +package org.xbib.net.path.spring; + +import org.xbib.net.path.spring.util.MultiValueMap; + +import java.util.Map; + +/** + * Holder for the result of a match on the start of a pattern. + * Provides access to the remaining path not matched to the pattern as well + * as any variables bound in that first part that was matched. + */ +public class PathRemainingMatchInfo { + + private final PathContainer pathMatched; + + private final PathContainer pathRemaining; + + private final PathMatchInfo pathMatchInfo; + + PathRemainingMatchInfo(PathContainer pathMatched, PathContainer pathRemaining) { + this(pathMatched, pathRemaining, PathMatchInfo.EMPTY); + } + + PathRemainingMatchInfo(PathContainer pathMatched, PathContainer pathRemaining, + PathMatchInfo pathMatchInfo) { + this.pathRemaining = pathRemaining; + this.pathMatched = pathMatched; + this.pathMatchInfo = pathMatchInfo; + } + + /** + * Return the part of a path that was matched by a pattern. + */ + public PathContainer getPathMatched() { + return this.pathMatched; + } + + /** + * Return the part of a path that was not matched by a pattern. + */ + public PathContainer getPathRemaining() { + return this.pathRemaining; + } + + /** + * Return variables that were bound in the part of the path that was + * successfully matched or an empty map. + */ + public Map getUriVariables() { + return this.pathMatchInfo.getUriVariables(); + } + + /** + * Return the path parameters for each bound variable. + */ + public Map> getMatrixVariables() { + return this.pathMatchInfo.getMatrixVariables(); + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/PatternParseException.java b/net-path/src/main/java/org/xbib/net/path/spring/PatternParseException.java new file mode 100644 index 0000000..a7de170 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/PatternParseException.java @@ -0,0 +1,94 @@ +package org.xbib.net.path.spring; + +import java.text.MessageFormat; + +/** + * Exception that is thrown when there is a problem with the pattern being parsed. + */ +@SuppressWarnings("serial") +public class PatternParseException extends IllegalArgumentException { + + private final int position; + + private final char[] pattern; + + private final PatternMessage messageType; + + private final Object[] inserts; + + public PatternParseException(int pos, char[] pattern, PatternMessage messageType, Object... inserts) { + super(messageType.formatMessage(inserts)); + this.position = pos; + this.pattern = pattern; + this.messageType = messageType; + this.inserts = inserts; + } + + public PatternParseException(Throwable cause, int pos, char[] pattern, PatternMessage messageType, Object... inserts) { + super(messageType.formatMessage(inserts), cause); + this.position = pos; + this.pattern = pattern; + this.messageType = messageType; + this.inserts = inserts; + } + + /** + * Return a formatted message with inserts applied. + */ + @Override + public String getMessage() { + return this.messageType.formatMessage(this.inserts); + } + + /** + * Return a detailed message that includes the original pattern text + * with a pointer to the error position, as well as the error message. + */ + public String toDetailedString() { + return String.valueOf(this.pattern) + '\n' + + " ".repeat(Math.max(0, this.position)) + + "^\n" + + getMessage(); + } + + public int getPosition() { + return this.position; + } + + public PatternMessage getMessageType() { + return this.messageType; + } + + public Object[] getInserts() { + return this.inserts; + } + + + /** + * The messages that can be included in a {@link PatternParseException} when there is a parse failure. + */ + public enum PatternMessage { + + MISSING_CLOSE_CAPTURE("Expected close capture character after variable name '}'"), + MISSING_OPEN_CAPTURE("Missing preceding open capture character before variable name'{'"), + ILLEGAL_NESTED_CAPTURE("Not allowed to nest variable captures"), + ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR("Char ''{0}'' not allowed at start of captured variable name"), + ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR("Char ''{0}'' is not allowed in a captured variable name"), + NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST("No more pattern data allowed after '{*...}' or '**' pattern element"), + MISSING_REGEX_CONSTRAINT("Missing regex constraint on capture"), + ILLEGAL_DOUBLE_CAPTURE("Not allowed to capture ''{0}'' twice in the same pattern"), + REGEX_PATTERN_SYNTAX_EXCEPTION("Exception occurred in regex pattern compilation"), + CAPTURE_ALL_IS_STANDALONE_CONSTRUCT("'{*...}' can only be preceded by a path separator"); + + private final String message; + + PatternMessage(String message) { + this.message = message; + } + + public String formatMessage(Object... inserts) { + return MessageFormat.format(this.message, inserts); + } + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/element/CaptureTheRestPathElement.java b/net-path/src/main/java/org/xbib/net/path/spring/element/CaptureTheRestPathElement.java new file mode 100644 index 0000000..1ec6558 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/element/CaptureTheRestPathElement.java @@ -0,0 +1,103 @@ +package org.xbib.net.path.spring.element; + +import org.xbib.net.path.spring.MatchingContext; +import org.xbib.net.path.spring.PathContainer; +import org.xbib.net.path.spring.util.LinkedMultiValueMap; +import org.xbib.net.path.spring.util.MultiValueMap; + +import java.util.List; + +/** + * A path element representing capturing the rest of a path. In the pattern + * '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureTheRestPathElement}. + */ +public class CaptureTheRestPathElement extends PathElement { + + private final String variableName; + + /** + * Create a new {@link CaptureTheRestPathElement} instance. + * @param pos position of the path element within the path pattern text + * @param captureDescriptor a character array containing contents like '{' '*' 'a' 'b' '}' + * @param separator the separator used in the path pattern + */ + public CaptureTheRestPathElement(int pos, char[] captureDescriptor, char separator) { + super(pos, separator); + this.variableName = new String(captureDescriptor, 2, captureDescriptor.length - 3); + } + + + @Override + public boolean matches(int pathIndex, MatchingContext matchingContext) { + // No need to handle 'match start' checking as this captures everything + // anyway and cannot be followed by anything else + // assert next == null + + // If there is more data, it must start with the separator + if (pathIndex < matchingContext.pathLength && !matchingContext.isSeparator(pathIndex)) { + return false; + } + if (matchingContext.determineRemainingPath) { + matchingContext.remainingPathIndex = matchingContext.pathLength; + } + if (matchingContext.extractingVariables) { + // Collect the parameters from all the remaining segments + MultiValueMap parametersCollector = null; + for (int i = pathIndex; i < matchingContext.pathLength; i++) { + PathContainer.Element element = matchingContext.pathElements.get(i); + if (element instanceof PathContainer.PathSegment) { + MultiValueMap parameters = ((PathContainer.PathSegment) element).parameters(); + if (!parameters.isEmpty()) { + if (parametersCollector == null) { + parametersCollector = new LinkedMultiValueMap<>(); + } + parametersCollector.addAll(parameters); + } + } + } + matchingContext.set(this.variableName, pathToString(pathIndex, matchingContext.pathElements), + parametersCollector == null?NO_PARAMETERS:parametersCollector); + } + return true; + } + + private String pathToString(int fromSegment, List pathElements) { + StringBuilder sb = new StringBuilder(); + for (int i = fromSegment, max = pathElements.size(); i < max; i++) { + PathContainer.Element element = pathElements.get(i); + if (element instanceof PathContainer.PathSegment) { + sb.append(((PathContainer.PathSegment)element).valueToMatch()); + } + else { + sb.append(element.value()); + } + } + return sb.toString(); + } + + @Override + public int getNormalizedLength() { + return 1; + } + + @Override + public char[] getChars() { + return ("/{*" + this.variableName + "}").toCharArray(); + } + + @Override + public int getWildcardCount() { + return 0; + } + + @Override + public int getCaptureCount() { + return 1; + } + + @Override + public String toString() { + return "CaptureTheRest(/{*" + this.variableName + "})"; + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/element/CaptureVariablePathElement.java b/net-path/src/main/java/org/xbib/net/path/spring/element/CaptureVariablePathElement.java new file mode 100644 index 0000000..7ebe663 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/element/CaptureVariablePathElement.java @@ -0,0 +1,150 @@ +package org.xbib.net.path.spring.element; + +import org.xbib.net.path.spring.MatchingContext; +import org.xbib.net.path.spring.PathContainer; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A path element representing capturing a piece of the path as a variable. In the pattern + * '/foo/{bar}/goo' the {bar} is represented as a {@link CaptureVariablePathElement}. There + * must be at least one character to bind to the variable. + */ +public class CaptureVariablePathElement extends PathElement { + + private final String variableName; + + private final Pattern constraintPattern; + + /** + * Create a new {@link CaptureVariablePathElement} instance. + * @param pos the position in the pattern of this capture element + * @param captureDescriptor is of the form {AAAAA[:pattern]} + */ + public CaptureVariablePathElement(int pos, char[] captureDescriptor, boolean caseSensitive, char separator) { + super(pos, separator); + int colon = -1; + for (int i = 0; i < captureDescriptor.length; i++) { + if (captureDescriptor[i] == ':') { + colon = i; + break; + } + } + if (colon == -1) { + // no constraint + this.variableName = new String(captureDescriptor, 1, captureDescriptor.length - 2); + this.constraintPattern = null; + } + else { + this.variableName = new String(captureDescriptor, 1, colon - 1); + if (caseSensitive) { + this.constraintPattern = Pattern.compile( + new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2)); + } + else { + this.constraintPattern = Pattern.compile( + new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2), + Pattern.CASE_INSENSITIVE); + } + } + } + + + @Override + public boolean matches(int pathIndex, MatchingContext matchingContext) { + if (pathIndex >= matchingContext.pathLength) { + // no more path left to match this element + return false; + } + String candidateCapture = matchingContext.pathElementValue(pathIndex); + if (candidateCapture.length() == 0) { + return false; + } + + if (this.constraintPattern != null) { + // TODO possible optimization - only regex match if rest of pattern matches? + // Benefit likely to vary pattern to pattern + Matcher matcher = this.constraintPattern.matcher(candidateCapture); + if (matcher.groupCount() != 0) { + throw new IllegalArgumentException( + "No capture groups allowed in the constraint regex: " + this.constraintPattern.pattern()); + } + if (!matcher.matches()) { + return false; + } + } + + boolean match = false; + pathIndex++; + if (isNoMorePattern()) { + if (matchingContext.determineRemainingPath) { + matchingContext.remainingPathIndex = pathIndex; + match = true; + } + else { + // Needs to be at least one character #SPR15264 + match = (pathIndex == matchingContext.pathLength); + if (!match && matchingContext.isMatchOptionalTrailingSeparator()) { + match = //(nextPos > candidateIndex) && + (pathIndex + 1) == matchingContext.pathLength && + matchingContext.isSeparator(pathIndex); + } + } + } + else { + if (this.next != null) { + match = this.next.matches(pathIndex, matchingContext); + } + } + + if (match && matchingContext.extractingVariables) { + matchingContext.set(this.variableName, candidateCapture, + ((PathContainer.PathSegment)matchingContext.pathElements.get(pathIndex-1)).parameters()); + } + return match; + } + + public String getVariableName() { + return this.variableName; + } + + @Override + public int getNormalizedLength() { + return 1; + } + + @Override + public char[] getChars() { + StringBuilder sb = new StringBuilder(); + sb.append('{'); + sb.append(this.variableName); + if (this.constraintPattern != null) { + sb.append(':').append(this.constraintPattern.pattern()); + } + sb.append('}'); + return sb.toString().toCharArray(); + } + + @Override + public int getWildcardCount() { + return 0; + } + + @Override + public int getCaptureCount() { + return 1; + } + + @Override + public int getScore() { + return CAPTURE_VARIABLE_WEIGHT; + } + + @Override + public String toString() { + return "CaptureVariable({" + this.variableName + + (this.constraintPattern != null ? ":" + this.constraintPattern.pattern() : "") + "})"; + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/element/LiteralPathElement.java b/net-path/src/main/java/org/xbib/net/path/spring/element/LiteralPathElement.java new file mode 100644 index 0000000..ac50b02 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/element/LiteralPathElement.java @@ -0,0 +1,104 @@ +package org.xbib.net.path.spring.element; + +import org.xbib.net.path.spring.MatchingContext; +import org.xbib.net.path.spring.PathContainer; + +/** + * A literal path element. In the pattern '/foo/bar/goo' there are three + * literal path elements 'foo', 'bar' and 'goo'. + */ +public class LiteralPathElement extends PathElement { + + private final char[] text; + + private final int len; + + private final boolean caseSensitive; + + public LiteralPathElement(int pos, char[] literalText, boolean caseSensitive, char separator) { + super(pos, separator); + this.len = literalText.length; + this.caseSensitive = caseSensitive; + if (caseSensitive) { + this.text = literalText; + } + else { + // Force all the text lower case to make matching faster + this.text = new char[literalText.length]; + for (int i = 0; i < this.len; i++) { + this.text[i] = Character.toLowerCase(literalText[i]); + } + } + } + + @Override + public boolean matches(int pathIndex, MatchingContext matchingContext) { + if (pathIndex >= matchingContext.pathLength) { + // no more path left to match this element + return false; + } + PathContainer.Element element = matchingContext.pathElements.get(pathIndex); + if (!(element instanceof PathContainer.PathSegment)) { + return false; + } + String value = ((PathContainer.PathSegment)element).valueToMatch(); + if (value.length() != this.len) { + // Not enough data to match this path element + return false; + } + + if (this.caseSensitive) { + for (int i = 0; i < this.len; i++) { + if (value.charAt(i) != this.text[i]) { + return false; + } + } + } + else { + for (int i = 0; i < this.len; i++) { + // TODO revisit performance if doing a lot of case insensitive matching + if (Character.toLowerCase(value.charAt(i)) != this.text[i]) { + return false; + } + } + } + + pathIndex++; + if (isNoMorePattern()) { + if (matchingContext.determineRemainingPath) { + matchingContext.remainingPathIndex = pathIndex; + return true; + } + else { + if (pathIndex == matchingContext.pathLength) { + return true; + } + else { + return (matchingContext.isMatchOptionalTrailingSeparator() && + (pathIndex + 1) == matchingContext.pathLength && + matchingContext.isSeparator(pathIndex)); + } + } + } + else { + return (this.next != null && this.next.matches(pathIndex, matchingContext)); + } + } + + @Override + public int getNormalizedLength() { + return this.len; + } + + @Override + public char[] getChars() { + return this.text; + } + + + @Override + public String toString() { + return "Literal(" + String.valueOf(this.text) + ")"; + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/element/PathElement.java b/net-path/src/main/java/org/xbib/net/path/spring/element/PathElement.java new file mode 100644 index 0000000..0cab9ad --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/element/PathElement.java @@ -0,0 +1,88 @@ +package org.xbib.net.path.spring.element; + +import org.xbib.net.path.spring.MatchingContext; +import org.xbib.net.path.spring.util.LinkedMultiValueMap; +import org.xbib.net.path.spring.util.MultiValueMap; + +/** + * Common supertype for the Ast nodes created to represent a path pattern. + */ +public abstract class PathElement { + + // Score related + protected static final int WILDCARD_WEIGHT = 100; + + protected static final int CAPTURE_VARIABLE_WEIGHT = 1; + + protected static final MultiValueMap NO_PARAMETERS = new LinkedMultiValueMap<>(); + + // Position in the pattern where this path element starts + protected final int pos; + + // The separator used in this path pattern + protected final char separator; + + // The next path element in the chain + public PathElement next; + + // The previous path element in the chain + public PathElement prev; + + + /** + * Create a new path element. + * @param pos the position where this path element starts in the pattern data + * @param separator the separator in use in the path pattern + */ + public PathElement(int pos, char separator) { + this.pos = pos; + this.separator = separator; + } + + + /** + * Attempt to match this path element. + * @param candidatePos the current position within the candidate path + * @param matchingContext encapsulates context for the match including the candidate + * @return {@code true} if it matches, otherwise {@code false} + */ + public abstract boolean matches(int candidatePos, MatchingContext matchingContext); + + /** + * Return the length of the path element where captures are considered to be one character long. + * @return the normalized length + */ + public abstract int getNormalizedLength(); + + public abstract char[] getChars(); + + /** + * Return the number of variables captured by the path element. + */ + public int getCaptureCount() { + return 0; + } + + /** + * Return the number of wildcard elements (*, ?) in the path element. + */ + public int getWildcardCount() { + return 0; + } + + /** + * Return the score for this PathElement, combined score is used to compare parsed patterns. + */ + public int getScore() { + return 0; + } + + /** + * Return if the there are no more PathElements in the pattern. + * @return {@code true} if the there are no more elements + */ + protected final boolean isNoMorePattern() { + return this.next == null; + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/element/RegexPathElement.java b/net-path/src/main/java/org/xbib/net/path/spring/element/RegexPathElement.java new file mode 100644 index 0000000..d46e017 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/element/RegexPathElement.java @@ -0,0 +1,191 @@ +package org.xbib.net.path.spring.element; + +import org.xbib.net.path.spring.MatchingContext; +import org.xbib.net.path.spring.PathContainer; +import org.xbib.net.path.spring.PatternParseException; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A regex path element. Used to represent any complicated element of the path. + * For example in '/foo/*_*/*_{foobar}' both *_* and *_{foobar} + * are {@link RegexPathElement} path elements. + */ +public class RegexPathElement extends PathElement { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; + + private final char[] regex; + + private final boolean caseSensitive; + + private final Pattern pattern; + + private int wildcardCount; + + private final List variableNames = new ArrayList<>(); + + + public RegexPathElement(int pos, char[] regex, boolean caseSensitive, char[] completePattern, char separator) { + super(pos, separator); + this.regex = regex; + this.caseSensitive = caseSensitive; + this.pattern = buildPattern(regex, completePattern); + } + + + public Pattern buildPattern(char[] regex, char[] completePattern) { + StringBuilder patternBuilder = new StringBuilder(); + String text = new String(regex); + Matcher matcher = GLOB_PATTERN.matcher(text); + int end = 0; + + while (matcher.find()) { + patternBuilder.append(quote(text, end, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + patternBuilder.append('.'); + } + else if ("*".equals(match)) { + patternBuilder.append(".*"); + int pos = matcher.start(); + if (pos < 1 || text.charAt(pos-1) != '.') { + // To be compatible with the AntPathMatcher comparator, + // '.*' is not considered a wildcard usage + this.wildcardCount++; + } + } + else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + String variableName = matcher.group(1); + if (this.variableNames.contains(variableName)) { + throw new PatternParseException(this.pos, completePattern, + PatternParseException.PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName); + } + this.variableNames.add(variableName); + } + else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('('); + patternBuilder.append(variablePattern); + patternBuilder.append(')'); + String variableName = match.substring(1, colonIdx); + if (this.variableNames.contains(variableName)) { + throw new PatternParseException(this.pos, completePattern, + PatternParseException.PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName); + } + this.variableNames.add(variableName); + } + } + end = matcher.end(); + } + + patternBuilder.append(quote(text, end, text.length())); + if (this.caseSensitive) { + return Pattern.compile(patternBuilder.toString()); + } + else { + return Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE); + } + } + + public List getVariableNames() { + return this.variableNames; + } + + private String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + @Override + public boolean matches(int pathIndex, MatchingContext matchingContext) { + String textToMatch = matchingContext.pathElementValue(pathIndex); + Matcher matcher = this.pattern.matcher(textToMatch); + boolean matches = matcher.matches(); + if (matches) { + if (isNoMorePattern()) { + if (matchingContext.determineRemainingPath && + (this.variableNames.isEmpty() || textToMatch.length() > 0)) { + matchingContext.remainingPathIndex = pathIndex + 1; + } + else { + // No more pattern, is there more data? + // If pattern is capturing variables there must be some actual data to bind to them + matches = (pathIndex + 1 >= matchingContext.pathLength) && + (this.variableNames.isEmpty() || textToMatch.length() > 0); + if (!matches && matchingContext.isMatchOptionalTrailingSeparator()) { + matches = (this.variableNames.isEmpty() || textToMatch.length() > 0) && + (pathIndex + 2 >= matchingContext.pathLength) && + matchingContext.isSeparator(pathIndex + 1); + } + } + } + else { + matches = (this.next != null && this.next.matches(pathIndex + 1, matchingContext)); + } + } + if (matches && matchingContext.extractingVariables) { + // Process captures + if (this.variableNames.size() != matcher.groupCount()) { // SPR-8455 + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + this.pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + String value = matcher.group(i); + matchingContext.set(name, value, + (i == this.variableNames.size())? + ((PathContainer.PathSegment)matchingContext.pathElements.get(pathIndex)).parameters(): + NO_PARAMETERS); + } + } + return matches; + } + + @Override + public int getNormalizedLength() { + int varsLength = 0; + for (String variableName : this.variableNames) { + varsLength += variableName.length(); + } + return (this.regex.length - varsLength - this.variableNames.size()); + } + + @Override + public char[] getChars() { + return this.regex; + } + + @Override + public int getCaptureCount() { + return this.variableNames.size(); + } + + @Override + public int getWildcardCount() { + return this.wildcardCount; + } + + @Override + public int getScore() { + return (getCaptureCount() * CAPTURE_VARIABLE_WEIGHT + getWildcardCount() * WILDCARD_WEIGHT); + } + + @Override + public String toString() { + return "Regex(" + String.valueOf(this.regex) + ")"; + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/element/SeparatorPathElement.java b/net-path/src/main/java/org/xbib/net/path/spring/element/SeparatorPathElement.java new file mode 100644 index 0000000..2e7110a --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/element/SeparatorPathElement.java @@ -0,0 +1,56 @@ +package org.xbib.net.path.spring.element; + +import org.xbib.net.path.spring.MatchingContext; + +/** + * A separator path element. In the pattern '/foo/bar' the two occurrences + * of '/' will be represented by a SeparatorPathElement (if the default + * separator of '/' is being used). + */ +public class SeparatorPathElement extends PathElement { + + public SeparatorPathElement(int pos, char separator) { + super(pos, separator); + } + + /** + * Matching a separator is easy, basically the character at candidateIndex + * must be the separator. + */ + @Override + public boolean matches(int pathIndex, MatchingContext matchingContext) { + if (pathIndex < matchingContext.pathLength && matchingContext.isSeparator(pathIndex)) { + if (isNoMorePattern()) { + if (matchingContext.determineRemainingPath) { + matchingContext.remainingPathIndex = pathIndex + 1; + return true; + } + else { + return (pathIndex + 1 == matchingContext.pathLength); + } + } + else { + pathIndex++; + return (this.next != null && this.next.matches(pathIndex, matchingContext)); + } + } + return false; + } + + @Override + public int getNormalizedLength() { + return 1; + } + + @Override + public char[] getChars() { + return new char[] {this.separator}; + } + + + @Override + public String toString() { + return "Separator(" + this.separator + ")"; + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/element/SingleCharWildcardedPathElement.java b/net-path/src/main/java/org/xbib/net/path/spring/element/SingleCharWildcardedPathElement.java new file mode 100644 index 0000000..48a4abc --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/element/SingleCharWildcardedPathElement.java @@ -0,0 +1,114 @@ +package org.xbib.net.path.spring.element; + +import org.xbib.net.path.spring.MatchingContext; +import org.xbib.net.path.spring.PathContainer; + +/** + * A literal path element that does includes the single character wildcard '?' one + * or more times (to basically many any character at that position). + */ +public class SingleCharWildcardedPathElement extends PathElement { + + private final char[] text; + + private final int len; + + private final int questionMarkCount; + + private final boolean caseSensitive; + + public SingleCharWildcardedPathElement( + int pos, char[] literalText, int questionMarkCount, boolean caseSensitive, char separator) { + + super(pos, separator); + this.len = literalText.length; + this.questionMarkCount = questionMarkCount; + this.caseSensitive = caseSensitive; + if (caseSensitive) { + this.text = literalText; + } + else { + this.text = new char[literalText.length]; + for (int i = 0; i < this.len; i++) { + this.text[i] = Character.toLowerCase(literalText[i]); + } + } + } + + @Override + public boolean matches(int pathIndex, MatchingContext matchingContext) { + if (pathIndex >= matchingContext.pathLength) { + // no more path left to match this element + return false; + } + + PathContainer.Element element = matchingContext.pathElements.get(pathIndex); + if (!(element instanceof PathContainer.PathSegment)) { + return false; + } + String value = ((PathContainer.PathSegment)element).valueToMatch(); + if (value.length() != this.len) { + // Not enough data to match this path element + return false; + } + if (this.caseSensitive) { + for (int i = 0; i < this.len; i++) { + char ch = this.text[i]; + if ((ch != '?') && (ch != value.charAt((i)))) { + return false; + } + } + } + else { + for (int i = 0; i < this.len; i++) { + char ch = this.text[i]; + // TODO revisit performance if doing a lot of case insensitive matching + if ((ch != '?') && (ch != Character.toLowerCase(value.charAt(i)))) { + return false; + } + } + } + pathIndex++; + if (isNoMorePattern()) { + if (matchingContext.determineRemainingPath) { + matchingContext.remainingPathIndex = pathIndex; + return true; + } + else { + if (pathIndex == matchingContext.pathLength) { + return true; + } + else { + return (matchingContext.isMatchOptionalTrailingSeparator() && + (pathIndex + 1) == matchingContext.pathLength && + matchingContext.isSeparator(pathIndex)); + } + } + } + else { + return (this.next != null && this.next.matches(pathIndex, matchingContext)); + } + } + + @Override + public int getWildcardCount() { + return this.questionMarkCount; + } + + @Override + public int getNormalizedLength() { + return this.len; + } + + @Override + public char[] getChars() { + return this.text; + } + + + @Override + public String toString() { + return "SingleCharWildcarded(" + String.valueOf(this.text) + ")"; + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/element/WildcardPathElement.java b/net-path/src/main/java/org/xbib/net/path/spring/element/WildcardPathElement.java new file mode 100644 index 0000000..1651225 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/element/WildcardPathElement.java @@ -0,0 +1,86 @@ +package org.xbib.net.path.spring.element; + +import org.xbib.net.path.spring.MatchingContext; +import org.xbib.net.path.spring.PathContainer; + +/** + * A wildcard path element. In the pattern '/foo/*/goo' the * is + * represented by a WildcardPathElement. Within a path it matches at least + * one character but at the end of a path it can match zero characters. + */ +public class WildcardPathElement extends PathElement { + + public WildcardPathElement(int pos, char separator) { + super(pos, separator); + } + + /** + * Matching on a WildcardPathElement is quite straight forward. Scan the + * candidate from the candidateIndex onwards for the next separator or the end of the + * candidate. + */ + @Override + public boolean matches(int pathIndex, MatchingContext matchingContext) { + String segmentData = null; + // Assert if it exists it is a segment + if (pathIndex < matchingContext.pathLength) { + PathContainer.Element element = matchingContext.pathElements.get(pathIndex); + if (!(element instanceof PathContainer.PathSegment)) { + // Should not match a separator + return false; + } + segmentData = ((PathContainer.PathSegment)element).valueToMatch(); + pathIndex++; + } + if (isNoMorePattern()) { + if (matchingContext.determineRemainingPath) { + matchingContext.remainingPathIndex = pathIndex; + return true; + } + else { + if (pathIndex == matchingContext.pathLength) { + // and the path data has run out too + return true; + } + else { + return (matchingContext.isMatchOptionalTrailingSeparator() && // if optional slash is on... + segmentData != null && segmentData.length() > 0 && // and there is at least one character to match the *... + (pathIndex + 1) == matchingContext.pathLength && // and the next path element is the end of the candidate... + matchingContext.isSeparator(pathIndex)); // and the final element is a separator + } + } + } else { + // Within a path (e.g. /aa/*/bb) there must be at least one character to match the wildcard + if (segmentData == null || segmentData.length() == 0) { + return false; + } + return (this.next != null && this.next.matches(pathIndex, matchingContext)); + } + } + + @Override + public int getNormalizedLength() { + return 1; + } + + @Override + public char[] getChars() { + return new char[] {'*'}; + } + + @Override + public int getWildcardCount() { + return 1; + } + + @Override + public int getScore() { + return WILDCARD_WEIGHT; + } + + @Override + public String toString() { + return "Wildcard(*)"; + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/element/WildcardTheRestPathElement.java b/net-path/src/main/java/org/xbib/net/path/spring/element/WildcardTheRestPathElement.java new file mode 100644 index 0000000..acad1dd --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/element/WildcardTheRestPathElement.java @@ -0,0 +1,47 @@ +package org.xbib.net.path.spring.element; + +import org.xbib.net.path.spring.MatchingContext; + +/** + * A path element representing wildcarding the rest of a path. In the pattern + * '/foo/**' the /** is represented as a {@link WildcardTheRestPathElement}. + * + */ +public class WildcardTheRestPathElement extends PathElement { + + public WildcardTheRestPathElement(int pos, char separator) { + super(pos, separator); + } + + @Override + public boolean matches(int pathIndex, MatchingContext matchingContext) { + // If there is more data, it must start with the separator + if (pathIndex < matchingContext.pathLength && !matchingContext.isSeparator(pathIndex)) { + return false; + } + if (matchingContext.determineRemainingPath) { + matchingContext.remainingPathIndex = matchingContext.pathLength; + } + return true; + } + + @Override + public int getNormalizedLength() { + return 1; + } + + @Override + public char[] getChars() { + return (this.separator + "**").toCharArray(); + } + + @Override + public int getWildcardCount() { + return 1; + } + + @Override + public String toString() { + return "WildcardTheRest(" + this.separator + "**)"; + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/util/LinkedMultiValueMap.java b/net-path/src/main/java/org/xbib/net/path/spring/util/LinkedMultiValueMap.java new file mode 100644 index 0000000..9a3cc42 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/util/LinkedMultiValueMap.java @@ -0,0 +1,65 @@ +package org.xbib.net.path.spring.util; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Simple implementation of {@link MultiValueMap} that wraps a {@link LinkedHashMap}, + * storing multiple values in an {@link ArrayList}. + * + * This Map implementation is generally not thread-safe. It is primarily designed + * for data structures exposed from request objects, for use in a single thread only. + * + * @param the key type + * @param the value element type + */ +public class LinkedMultiValueMap extends MultiValueMapAdapter { + + static final float DEFAULT_LOAD_FACTOR = 0.75f; + + /** + * Create a new LinkedMultiValueMap that wraps a {@link LinkedHashMap}. + */ + public LinkedMultiValueMap() { + super(new LinkedHashMap<>()); + } + + /** + * Create a new LinkedMultiValueMap that wraps a {@link LinkedHashMap} + * with an initial capacity that can accommodate the specified number of + * elements without any immediate resize/rehash operations to be expected. + * @param expectedSize the expected number of elements (with a corresponding + * capacity to be derived so that no resize/rehash operations are needed) + */ + public LinkedMultiValueMap(int expectedSize) { + super(new LinkedHashMap<>((int) (expectedSize / DEFAULT_LOAD_FACTOR), DEFAULT_LOAD_FACTOR)); + } + + /** + * Copy constructor: Create a new LinkedMultiValueMap with the same mappings as + * the specified Map. Note that this will be a shallow copy; its value-holding + * List entries will get reused and therefore cannot get modified independently. + * @param otherMap the Map whose mappings are to be placed in this Map + * @see #clone() + * @see #deepCopy() + */ + public LinkedMultiValueMap(Map> otherMap) { + super(new LinkedHashMap<>(otherMap)); + } + + /** + * Create a deep copy of this Map. + * @return a copy of this Map, including a copy of each value-holding List entry + * (consistently using an independent modifiable {@link ArrayList} for each entry) + * along the lines of {@code MultiValueMap.addAll} semantics + * @see #addAll(MultiValueMap) + * @see #clone() + */ + public LinkedMultiValueMap deepCopy() { + LinkedMultiValueMap copy = new LinkedMultiValueMap<>(size()); + forEach((key, values) -> copy.put(key, new ArrayList<>(values))); + return copy; + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/util/MultiValueMap.java b/net-path/src/main/java/org/xbib/net/path/spring/util/MultiValueMap.java new file mode 100644 index 0000000..e1d227a --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/util/MultiValueMap.java @@ -0,0 +1,34 @@ +package org.xbib.net.path.spring.util; + +import java.util.List; +import java.util.Map; + +/** + * Extension of the {@code Map} interface that stores multiple values. + * + * @param the key type + * @param the value element type + */ +public interface MultiValueMap extends Map> { + + /** + * Add the given single value to the current list of values for the given key. + * @param key the key + * @param value the value to be added + */ + void add(K key, V value); + + /** + * Add all the values of the given list to the current list of values for the given key. + * @param key they key + * @param values the values to be added + */ + void addAll(K key, List values); + + /** + * Add all the values of the given {@code MultiValueMap} to the current values. + * @param values the values to be added + */ + void addAll(MultiValueMap values); + +} diff --git a/net-path/src/main/java/org/xbib/net/path/spring/util/MultiValueMapAdapter.java b/net-path/src/main/java/org/xbib/net/path/spring/util/MultiValueMapAdapter.java new file mode 100644 index 0000000..8a2e728 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/spring/util/MultiValueMapAdapter.java @@ -0,0 +1,124 @@ +package org.xbib.net.path.spring.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Adapts a given {@link Map} to the {@link MultiValueMap} contract. + * + * @param the key type + * @param the value element type + * @see LinkedMultiValueMap + */ +@SuppressWarnings("serial") +public class MultiValueMapAdapter implements MultiValueMap { + + private final Map> targetMap; + + /** + * Wrap the given target {@link Map} as a {@link MultiValueMap} adapter. + * @param targetMap the plain target {@code Map} + */ + public MultiValueMapAdapter(Map> targetMap) { + //Assert.notNull(targetMap, "'targetMap' must not be null"); + this.targetMap = targetMap; + } + + @Override + public void add(K key, V value) { + List values = this.targetMap.computeIfAbsent(key, k -> new ArrayList<>(1)); + values.add(value); + } + + @Override + public void addAll(K key, List values) { + List currentValues = this.targetMap.computeIfAbsent(key, k -> new ArrayList<>(1)); + currentValues.addAll(values); + } + + @Override + public void addAll(MultiValueMap values) { + for (Entry> entry : values.entrySet()) { + addAll(entry.getKey(), entry.getValue()); + } + } + + @Override + public int size() { + return this.targetMap.size(); + } + + @Override + public boolean isEmpty() { + return this.targetMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.targetMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.targetMap.containsValue(value); + } + + @Override + public List get(Object key) { + return this.targetMap.get(key); + } + + @Override + public List put(K key, List value) { + return this.targetMap.put(key, value); + } + + @Override + public List remove(Object key) { + return this.targetMap.remove(key); + } + + @Override + public void putAll(Map> map) { + this.targetMap.putAll(map); + } + + @Override + public void clear() { + this.targetMap.clear(); + } + + @Override + public Set keySet() { + return this.targetMap.keySet(); + } + + @Override + public Collection> values() { + return this.targetMap.values(); + } + + @Override + public Set>> entrySet() { + return this.targetMap.entrySet(); + } + + @Override + public boolean equals(Object other) { + return (this == other || this.targetMap.equals(other)); + } + + @Override + public int hashCode() { + return this.targetMap.hashCode(); + } + + @Override + public String toString() { + return this.targetMap.toString(); + } + +} diff --git a/net-path/src/main/java/org/xbib/net/path/structure/Path.java b/net-path/src/main/java/org/xbib/net/path/structure/Path.java new file mode 100644 index 0000000..0a4091d --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/structure/Path.java @@ -0,0 +1,194 @@ +package org.xbib.net.path.structure; + +import org.xbib.net.PathNormalizer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.StringTokenizer; +import java.util.regex.Pattern; + +public class Path { + + protected static final Pattern PARAMETER_PATTERN = Pattern.compile("\\{[^/]+?}"); + + protected static final String CATCH_ALL = "**"; + + protected String pathSpec; + + protected String pathSeparator; + + protected boolean trimTokens; + + protected boolean caseSensitive; + + protected List segments; + + private int parameterCount; + + private int singleWildcards; + + private int doubleWildcards; + + private boolean catchAllPattern; + + private boolean prefixPattern; + + private Integer length; + + public Path() { + } + + public Path(String pathSpec) { + this(pathSpec, "/", false, false); + } + + public Path(String pathSpec, String pathSeparator, boolean trimTokens, boolean caseSensitive) { + init(pathSpec, pathSeparator, trimTokens, caseSensitive); + } + + public static Path of(String pathSpec) { + return new Path(pathSpec); + } + + public void init(String pathSpec, String pathSeparator, boolean trimTokens, boolean caseSensitive) { + this.pathSpec = PathNormalizer.normalize(pathSpec); + this.pathSeparator = pathSeparator; + this.trimTokens = trimTokens; + this.caseSensitive = caseSensitive; + this.segments = tokenize(this.pathSpec); + if (pathSpec != null) { + initCounters(); + this.catchAllPattern = pathSpec.equals(CATCH_ALL); + this.prefixPattern = !catchAllPattern && pathSpec.endsWith(CATCH_ALL); + } + if (this.parameterCount == 0) { + this.length = pathSpec != null ? pathSpec.length() : 0; + } + } + + public String getPathSpec() { + return pathSpec; + } + + public String getPathSeparator() { + return pathSeparator; + } + + public boolean isTrimTokens() { + return trimTokens; + } + + public boolean isCaseSensitive() { + return caseSensitive; + } + + public int getParameterCount() { + return parameterCount; + } + + public int getSingleWildcards() { + return singleWildcards; + } + + public int getDoubleWildcards() { + return doubleWildcards; + } + + public boolean isLeastSpecific() { + return pathSpec == null || catchAllPattern; + } + + public boolean isPrefixPattern() { + return prefixPattern; + } + + public int getTotalCount() { + return parameterCount + singleWildcards + (2 * doubleWildcards); + } + + public boolean isCatchAllPattern() { + return catchAllPattern; + } + + public boolean isWildCard() { + return singleWildcards > 0 || doubleWildcards > 0; + } + + public int getLength() { + if (length == null) { + length = PARAMETER_PATTERN.matcher(pathSpec).replaceAll("#").length(); + } + return length; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Path that = (Path) o; + return parameterCount == that.parameterCount && singleWildcards == that.singleWildcards && doubleWildcards == that.doubleWildcards && catchAllPattern == that.catchAllPattern && prefixPattern == that.prefixPattern && Objects.equals(pathSpec, that.pathSpec) && Objects.equals(length, that.length); + } + + @Override + public int hashCode() { + return Objects.hash(pathSpec, parameterCount, singleWildcards, doubleWildcards, catchAllPattern, prefixPattern, length); + } + + public List tokenize(String string) { + return tokenize(string, pathSeparator, trimTokens, caseSensitive); + } + + public static List tokenize(String string, String pathSeparator, boolean trimTokens, boolean caseSensitive) { + List pathSegments = new ArrayList<>(); + if (string == null) { + return pathSegments; + } + StringTokenizer st = new StringTokenizer(string, pathSeparator); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (token.length() > 0) { + if (!caseSensitive) { + token = token.toLowerCase(Locale.ROOT); + } + PathSegment pathSegment = new PathSegment(); + pathSegment.setString(token); + pathSegments.add(pathSegment); + } + } + return pathSegments; + } + + private void initCounters() { + singleWildcards = 0; + doubleWildcards = 0; + int pos = 0; + while (pos < pathSpec.length()) { + char ch = pathSpec.charAt(pos); + if (ch == '{') { + parameterCount++; + pos++; + } else if (ch == '*') { + if (pos + 1 < pathSpec.length() && pathSpec.charAt(pos + 1) == '*') { + doubleWildcards++; + pos += 2; + } else if (pos > 0 && !(pathSpec.charAt(pos - 1) == '.' && pathSpec.charAt(pos) == '*')) { + singleWildcards++; + pos++; + } else { + pos++; + } + } else { + pos++; + } + } + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/structure/PathComparator.java b/net-path/src/main/java/org/xbib/net/path/structure/PathComparator.java new file mode 100644 index 0000000..4c45244 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/structure/PathComparator.java @@ -0,0 +1,68 @@ +package org.xbib.net.path.structure; + +import java.util.Comparator; + +/** + * Path structure comparator. + */ +@SuppressWarnings("serial") +public class PathComparator implements Comparator { + + private final String path; + + public PathComparator(String path) { + this.path = path; + } + + @Override + public int compare(Path path1, Path path2) { + if (path1 == null) { + if (path2 == null) { + return 0; + } else { + return 1; + } + } + if (path2 == null) { + return -1; + } + if (path1.isLeastSpecific() && path2.isLeastSpecific()) { + return 0; + } else if (path1.isLeastSpecific()) { + return 1; + } else if (path2.isLeastSpecific()) { + return -1; + } + boolean pattern1EqualsPath = path1.getPathSpec().equals(path); + boolean pattern2EqualsPath = path2.getPathSpec().equals(path); + if (pattern1EqualsPath && pattern2EqualsPath) { + return 0; + } else if (pattern1EqualsPath) { + return -1; + } else if (pattern2EqualsPath) { + return 1; + } + if (path1.isPrefixPattern() && path2.getDoubleWildcards() == 0) { + return 1; + } else if (path2.isPrefixPattern() && path1.getDoubleWildcards() == 0) { + return -1; + } + if (path1.getTotalCount() != path2.getTotalCount()) { + return path1.getTotalCount() - path2.getTotalCount(); + } + if (path1.getLength() != path2.getLength()) { + return path2.getLength() - path1.getLength(); + } + if (path1.getSingleWildcards() < path2.getSingleWildcards()) { + return -1; + } else if (path2.getSingleWildcards() < path1.getSingleWildcards()) { + return 1; + } + if (path1.getParameterCount() < path2.getParameterCount()) { + return -1; + } else if (path2.getParameterCount() < path1.getParameterCount()) { + return 1; + } + return 0; + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/structure/PathMatcher.java b/net-path/src/main/java/org/xbib/net/path/structure/PathMatcher.java new file mode 100644 index 0000000..5a81a13 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/structure/PathMatcher.java @@ -0,0 +1,269 @@ +package org.xbib.net.path.structure; + +import org.xbib.net.Parameter; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.PathNormalizer; +import org.xbib.net.util.CharMatcher; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PathMatcher extends Path { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*\\*|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}"); + + private static final Pattern MATCH_ALL_PATTERN = Pattern.compile("(.*)"); + + private static final CharMatcher RESERVED_URL_CHARS = CharMatcher.anyOf(":/?#[]{}"); + + private static final String DEFAULT_PATTERN = "(.*)"; + + private static final String WILDCARD = "*"; + + private final boolean fullMatch; + + private List analyzedSegments; + + private final ParameterBuilder parameterBuilder; + + public PathMatcher() { + this(null, "/", true, true, true, + Parameter.builder().domain("PATH").enableSort()); + } + + public PathMatcher(String pathSpec, + String pathSeparator, + boolean trimTokens, + boolean caseSensitive, + boolean fullMatch, + ParameterBuilder parameterBuilder) { + super.init(pathSpec, pathSeparator, trimTokens, caseSensitive); + this.analyzedSegments = analyzeTokens(); + this.fullMatch = fullMatch; + this.parameterBuilder = parameterBuilder; + } + + public static PathMatcher of(String pathSpec) { + return new PathMatcher(pathSpec, "/", true, true, true, + Parameter.builder().domain("PATH").enableSort()); + } + + public void setPathSeparator(String pathSeparator) { + this.pathSeparator = pathSeparator; + } + + public Parameter getParameter() { + return parameterBuilder.build(); + } + + public List getAnalyzedSegments() { + return analyzedSegments; + } + + public boolean match(String pathSpec, String path) { + super.init(pathSpec, pathSeparator, trimTokens, caseSensitive); + this.analyzedSegments = analyzeTokens(); + return match(path); + } + + public boolean match(String path) { + String normalizedPath = PathNormalizer.normalize(path); + if (normalizedPath.startsWith(pathSeparator) != pathSpec.startsWith(pathSeparator)) { + return false; + } + List analyzedSegments = getAnalyzedSegments(); + List pathSegments = tokenize(normalizedPath); + int patternStart = 0; + int patternEnd = analyzedSegments.size() - 1; + int pathStart = 0; + int pathEnd = pathSegments.size() - 1; + while (patternStart <= patternEnd && pathStart <= pathEnd) { + PathSegment pathSegment = analyzedSegments.get(patternStart); + if (pathSegment.isCatchAll()) { + break; + } + boolean b = matchAndExtractVariables(pathSegment, pathSegments.get(pathStart)); + if (!b) { + return false; + } + patternStart++; + pathStart++; + } + if (pathStart > pathEnd) { + if (patternStart > patternEnd) { + return pathSpec.endsWith(pathSeparator) == normalizedPath.endsWith(pathSeparator); + } + if (!fullMatch) { + return true; + } + if (patternStart == patternEnd + && analyzedSegments.get(patternStart).getString().equals(WILDCARD) + && normalizedPath.endsWith(pathSeparator)) { + return true; + } + for (int i = patternStart; i <= patternEnd; i++) { + if (!analyzedSegments.get(i).isCatchAll()) { + return false; + } + } + return true; + } else if (patternStart > patternEnd) { + return false; + } else if (!fullMatch && analyzedSegments.get(patternStart).isCatchAll()) { + return true; + } + while (patternStart <= patternEnd && pathStart <= pathEnd) { + PathSegment pathSegment = analyzedSegments.get(patternEnd); + if (pathSegment.isCatchAll()) { + break; + } + boolean b = matchAndExtractVariables(pathSegment, pathSegments.get(pathEnd)); + if (!b) { + return false; + } + patternEnd--; + pathEnd--; + } + if (pathStart > pathEnd) { + for (int i = patternStart; i <= patternEnd; i++) { + if (!analyzedSegments.get(i).isCatchAll()) { + return false; + } + } + return true; + } + while (patternStart != patternEnd && pathStart <= pathEnd) { + int patternIndexTemp = -1; + for (int i = patternStart + 1; i <= patternEnd; i++) { + if (analyzedSegments.get(i).isCatchAll()) { + patternIndexTemp = i; + break; + } + } + if (patternIndexTemp == patternStart + 1) { + patternStart++; + continue; + } + int patternLength = patternIndexTemp - patternStart - 1; + int strLength = pathEnd - pathStart + 1; + int foundIndex = -1; + boolean loop = true; + while (loop) { + for (int i = 0; i <= strLength - patternLength; i++) { + for (int j = 0; j < patternLength; j++) { + PathSegment segment = pathSegments.get(pathStart + i + j); + PathSegment pathSegment = analyzedSegments.get(patternStart + j + 1); + boolean b = matchAndExtractVariables(pathSegment, segment); + if (b) { + loop = false; + break; + } + } + if (loop) { + foundIndex = pathStart + i; + } else { + break; + } + } + } + if (foundIndex == -1) { + return false; + } + patternStart = patternIndexTemp; + pathStart = foundIndex + patternLength; + } + for (int i = patternStart; i <= patternEnd; i++) { + if (!analyzedSegments.get(i).isCatchAll()) { + return false; + } + } + return true; + } + + private boolean matchAndExtractVariables(PathSegment patternSegment, PathSegment pathSegment) { + if (patternSegment.getPattern() == null) { + return true; + } + Matcher matcher = patternSegment.getPattern().matcher(pathSegment.getString()); + if (!matcher.matches()) { + return false; + } else { + if (patternSegment.getParameterNames() == null) { + return true; + } + if (patternSegment.getParameterNames().size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + patternSegment.getString() + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = patternSegment.getParameterNames().get(i - 1); + String value = matcher.group(i); + parameterBuilder.add(name, value); + } + return true; + } + } + + private List analyzeTokens() { + List pathSegments = new ArrayList<>(); + for (PathSegment pathSegment : segments) { + String token = pathSegment.getString(); + List parameterNames = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(token); + int start = 0; + boolean isPattern = false; + boolean isCatchAll = false; + while (matcher.find()) { + sb.append(quote(token, start, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + sb.append('.'); + isPattern = true; + } else if ("*".equals(match)) { + sb.append(".*"); + isPattern = true; + } else if (CATCH_ALL.equals(match)) { + isCatchAll = true; + } else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + sb.append(DEFAULT_PATTERN); + parameterNames.add(matcher.group(1)); + } else { + String parameterPattern = match.substring(colonIdx + 1, match.length() - 1); + sb.append('(').append(parameterPattern).append(')'); + parameterNames.add(match.substring(1, colonIdx)); + } + } else { + if (RESERVED_URL_CHARS.matchesAnyOf(match)) { + throw new IllegalArgumentException("found reserved chars in " + match); + } + } + start = matcher.end(); + } + sb.append(quote(token, start, token.length())); + if (isPattern) { + pathSegment.setPattern(caseSensitive ? Pattern.compile(sb.toString()) : + Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE)); + } + if (!parameterNames.isEmpty()) { + pathSegment.setParameterNames(parameterNames); + pathSegment.setPattern(MATCH_ALL_PATTERN); + } + if (isCatchAll) { + pathSegment.setCatchAll(isCatchAll); + } + pathSegments.add(pathSegment); + } + return pathSegments; + } + + private static String quote(String s, int start, int end) { + return start == end ? "" : Pattern.quote(s.substring(start, end)); + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/structure/PathResolver.java b/net-path/src/main/java/org/xbib/net/path/structure/PathResolver.java new file mode 100644 index 0000000..fc06e1e --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/structure/PathResolver.java @@ -0,0 +1,246 @@ +package org.xbib.net.path.structure; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.Parameter; +import org.xbib.net.ParameterBuilder; +import org.xbib.net.PathNormalizer; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; + +/** + * A resolver that maps path specifications to arbitrary objects using a trie structure. + * Each node in the tree is a pattern segment. For example, given a path "discovery/v1/apis", the data would + * be stored in the node path represented by "discovery" -> "v1" -> "apis". + * + * @param type + */ +public class PathResolver implements org.xbib.net.path.PathResolver { + + private static final Logger logger = Logger.getLogger(PathResolver.class.getName()); + + private static final String PATH_DOMAIN = "PATH"; + + private final Builder builder; + + private final Map> children; + + private PathResolver(Builder builder) { + this.builder = builder; + this.children = new LinkedHashMap<>(); + for (Map.Entry> entry : builder.children.entrySet()) { + children.put(entry.getKey(), new PathResolver<>(entry.getValue())); + } + } + + @Override + public void resolve(String method, String path, ResultListener listener) { + Objects.requireNonNull(method, "method"); + Objects.requireNonNull(path, "path"); + List pathSegments = PathMatcher.tokenize(PathNormalizer.normalize(path), + builder.pathSeparator, builder.trimTokens, builder.caseSensitive); + ParameterBuilder parameterBuilder = Parameter.builder().domain(PATH_DOMAIN).enableSort(); + resolve(pathSegments, 0, parameterBuilder, listener); + } + + @Override + public String toString() { + return "PathResolver[builder = " + builder + ", path resolver map = " + children + ']'; + } + + private ParameterBuilder resolve(List pathSegments, + int index, + ParameterBuilder parameterBuilder, + ResultListener listener) { + ParameterBuilder pb = parameterBuilder; + if (index < pathSegments.size()) { + PathSegment segment = pathSegments.get(index); + List> list = new ArrayList<>(); + boolean shortCircuit = match(segment, list, pb); + if (list.isEmpty()) { + pb = Parameter.builder().domain(PATH_DOMAIN).enableSort(); + } + if (shortCircuit) { + PathResolver pathResolver = list.get(list.size() - 1); + if (pathResolver.builder.value != null) { + T value = pathResolver.builder.value; + if (listener != null) { + listener.onResult(new Result<>(value, pb.build(), pathResolver.builder.method)); + pb = Parameter.builder().domain(PATH_DOMAIN).enableSort(); + } + } + } else { + for (PathResolver pathResolver : list) { + pb = pathResolver.resolve(pathSegments, index + 1, pb, listener); + } + } + } else { + if (builder.value != null) { + T value = builder.value; + if (listener != null) { + listener.onResult(new Result<>(value, pb.build(), builder.method)); + pb = Parameter.builder().domain(PATH_DOMAIN).enableSort(); + } + } + } + return pb; + } + + private boolean match(PathSegment segment, + List> list, + ParameterBuilder parameterBuilder) { + boolean lastSegment = false; + int i = 0; + int size = children.size(); + for (PathSegment pathSegment : children.keySet()) { + if (pathSegment.getParameterNames() != null) { + matchAndExtractVariables(parameterBuilder, pathSegment, segment); + PathResolver pathResolver = children.get(pathSegment); + list.add(pathResolver); + } else if (pathSegment.getPattern() != null) { + if (pathSegment.getPattern().matcher(segment.getString()).matches()) { + list.add(children.get(pathSegment)); + } + } else if (pathSegment.getString().equals(segment.getString())) { + list.add(children.get(pathSegment)); + } else if (pathSegment.isCatchAll()) { + list.add(children.get(pathSegment)); + lastSegment = i == size - 1; + } + i++; + } + logger.log(Level.INFO, "size = " + size + " lastSegment = " + lastSegment + " list.size() = " + list.size()); + return lastSegment; + } + + private void matchAndExtractVariables(ParameterBuilder parameterBuilder, PathSegment patternSegment, PathSegment pathSegment) { + if (patternSegment.getPattern() == null) { + return; + } + Matcher matcher = patternSegment.getPattern().matcher(pathSegment.getString()); + if (matcher.matches()) { + if (patternSegment.getParameterNames() == null) { + return; + } + if (patternSegment.getParameterNames().size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + patternSegment.getString() + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = patternSegment.getParameterNames().get(i - 1); + String value = matcher.group(i); + parameterBuilder.add(name, value); + } + } + } + + public static class Result implements org.xbib.net.path.PathResolver.Result { + + private final T value; + + private final Parameter parameter; + + private final String method; + + Result(T value, Parameter parameter, String method) { + this.value = value; + this.parameter = parameter; + this.method = method; + } + + @Override + public T getValue() { + return value; + } + + @Override + public Parameter getParameter() { + return parameter; + } + + @Override + public String getMethod() { + return method; + } + + @Override + public String toString() { + return value != null ? value.toString() : null; + } + } + + public static Builder builder() { + return new Builder<>(); + } + + public static class Builder { + + private final Map> children; + + private String pathSeparator; + + private boolean trimTokens; + + private boolean caseSensitive; + + private T value; + + private String method; + + Builder() { + this.children = new LinkedHashMap<>(); + this.pathSeparator = "/"; + this.trimTokens = true; + this.caseSensitive = true; + } + + public Builder pathSeparator(String pathSeparator) { + this.pathSeparator = pathSeparator; + return this; + } + + public Builder trimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + return this; + } + + public Builder caseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + return this; + } + + public Builder add(String method, String pathSpec, T value) { + Objects.requireNonNull(method, "method"); + Objects.requireNonNull(pathSpec, "pathSpec"); + Objects.requireNonNull(value, "value"); + PathMatcher pathMatcher = new PathMatcher(pathSpec, pathSeparator, trimTokens, caseSensitive, true, + Parameter.builder().domain(PATH_DOMAIN).enableSort()); + add(pathMatcher.getAnalyzedSegments(), value, method, 0); + return this; + } + + public PathResolver build() { + return new PathResolver<>(this); + } + + private void add(List pathSegments, T value, String method, int index) { + if (index < pathSegments.size()) { + children.computeIfAbsent(pathSegments.get(index), k -> new Builder() + .pathSeparator(pathSeparator) + .trimTokens(trimTokens) + .caseSensitive(caseSensitive) + ).add(pathSegments, value, method, index + 1); + } else { + this.value = value; + this.method = method; + } + } + } +} diff --git a/net-path/src/main/java/org/xbib/net/path/structure/PathSegment.java b/net-path/src/main/java/org/xbib/net/path/structure/PathSegment.java new file mode 100644 index 0000000..985f488 --- /dev/null +++ b/net-path/src/main/java/org/xbib/net/path/structure/PathSegment.java @@ -0,0 +1,67 @@ +package org.xbib.net.path.structure; + +import java.util.List; +import java.util.regex.Pattern; + +public class PathSegment implements Comparable { + + private String string; + + private Pattern pattern; + + private List parameterNames; + + private boolean isCatchAll; + + public PathSegment() { + } + + public void setString(String string) { + this.string = string; + } + + public String getString() { + return string; + } + + public void setPattern(Pattern pattern) { + this.pattern = pattern; + } + + public Pattern getPattern() { + return pattern; + } + + public void setParameterNames(List parameterNames) { + this.parameterNames = parameterNames; + } + + public List getParameterNames() { + return parameterNames; + } + + public void setCatchAll(boolean catchAll) { + this.isCatchAll = catchAll; + } + + public boolean isCatchAll() { + return isCatchAll; + } + + @Override + public String toString() { + return "PathSegment[" + + "string='" + string + '\'' + + ", pattern=" + pattern + + ", parameterNames=" + parameterNames + + ", isCatchAll=" + isCatchAll + + "]"; + } + + @Override + public int compareTo(PathSegment o) { + Integer prio1 = isCatchAll ? 0 : pattern != null ? 1 : string != null ? 2 : -1; + Integer prio2 = o.isCatchAll ? 0 : o.pattern != null ? 1 : o.string != null ? 2 : -1; + return prio1.compareTo(prio2); + } +} diff --git a/net-path/src/test/java/org/xbib/net/path/PathDecoderTest.java b/net-path/src/test/java/org/xbib/net/path/PathDecoderTest.java new file mode 100644 index 0000000..41f350c --- /dev/null +++ b/net-path/src/test/java/org/xbib/net/path/PathDecoderTest.java @@ -0,0 +1,64 @@ +package org.xbib.net.path; + +import org.junit.jupiter.api.Test; +import org.xbib.net.URL; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class PathDecoderTest { + + @Test + void testPlusSign() throws Exception { + PathDecoder decoder = new PathDecoder("/path?a=b+c", "d=e+f"); + assertEquals("[b c]", decoder.getParameter().getAll("DEFAULT", "a").toString()); + assertEquals("[e f]", decoder.getParameter().getAll("DEFAULT", "d").toString()); + } + + @Test + void testSlash() throws Exception { + PathDecoder decoder = new PathDecoder("path/foo/bar/?a=b+c", "d=e+f"); + assertEquals("[b c]", decoder.getParameter().getAll("DEFAULT", "a").toString()); + assertEquals("[e f]", decoder.getParameter().getAll("DEFAULT", "d").toString()); + } + + @Test + void testDoubleSlashes() throws Exception { + PathDecoder decoder = new PathDecoder("//path", ""); + assertEquals("/path", decoder.path()); + } + + @Test + void testSlashes() throws Exception { + PathDecoder decoder = new PathDecoder("//path?a=b+c", "d=e+f"); + assertEquals("/path", decoder.path()); + assertEquals("[b c]", decoder.getParameter().getAll("DEFAULT", "a").toString()); + assertEquals("[e f]", decoder.getParameter().getAll("DEFAULT", "d").toString()); + } + + @Test + void testPlusPercent() throws Exception { + PathDecoder decoder = new PathDecoder("//path?a=b%2Bc", "d=e%2Bf"); + assertEquals("/path", decoder.path()); + assertEquals("[b+c]", decoder.getParameter().getAll("DEFAULT", "a").toString()); + assertEquals("[e+f]", decoder.getParameter().getAll("DEFAULT", "d").toString()); + } + + @Test + void decodeURL() { + String requestURI = "/pdfconverter/index.gtpl?x-fl-key=20190035592&x-source=ftp://dummy@xbib.org/upload/20190035592/20190035592.pdf&x-fl-target=ftp://dummy@xbib.org/fl/download/20190035592/Fernleihe_Kopienlieferung_null_FB201900373_BLQDMT62_20190035592_20190035592.pdf&x-fl-copy=&x-fl-ack=https://xbib.org/ack/&x-fl-pages=1-"; + URL url = URL.builder().path(requestURI).build(); + assertNull(url.getHost()); + assertNull(url.getPort()); + assertEquals("/pdfconverter/index.gtpl", url.getPath()); + assertNull(url.getFragment()); + PathDecoder decoder = new PathDecoder(requestURI); + if (url.getQuery() != null) { + decoder.parse(url.getDecodedQuery()); + } + assertEquals("x-fl-key=20190035592&x-source=ftp://dummy@xbib.org/upload/20190035592/20190035592.pdf&x-fl-target=ftp://dummy@xbib.org/fl/download/20190035592/Fernleihe_Kopienlieferung_null_FB201900373_BLQDMT62_20190035592_20190035592.pdf&x-fl-copy=&x-fl-ack=https://xbib.org/ack/&x-fl-pages=1-", url.getDecodedQuery()); + assertEquals("[x-fl-key=20190035592, x-source=ftp://dummy@xbib.org/upload/20190035592/20190035592.pdf, x-fl-target=ftp://dummy@xbib.org/fl/download/20190035592/Fernleihe_Kopienlieferung_null_FB201900373_BLQDMT62_20190035592_20190035592.pdf, x-fl-copy=, x-fl-ack=https://xbib.org/ack/, x-fl-pages=1-]", decoder.getParameter().toString()); + url = URL.from(decoder.getParameter().getAll("DEFAULT", "x-fl-target").get(0).toString()); + assertEquals("ftp://dummy@xbib.org/fl/download/20190035592/Fernleihe_Kopienlieferung_null_FB201900373_BLQDMT62_20190035592_20190035592.pdf", url.toString()); + } +} diff --git a/net-path/src/test/java/org/xbib/net/path/simple/PathMatcherTest.java b/net-path/src/test/java/org/xbib/net/path/simple/PathMatcherTest.java new file mode 100644 index 0000000..4634b6a --- /dev/null +++ b/net-path/src/test/java/org/xbib/net/path/simple/PathMatcherTest.java @@ -0,0 +1,423 @@ +package org.xbib.net.path.simple; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.xbib.net.Parameter; +import org.xbib.net.path.simple.PathMatcher; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PathMatcherTest { + + private final PathMatcher pathMatcher = new PathMatcher(); + + @Test + void match() { + // test exact matching + assertTrue(pathMatcher.match("test", "test")); + assertTrue(pathMatcher.match("/test", "/test")); + assertFalse(pathMatcher.match("/test.jpg", "test.jpg")); + assertFalse(pathMatcher.match("test", "/test")); + assertFalse(pathMatcher.match("/test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.match("t?st", "test")); + assertTrue(pathMatcher.match("??st", "test")); + assertTrue(pathMatcher.match("tes?", "test")); + assertTrue(pathMatcher.match("te??", "test")); + assertTrue(pathMatcher.match("?es?", "test")); + assertFalse(pathMatcher.match("tes?", "tes")); + assertFalse(pathMatcher.match("tes?", "testt")); + assertFalse(pathMatcher.match("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.match("*", "test")); + assertTrue(pathMatcher.match("test*", "test")); + assertTrue(pathMatcher.match("test*", "testTest")); + assertTrue(pathMatcher.match("test/*", "test/Test")); + assertTrue(pathMatcher.match("test/*", "test/t")); + assertTrue(pathMatcher.match("test/*", "test/")); + assertTrue(pathMatcher.match("*test*", "AnothertestTest")); + assertTrue(pathMatcher.match("*test", "Anothertest")); + assertTrue(pathMatcher.match("*.*", "test.")); + assertTrue(pathMatcher.match("*.*", "test.test")); + assertTrue(pathMatcher.match("*.*", "test.test.test")); + assertTrue(pathMatcher.match("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.match("test*", "tst")); + assertFalse(pathMatcher.match("test*", "tsttest")); + assertFalse(pathMatcher.match("test*", "test/")); + assertFalse(pathMatcher.match("test*", "test/t")); + assertFalse(pathMatcher.match("test/*", "test")); + assertFalse(pathMatcher.match("*test*", "tsttst")); + assertFalse(pathMatcher.match("*test", "tsttst")); + assertFalse(pathMatcher.match("*.*", "tsttst")); + assertFalse(pathMatcher.match("test*aaa", "test")); + assertFalse(pathMatcher.match("test*aaa", "testblaaab")); + + // test matching with ?'s and /'s + assertTrue(pathMatcher.match("/?", "/a")); + assertTrue(pathMatcher.match("/?/a", "/a/a")); + assertTrue(pathMatcher.match("/a/?", "/a/b")); + assertTrue(pathMatcher.match("/??/a", "/aa/a")); + assertTrue(pathMatcher.match("/a/??", "/a/bb")); + assertTrue(pathMatcher.match("/?", "/a")); + + // test matching with **'s + assertTrue(pathMatcher.match("/**", "/testing/testing")); + assertTrue(pathMatcher.match("/*/**", "/testing/testing")); + assertTrue(pathMatcher.match("/**/*", "/testing/testing")); + assertTrue(pathMatcher.match("/bla/**/bla", "/bla/testing/testing/bla")); + assertTrue(pathMatcher.match("/bla/**/bla", "/bla/testing/testing/bla/bla")); + assertTrue(pathMatcher.match("/**/test", "/bla/bla/test")); + assertTrue(pathMatcher.match("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla")); + assertTrue(pathMatcher.match("/bla*bla/test", "/blaXXXbla/test")); + assertTrue(pathMatcher.match("/*bla/test", "/XXXbla/test")); + assertFalse(pathMatcher.match("/bla*bla/test", "/blaXXXbl/test")); + assertFalse(pathMatcher.match("/*bla/test", "XXXblab/test")); + assertFalse(pathMatcher.match("/*bla/test", "XXXbl/test")); + + assertFalse(pathMatcher.match("/????", "/bala/bla")); + assertFalse(pathMatcher.match("/**/*bla", "/bla/bla/bla/bbb")); + + assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.match("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg")); + + assertTrue(pathMatcher.match("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.match("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.match("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing")); + assertFalse(pathMatcher.match("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing")); + + assertFalse(pathMatcher.match("/x/x/**/bla", "/x/x/x/")); + + assertTrue(pathMatcher.match("", "")); + + assertTrue(pathMatcher.match("/{bla}.*", "/testing.html")); + } + + @Test + void withMatchStart() { + // test exact matching + assertTrue(pathMatcher.matchStart("test", "test")); + assertTrue(pathMatcher.matchStart("/test", "/test")); + assertFalse(pathMatcher.matchStart("/test.jpg", "test.jpg")); + assertFalse(pathMatcher.matchStart("test", "/test")); + assertFalse(pathMatcher.matchStart("/test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.matchStart("t?st", "test")); + assertTrue(pathMatcher.matchStart("??st", "test")); + assertTrue(pathMatcher.matchStart("tes?", "test")); + assertTrue(pathMatcher.matchStart("te??", "test")); + assertTrue(pathMatcher.matchStart("?es?", "test")); + assertFalse(pathMatcher.matchStart("tes?", "tes")); + assertFalse(pathMatcher.matchStart("tes?", "testt")); + assertFalse(pathMatcher.matchStart("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.matchStart("*", "test")); + assertTrue(pathMatcher.matchStart("test*", "test")); + assertTrue(pathMatcher.matchStart("test*", "testTest")); + assertTrue(pathMatcher.matchStart("test/*", "test/Test")); + assertTrue(pathMatcher.matchStart("test/*", "test/t")); + assertTrue(pathMatcher.matchStart("test/*", "test/")); + assertTrue(pathMatcher.matchStart("*test*", "AnothertestTest")); + assertTrue(pathMatcher.matchStart("*test", "Anothertest")); + assertTrue(pathMatcher.matchStart("*.*", "test.")); + assertTrue(pathMatcher.matchStart("*.*", "test.test")); + assertTrue(pathMatcher.matchStart("*.*", "test.test.test")); + assertTrue(pathMatcher.matchStart("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.matchStart("test*", "tst")); + assertFalse(pathMatcher.matchStart("test*", "test/")); + assertFalse(pathMatcher.matchStart("test*", "tsttest")); + assertFalse(pathMatcher.matchStart("test*", "test/")); + assertFalse(pathMatcher.matchStart("test*", "test/t")); + assertTrue(pathMatcher.matchStart("test/*", "test")); + assertTrue(pathMatcher.matchStart("test/t*.txt", "test")); + assertFalse(pathMatcher.matchStart("*test*", "tsttst")); + assertFalse(pathMatcher.matchStart("*test", "tsttst")); + assertFalse(pathMatcher.matchStart("*.*", "tsttst")); + assertFalse(pathMatcher.matchStart("test*aaa", "test")); + assertFalse(pathMatcher.matchStart("test*aaa", "testblaaab")); + + // test matching with ?'s and /'s + assertTrue(pathMatcher.matchStart("/?", "/a")); + assertTrue(pathMatcher.matchStart("/?/a", "/a/a")); + assertTrue(pathMatcher.matchStart("/a/?", "/a/b")); + assertTrue(pathMatcher.matchStart("/??/a", "/aa/a")); + assertTrue(pathMatcher.matchStart("/a/??", "/a/bb")); + assertTrue(pathMatcher.matchStart("/?", "/a")); + + // test matching with **'s + assertTrue(pathMatcher.matchStart("/**", "/testing/testing")); + assertTrue(pathMatcher.matchStart("/*/**", "/testing/testing")); + assertTrue(pathMatcher.matchStart("/**/*", "/testing/testing")); + assertTrue(pathMatcher.matchStart("test*/**", "test/")); + assertTrue(pathMatcher.matchStart("test*/**", "test/t")); + assertTrue(pathMatcher.matchStart("/bla/**/bla", "/bla/testing/testing/bla")); + assertTrue(pathMatcher.matchStart("/bla/**/bla", "/bla/testing/testing/bla/bla")); + assertTrue(pathMatcher.matchStart("/**/test", "/bla/bla/test")); + assertTrue(pathMatcher.matchStart("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla")); + assertTrue(pathMatcher.matchStart("/bla*bla/test", "/blaXXXbla/test")); + assertTrue(pathMatcher.matchStart("/*bla/test", "/XXXbla/test")); + assertFalse(pathMatcher.matchStart("/bla*bla/test", "/blaXXXbl/test")); + assertFalse(pathMatcher.matchStart("/*bla/test", "XXXblab/test")); + assertFalse(pathMatcher.matchStart("/*bla/test", "XXXbl/test")); + + assertFalse(pathMatcher.matchStart("/????", "/bala/bla")); + assertTrue(pathMatcher.matchStart("/**/*bla", "/bla/bla/bla/bbb")); + + assertTrue(pathMatcher.matchStart("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.matchStart("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.matchStart("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.matchStart("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg")); + + assertTrue(pathMatcher.matchStart("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.matchStart("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.matchStart("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.matchStart("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing")); + + assertTrue(pathMatcher.matchStart("/x/x/**/bla", "/x/x/x/")); + + assertTrue(pathMatcher.matchStart("", "")); + } + + @Test + void uniqueDeliminator() { + pathMatcher.setPathSeparator("."); + + // test exact matching + assertTrue(pathMatcher.match("test", "test")); + assertTrue(pathMatcher.match(".test", ".test")); + assertFalse(pathMatcher.match(".test/jpg", "test/jpg")); + assertFalse(pathMatcher.match("test", ".test")); + assertFalse(pathMatcher.match(".test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.match("t?st", "test")); + assertTrue(pathMatcher.match("??st", "test")); + assertTrue(pathMatcher.match("tes?", "test")); + assertTrue(pathMatcher.match("te??", "test")); + assertTrue(pathMatcher.match("?es?", "test")); + assertFalse(pathMatcher.match("tes?", "tes")); + assertFalse(pathMatcher.match("tes?", "testt")); + assertFalse(pathMatcher.match("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.match("*", "test")); + assertTrue(pathMatcher.match("test*", "test")); + assertTrue(pathMatcher.match("test*", "testTest")); + assertTrue(pathMatcher.match("*test*", "AnothertestTest")); + assertTrue(pathMatcher.match("*test", "Anothertest")); + assertTrue(pathMatcher.match("*/*", "test/")); + assertTrue(pathMatcher.match("*/*", "test/test")); + assertTrue(pathMatcher.match("*/*", "test/test/test")); + assertTrue(pathMatcher.match("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.match("test*", "tst")); + assertFalse(pathMatcher.match("test*", "tsttest")); + assertFalse(pathMatcher.match("*test*", "tsttst")); + assertFalse(pathMatcher.match("*test", "tsttst")); + assertFalse(pathMatcher.match("*/*", "tsttst")); + assertFalse(pathMatcher.match("test*aaa", "test")); + assertFalse(pathMatcher.match("test*aaa", "testblaaab")); + + // test matching with ?'s and .'s + assertTrue(pathMatcher.match(".?", ".a")); + assertTrue(pathMatcher.match(".?.a", ".a.a")); + assertTrue(pathMatcher.match(".a.?", ".a.b")); + assertTrue(pathMatcher.match(".??.a", ".aa.a")); + assertTrue(pathMatcher.match(".a.??", ".a.bb")); + assertTrue(pathMatcher.match(".?", ".a")); + + // test matching with **'s + assertTrue(pathMatcher.match(".**", ".testing.testing")); + assertTrue(pathMatcher.match(".*.**", ".testing.testing")); + assertTrue(pathMatcher.match(".**.*", ".testing.testing")); + assertTrue(pathMatcher.match(".bla.**.bla", ".bla.testing.testing.bla")); + assertTrue(pathMatcher.match(".bla.**.bla", ".bla.testing.testing.bla.bla")); + assertTrue(pathMatcher.match(".**.test", ".bla.bla.test")); + assertTrue(pathMatcher.match(".bla.**.**.bla", ".bla.bla.bla.bla.bla.bla")); + assertTrue(pathMatcher.match(".bla*bla.test", ".blaXXXbla.test")); + assertTrue(pathMatcher.match(".*bla.test", ".XXXbla.test")); + assertFalse(pathMatcher.match(".bla*bla.test", ".blaXXXbl.test")); + assertFalse(pathMatcher.match(".*bla.test", "XXXblab.test")); + assertFalse(pathMatcher.match(".*bla.test", "XXXbl.test")); + } + + @Test + void extractPathWithinPattern() throws Exception { + assertEquals("", + pathMatcher.extractPathWithinPattern("/docs/commit.html", "/docs/commit.html")); + assertEquals("cvs/commit", + pathMatcher.extractPathWithinPattern("/docs/*", "/docs/cvs/commit")); + assertEquals("commit.html", + pathMatcher.extractPathWithinPattern("/docs/cvs/*.html", "/docs/cvs/commit.html")); + assertEquals("cvs/commit", + pathMatcher.extractPathWithinPattern("/docs/**", "/docs/cvs/commit")); + assertEquals("cvs/commit.html", + pathMatcher.extractPathWithinPattern("/docs/**/*.html", "/docs/cvs/commit.html")); + assertEquals("commit.html", + pathMatcher.extractPathWithinPattern("/docs/**/*.html", "/docs/commit.html")); + assertEquals("commit.html", + pathMatcher.extractPathWithinPattern("/*.html", "/commit.html")); + assertEquals("docs/commit.html", + pathMatcher.extractPathWithinPattern("/*.html", "/docs/commit.html")); + assertEquals("/commit.html", + pathMatcher.extractPathWithinPattern("*.html", "/commit.html")); + assertEquals("/docs/commit.html", + pathMatcher.extractPathWithinPattern("*.html", "/docs/commit.html")); + assertEquals("/docs/commit.html", + pathMatcher.extractPathWithinPattern("**/*.*", "/docs/commit.html")); + assertEquals("/docs/commit.html", + pathMatcher.extractPathWithinPattern("*", "/docs/commit.html")); + assertEquals("/docs/cvs/other/commit.html", + pathMatcher.extractPathWithinPattern("**/commit.html", "/docs/cvs/other/commit.html")); + assertEquals("cvs/other/commit.html", + pathMatcher.extractPathWithinPattern("/docs/**/commit.html", "/docs/cvs/other/commit.html")); + assertEquals("cvs/other/commit.html", + pathMatcher.extractPathWithinPattern("/docs/**/**/**/**", "/docs/cvs/other/commit.html")); + + assertEquals("docs/cvs/commit", + pathMatcher.extractPathWithinPattern("/d?cs/*", "/docs/cvs/commit")); + assertEquals("cvs/commit.html", + pathMatcher.extractPathWithinPattern("/docs/c?s/*.html", "/docs/cvs/commit.html")); + assertEquals("docs/cvs/commit", + pathMatcher.extractPathWithinPattern("/d?cs/**", "/docs/cvs/commit")); + assertEquals("docs/cvs/commit.html", + pathMatcher.extractPathWithinPattern("/d?cs/**/*.html", "/docs/cvs/commit.html")); + } + + @Test + void extractUriTemplateVariables() throws Exception { + Parameter result = pathMatcher.extractUriTemplateVariables("/hotels/{hotel}", "/hotels/1"); + assertEquals("[hotel=1]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/h?tels/{hotel}", "/hotels/1"); + assertEquals("[hotel=1]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/hotels/{hotel}/bookings/{booking}", "/hotels/1/bookings/2"); + assertEquals("[hotel=1, booking=2]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/**/hotels/**/{hotel}", "/foo/hotels/bar/1"); + assertEquals("[hotel=1]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/{page}.html", "/42.html"); + assertEquals("[page=42]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/{page}.*", "/42.html"); + assertEquals("[page=42]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/A-{B}-C", "/A-b-C"); + assertEquals("[B=b]", result.toString()); + result = pathMatcher.extractUriTemplateVariables("/{name}.{extension}", "/test.html"); + assertEquals("[name=test, extension=html]", result.toString()); + } + + @Test + void extractUriTemplateVariablesRegex() { + Parameter result = pathMatcher.extractUriTemplateVariables("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar", + "com.example-1.0.0.jar"); + assertEquals("com.example", result.getAll("DEFAULT", "symbolicName").get(0)); + assertEquals("1.0.0", result.getAll("DEFAULT", "version").get(0)); + + result = pathMatcher.extractUriTemplateVariables("{symbolicName:[\\w\\.]+}-sources-{version:[\\w\\.]+}.jar", + "com.example-sources-1.0.0.jar"); + assertEquals("com.example", result.getAll("DEFAULT", "symbolicName").get(0)); + assertEquals("1.0.0", result.getAll("DEFAULT", "version").get(0)); + } + + @Test + void extractUriTemplateVarsRegexQualifiers() { + Parameter result = pathMatcher.extractUriTemplateVariables( + "{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar", + "com.example-sources-1.0.0.jar"); + assertEquals("com.example", result.getAll("DEFAULT", "symbolicName").get(0)); + assertEquals("1.0.0", result.getAll("DEFAULT", "version").get(0)); + result = pathMatcher.extractUriTemplateVariables( + "{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\.]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar", + "com.example-sources-1.0.0-20100220.jar"); + assertEquals("com.example", result.getAll("DEFAULT", "symbolicName").get(0)); + assertEquals("1.0.0", result.getAll("DEFAULT", "version").get(0)); + assertEquals("2010", result.getAll("DEFAULT", "year").get(0)); + assertEquals("02", result.getAll("DEFAULT", "month").get(0)); + assertEquals("20", result.getAll("DEFAULT", "day").get(0)); + result = pathMatcher.extractUriTemplateVariables( + "{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar", + "com.example-sources-1.0.0.{12}.jar"); + assertEquals("com.example", result.getAll("DEFAULT", "symbolicName").get(0)); + assertEquals("1.0.0.{12}", result.getAll("DEFAULT", "version").get(0)); + } + + @Test + void extractUriTemplateVarsRegexCapturingGroups() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + pathMatcher.extractUriTemplateVariables("/web/{id:foo(bar)?}", "/web/foobar"); + }); + } + + @Test + void testCombineMathing() { + assertEquals("", pathMatcher.combine(null, null)); + assertEquals("/hotels", pathMatcher.combine("/hotels", null)); + assertEquals("/hotels", pathMatcher.combine(null, "/hotels")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels/*", "booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels/*", "/booking")); + assertEquals("/hotels/**/booking", pathMatcher.combine("/hotels/**", "booking")); + assertEquals("/hotels/**/booking", pathMatcher.combine("/hotels/**", "/booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels", "/booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels", "booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels/", "booking")); + assertEquals("/hotels/{hotel}", pathMatcher.combine("/hotels/*", "{hotel}")); + assertEquals("/hotels/**/{hotel}", pathMatcher.combine("/hotels/**", "{hotel}")); + assertEquals("/hotels/{hotel}", pathMatcher.combine("/hotels", "{hotel}")); + assertEquals("/hotels/{hotel}.*", pathMatcher.combine("/hotels", "{hotel}.*")); + assertEquals("/hotels/*/booking/{booking}", pathMatcher.combine("/hotels/*/booking", "{booking}")); + assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel.html")); + assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel")); + assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel.*")); + assertEquals("/*.html", pathMatcher.combine("/**", "/*.html")); + assertEquals("/*.html", pathMatcher.combine("/*", "/*.html")); + assertEquals("/*.html", pathMatcher.combine("/*.*", "/*.html")); + assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); + assertEquals("/user/user", pathMatcher.combine("/user", "/user")); + assertEquals("/{foo:.*[^0-9].*}/edit/", pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")); + assertEquals("/1.0/foo/test", pathMatcher.combine("/1.0", "/foo/test")); + assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); + assertEquals("/hotel/booking", pathMatcher.combine("/hotel/", "/booking")); + } + + @Test + void combineWithTwoFileExtensionPatterns() { + Assertions.assertThrows(IllegalArgumentException.class, () ->{ + pathMatcher.combine("/*.html", "/*.txt"); + }); + } + + @Test + void testTrimTokensOff() { + pathMatcher.setCaseSensitive(true); + pathMatcher.setTrimTokens(false); + assertTrue(pathMatcher.match("/group/{groupName}/members", "/group/sales/members")); + assertTrue(pathMatcher.match("/group/{groupName}/members", "/group/ sales/members")); + } + + @Test + void testTrimTokensOff2() { + pathMatcher.setCaseSensitive(true); + pathMatcher.setTrimTokens(false); + assertFalse(pathMatcher.match("/group/{groupName}/members", "/Group/ Sales/Members")); + } + + @Test + void caseInsensitive() { + pathMatcher.setCaseSensitive(false); + pathMatcher.setTrimTokens(true); + assertTrue(pathMatcher.match("/group/{groupName}/members", "/group/sales/members")); + assertTrue(pathMatcher.match("/group/{groupName}/members", "/Group/Sales/Members")); + assertTrue(pathMatcher.match("/Group/{groupName}/Members", "/group/Sales/members")); + } + + @Test + void extensionMappingWithDotPathSeparator() { + pathMatcher.setPathSeparator("."); + assertEquals("/*.html.hotel.*", pathMatcher.combine("/*.html", "hotel.*")); + } +} + diff --git a/net-path/src/test/java/org/xbib/net/path/simple/PathResolverTest.java b/net-path/src/test/java/org/xbib/net/path/simple/PathResolverTest.java new file mode 100644 index 0000000..f93780d --- /dev/null +++ b/net-path/src/test/java/org/xbib/net/path/simple/PathResolverTest.java @@ -0,0 +1,280 @@ +package org.xbib.net.path.simple; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +class PathResolverTest { + + @Test + void simple() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "explorer", 1234) + .build(); + assertSuccessfulGetResolution(pathResolver, "explorer", 1234); + assertFailedGetResolution(pathResolver, "PUT", "explorer"); + assertFailedGetResolution(pathResolver, ""); + assertFailedGetResolution(pathResolver, "test"); + } + + @Test + void name() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "/static/{file}", 1234) + .add("HEAD", "/static/{file}", 1234) + .build(); + assertSuccessfulResolution(pathResolver, "GET", "/static/test.txt", + Set.of(1234), Map.of("file", "test.txt")); + } + + @Test + void glob() { + PathResolver trie = PathResolver.builder() + .add("GET", "/static/**", 1234) + .build(); + assertSuccessfulResolution(trie, "GET", "/static/test.txt", + Set.of(1234), Map.of()); + } + + @Test + void sharedPrefix() { + PathResolver trie = PathResolver.builder() + .add("GET", "discovery/v1/rest", 1234) + .add("GET", "discovery/v2/rest", 4321) + .build(); + assertSuccessfulGetResolution(trie, "discovery/v1/rest", 1234); + assertSuccessfulGetResolution(trie, "discovery/v2/rest", 4321); + assertFailedGetResolution(trie, ""); + assertFailedGetResolution(trie, "discovery"); + assertFailedGetResolution(trie, "discovery/v1"); + } + + @Test + void prefix() { + PathResolver trie = PathResolver.builder() + .add("GET", "discovery", 1234) + .add("GET", "discovery/v1", 4321) + .build(); + assertSuccessfulGetResolution(trie, "discovery", 1234); + assertSuccessfulGetResolution(trie, "discovery/v1", 4321); + assertFailedGetResolution(trie, ""); + } + + @Test + void parameter() { + PathResolver trie = PathResolver.builder() + .add("GET", "discovery/{version}/rest", 1234) + .build(); + assertSuccessfulGetResolution( + trie, "discovery/v1/rest", 1234, Map.of("version", "v1")); + } + + @Test + void multipleParameters() { + PathResolver trie = PathResolver.builder() + .add("GET", "discovery/{discovery_version}/apis/{api}/{format}", 1234) + .build(); + assertSuccessfulGetResolution(trie, "discovery/v1/apis/test/rest", 1234, + Map.of("discovery_version", "v1", "api", "test", "format", "rest")); + } + + @Test + void sharedParameterPrefix() { + PathResolver trie = PathResolver.builder() + .add("GET", "discovery/{version}/rest", 1234) + .add("GET", "discovery/{version}/rpc", 4321) + .build(); + assertSuccessfulGetResolution( + trie, "discovery/v1/rest", 1234, Map.of("version", "v1")); + assertSuccessfulGetResolution( + trie, "discovery/v1/rpc", 4321, Map.of("version", "v1")); + } + + @Test + void testResolveParameterAfterLiteral() { + PathResolver trie = PathResolver.builder() + .add("GET", "{one}/three", 1234) + .add("GET", "one/two", 4321) + .build(); + assertSuccessfulGetResolution(trie, "one/three", 1234, Map.of("one", "one")); + assertSuccessfulGetResolution(trie, "one/two", 4321); + } + + @Test + void testResolveBacktrack() { + PathResolver trie = PathResolver.builder() + .add("GET", "{one}/{two}/three/{four}", 1234) + .add("GET", "one/two/{three}/four", 4321) + .build(); + assertSuccessfulResolution(trie, "GET", "one/two/three/five", Set.of(1234, 4321), + Map.of("one", "one", "two", "two", "four", "five")); + } + + @Test + void pathMethodsWithDifferentParameterNames() { + PathResolver trie = PathResolver.builder() + .add("GET", "test/{one}", 1234) + .add("PUT", "test/{two}", 4321) + .build(); + assertSuccessfulResolution(trie, "GET", "test/foo", Set.of(1234, 4321), + Map.of("one", "foo")); + } + + @Test + void duplicatePath() { + doStrictDuplicateTest("test/path", "test/path"); + } + + @Test + void duplicateParameterizedPath() { + doStrictDuplicateTest("test/{param}/path", "test/{parameterized}/path"); + } + + @Test + void laxDuplicatePath() { + PathResolver pathResolver = PathResolver.builder(false) + .add("GET", "test/{one}", 1234) + .add("GET", "test/{two}", 4321) + .build(); + pathResolver.resolve("GET", "test/foo", result -> { + if (result.getParameter().containsKey("PATH", "one")) { + assertThat(result.getValue(), is(1234)); + assertThat(result.getParameter().get("PATH", "one"), is("foo")); + } else { + assertThat(result.getValue(), is(4321)); + assertThat(result.getParameter().get("PATH", "two"), is("foo")); + } + }); + } + + @Test + void builderNullPath() { + try { + PathResolver.builder().add("GET", null, 1234); + fail("expected NullPointerException"); + } catch (NullPointerException e) { + // expected + } + } + + @Test + void builderNullValue() { + try { + PathResolver.builder().add("GET", "throws/an/exception", null); + fail("expected NullPointerException"); + } catch (NullPointerException e) { + // expected + } + } + + @Test + void resolveNullPath() { + try { + PathResolver trie = PathResolver.builder().build(); + trie.resolve("GET", null, r -> {}); + fail("expected NullPointerException"); + } catch (NullPointerException e) { + // expected + } + } + + @Test + void invalidParameterName() { + try { + PathResolver.builder().add("GET", "bad/{[test}", 1234); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + void invalidPathParameterSyntax() { + try { + PathResolver.builder().add("GET", "bad/{test", 1234); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + void invalidParameterSegment() { + String invalids = "?#[]{}"; + for (char c : invalids.toCharArray()) { + try { + PathResolver.builder().add("GET", "bad/" + c, 1234); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + } + + @Test + void testFallback() { + AtomicInteger counter = new AtomicInteger(0); + org.xbib.net.path.PathResolver trie = PathResolver.builder() + .add("GET", "/test/{one}", 1) + .add("GET", "/**", 2) + .build(); + trie.resolve("GET", "/test/foo", r-> { + assertEquals(1, (int) r.getValue()); + counter.incrementAndGet(); + }); + assertThat(counter.get(), equalTo(1)); + } + + private void doStrictDuplicateTest(String path, String duplicatePath) { + try { + PathResolver.builder() + .add("GET", path, 1234) + .add("GET", duplicatePath, 4321); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + private void assertSuccessfulGetResolution(PathResolver trie, String path, Integer value) { + assertSuccessfulResolution(trie, "GET", path, value); + } + + private void assertSuccessfulResolution(PathResolver trie, String method, String path, Integer value) { + assertSuccessfulResolution(trie, method, path, Set.of(value), Collections.emptyMap()); + } + + private void assertSuccessfulGetResolution(PathResolver trie, String path, Integer value, + Map rawParameters) { + assertSuccessfulResolution(trie, "GET", path, Set.of(value), rawParameters); + } + + private void assertSuccessfulResolution(PathResolver trie, + String method, + String path, + Set values, + Map rawParameters) { + trie.resolve(method, path, result -> { + assertTrue(values.contains(result.getValue())); + //assertThat(result.getRawParameters(), is(rawParameters)); + }); + } + + private void assertFailedGetResolution(PathResolver trie, String path) { + assertFailedGetResolution(trie, "GET", path); + } + + private void assertFailedGetResolution(PathResolver trie, String method, String path) { + trie.resolve(method, path, r-> { fail(); }); + } +} diff --git a/net-path/src/test/java/org/xbib/net/path/simple/PathTest.java b/net-path/src/test/java/org/xbib/net/path/simple/PathTest.java new file mode 100644 index 0000000..985a76d --- /dev/null +++ b/net-path/src/test/java/org/xbib/net/path/simple/PathTest.java @@ -0,0 +1,145 @@ +package org.xbib.net.path.simple; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class PathTest { + + @Test + void testComparator() { + Comparator comparator = new PathComparator("/hotels/new"); + assertEquals(0, comparator.compare(null, null)); + assertEquals(1, comparator.compare(null, Path.of("/hotels/new"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/new"), null)); + assertEquals(0, comparator.compare(Path.of("/hotels/new"), Path.of("/hotels/new"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/new"), Path.of("/hotels/*"))); + assertEquals(1, comparator.compare(Path.of("/hotels/*"), Path.of("/hotels/new"))); + assertEquals(0, comparator.compare(Path.of("/hotels/*"), Path.of("/hotels/*"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/new"), Path.of("/hotels/{hotel}"))); + assertEquals(1, comparator.compare(Path.of("/hotels/{hotel}"), Path.of("/hotels/new"))); + assertEquals(0, comparator.compare(Path.of("/hotels/{hotel}"), Path.of("/hotels/{hotel}"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/{hotel}/booking"), Path.of("/hotels/{hotel}/bookings/{booking}"))); + assertEquals(1, comparator.compare(Path.of("/hotels/{hotel}/bookings/{booking}"), Path.of("/hotels/{hotel}/booking"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"), Path.of("/**"))); + assertEquals(1, comparator.compare(Path.of("/**"), Path.of("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"))); + assertEquals(0, comparator.compare(Path.of("/**"), Path.of("/**"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/{hotel}"), Path.of("/hotels/*"))); + assertEquals(1, comparator.compare(Path.of("/hotels/*"), Path.of("/hotels/{hotel}"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/*"), Path.of("/hotels/*/**"))); + assertEquals(1, comparator.compare(Path.of("/hotels/*/**"), Path.of("/hotels/*"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/new"), Path.of("/hotels/new.*"))); + assertEquals(2, comparator.compare(Path.of("/hotels/{hotel}"), Path.of("/hotels/{hotel}.*"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"), Path.of("/hotels/**"))); + assertEquals(1, comparator.compare(Path.of("/hotels/**"), Path.of("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"))); + assertEquals(1, comparator.compare(Path.of("/hotels/foo/bar/**"), Path.of("/hotels/{hotel}"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/{hotel}"), Path.of("/hotels/foo/bar/**"))); + assertEquals(2, comparator.compare(Path.of("/hotels/**/bookings/**"), Path.of("/hotels/**"))); + assertEquals(-2, comparator.compare(Path.of("/hotels/**"), Path.of("/hotels/**/bookings/**"))); + assertEquals(1, comparator.compare(Path.of("/**"), Path.of("/hotels/{hotel}"))); + assertEquals(1, comparator.compare(Path.of("/hotels"), Path.of("/hotels2"))); + assertEquals(-1, comparator.compare(Path.of("*"), Path.of("*/**"))); + assertEquals(1, comparator.compare(Path.of("*/**"), Path.of("*"))); + } + + @Test + void testPatternComparatorSort() { + Comparator comparator = new PathComparator("/hotels/new"); + + List paths = new ArrayList<>(); + paths.add(null); + paths.add(Path.of("/hotels/new")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPattern()); + assertNull(paths.get(1)); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/new")); + paths.add(null); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPattern()); + assertNull(paths.get(1)); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/*")); + paths.add(Path.of("/hotels/new")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPattern()); + assertEquals("/hotels/*", paths.get(1).getPattern()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/new")); + paths.add(Path.of("/hotels/*")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPattern()); + assertEquals("/hotels/*", paths.get(1).getPattern()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/**")); + paths.add(Path.of("/hotels/*")); + paths.sort(comparator); + assertEquals("/hotels/*", paths.get(0).getPattern()); + assertEquals("/hotels/**", paths.get(1).getPattern()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/*")); + paths.add(Path.of("/hotels/**")); + paths.sort(comparator); + assertEquals("/hotels/*", paths.get(0).getPattern()); + assertEquals("/hotels/**", paths.get(1).getPattern()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/{hotel}")); + paths.add(Path.of("/hotels/new")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPattern()); + assertEquals("/hotels/{hotel}", paths.get(1).getPattern()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/new")); + paths.add(Path.of("/hotels/{hotel}")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPattern()); + assertEquals("/hotels/{hotel}", paths.get(1).getPattern()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/*")); + paths.add(Path.of("/hotels/{hotel}")); + paths.add(Path.of("/hotels/new")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPattern()); + assertEquals("/hotels/{hotel}", paths.get(1).getPattern()); + assertEquals("/hotels/*", paths.get(2).getPattern()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/ne*")); + paths.add(Path.of("/hotels/n*")); + Collections.shuffle(paths); + paths.sort(comparator); + assertEquals("/hotels/ne*", paths.get(0).getPattern()); + assertEquals("/hotels/n*", paths.get(1).getPattern()); + + paths = new ArrayList<>(); + comparator = new PathComparator("/hotels/new.html"); + paths.add(Path.of("/hotels/new.*")); + paths.add(Path.of("/hotels/{hotel}")); + Collections.shuffle(paths); + paths.sort(comparator); + assertEquals("/hotels/new.*", paths.get(0).getPattern()); + assertEquals("/hotels/{hotel}", paths.get(1).getPattern()); + + paths = new ArrayList<>(); + comparator = new PathComparator("/web/endUser/action/login.html"); + paths.add(Path.of("/**/login.*")); + paths.add(Path.of("/**/endUser/action/login.*")); + paths.sort(comparator); + assertEquals("/**/endUser/action/login.*", paths.get(0).getPattern()); + assertEquals("/**/login.*", paths.get(1).getPattern()); + } +} diff --git a/net-path/src/test/java/org/xbib/net/path/spring/PathMatcherTest.java b/net-path/src/test/java/org/xbib/net/path/spring/PathMatcherTest.java new file mode 100644 index 0000000..1ac02ce --- /dev/null +++ b/net-path/src/test/java/org/xbib/net/path/spring/PathMatcherTest.java @@ -0,0 +1,336 @@ +package org.xbib.net.path.spring; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PathMatcherTest { + + @Test + void isMatchWithCaseSensitiveWithDefaultPathSeparator() { + + final PathPatternParser pathMatcher = new PathPatternParser(); + + // test exact matching + assertTrue(pathMatcher.match("test", "test")); + assertTrue(pathMatcher.match("/test", "/test")); + assertTrue(pathMatcher.match("http://example.org", "http://example.org")); // SPR-14141 + assertFalse(pathMatcher.match("/test.jpg", "test.jpg")); + assertFalse(pathMatcher.match("test", "/test")); + assertFalse(pathMatcher.match("/test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.match("t?st", "test")); + assertTrue(pathMatcher.match("??st", "test")); + assertTrue(pathMatcher.match("tes?", "test")); + assertTrue(pathMatcher.match("te??", "test")); + assertTrue(pathMatcher.match("?es?", "test")); + assertFalse(pathMatcher.match("tes?", "tes")); + assertFalse(pathMatcher.match("tes?", "testt")); + assertFalse(pathMatcher.match("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.match("*", "test")); + assertTrue(pathMatcher.match("test*", "test")); + assertTrue(pathMatcher.match("test*", "testTest")); + assertTrue(pathMatcher.match("test/*", "test/Test")); + assertTrue(pathMatcher.match("test/*", "test/t")); + assertTrue(pathMatcher.match("test/*", "test/")); + assertTrue(pathMatcher.match("*test*", "AnothertestTest")); + assertTrue(pathMatcher.match("*test", "Anothertest")); + assertTrue(pathMatcher.match("*.*", "test.")); + assertTrue(pathMatcher.match("*.*", "test.test")); + assertTrue(pathMatcher.match("*.*", "test.test.test")); + assertTrue(pathMatcher.match("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.match("test*", "tst")); + assertFalse(pathMatcher.match("test*", "tsttest")); + //assertFalse(pathMatcher.match("test*", "test/")); + assertFalse(pathMatcher.match("test*", "test/t")); + assertFalse(pathMatcher.match("test/*", "test")); + assertFalse(pathMatcher.match("*test*", "tsttst")); + assertFalse(pathMatcher.match("*test", "tsttst")); + assertFalse(pathMatcher.match("*.*", "tsttst")); + assertFalse(pathMatcher.match("test*aaa", "test")); + assertFalse(pathMatcher.match("test*aaa", "testblaaab")); + + // test matching with ?'s and /'s + assertTrue(pathMatcher.match("/?", "/a")); + assertTrue(pathMatcher.match("/?/a", "/a/a")); + assertTrue(pathMatcher.match("/a/?", "/a/b")); + assertTrue(pathMatcher.match("/??/a", "/aa/a")); + assertTrue(pathMatcher.match("/a/??", "/a/bb")); + assertTrue(pathMatcher.match("/?", "/a")); + + // test matching with **'s + assertTrue(pathMatcher.match("/**", "/testing/testing")); + assertTrue(pathMatcher.match("/*/**", "/testing/testing")); + //assertTrue(pathMatcher.match("/**/*", "/testing/testing")); + //assertTrue(pathMatcher.match("/bla/**/bla", "/bla/testing/testing/bla")); + //assertTrue(pathMatcher.match("/bla/**/bla", "/bla/testing/testing/bla/bla")); + //assertTrue(pathMatcher.match("/**/test", "/bla/bla/test")); + //assertTrue(pathMatcher.match("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla")); + assertTrue(pathMatcher.match("/bla*bla/test", "/blaXXXbla/test")); + assertTrue(pathMatcher.match("/*bla/test", "/XXXbla/test")); + assertFalse(pathMatcher.match("/bla*bla/test", "/blaXXXbl/test")); + assertFalse(pathMatcher.match("/*bla/test", "XXXblab/test")); + assertFalse(pathMatcher.match("/*bla/test", "XXXbl/test")); + + assertFalse(pathMatcher.match("/????", "/bala/bla")); + //assertFalse(pathMatcher.match("/**/*bla", "/bla/bla/bla/bbb")); + + // assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/")); + // assertTrue(pathMatcher.match("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing")); + // assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing")); + // assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg")); + + // assertTrue(pathMatcher.match("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/")); + // assertTrue(pathMatcher.match("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing")); + // assertTrue(pathMatcher.match("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing")); + // assertFalse(pathMatcher.match("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing")); + + //assertFalse(pathMatcher.match("/x/x/**/bla", "/x/x/x/")); + + assertTrue(pathMatcher.match("/foo/bar/**", "/foo/bar")); + + assertTrue(pathMatcher.match("", "")); + + assertTrue(pathMatcher.match("/foo/bar/**", "/foo/bar")); + assertTrue(pathMatcher.match("/resource/1", "/resource/1")); + assertTrue(pathMatcher.match("/resource/*", "/resource/1")); + assertTrue(pathMatcher.match("/resource/*/", "/resource/1/")); + assertTrue(pathMatcher.match("/top-resource/*/resource/*/sub-resource/*", "/top-resource/1/resource/2/sub-resource/3")); + assertTrue(pathMatcher.match("/top-resource/*/resource/*/sub-resource/*", "/top-resource/999999/resource/8888888/sub-resource/77777777")); + assertTrue(pathMatcher.match("/*/*/*/*/secret.html", "/this/is/protected/path/secret.html")); + assertTrue(pathMatcher.match("/*/*/*/*/*.html", "/this/is/protected/path/secret.html")); + assertFalse(pathMatcher.match("*.html", "/this/is/protected/path/secret.html")); + assertTrue(pathMatcher.match("/*/*/*/*", "/this/is/protected/path")); + assertFalse(pathMatcher.match("/*.txt", "/path/my.txt")); + //assertTrue(pathMatcher.match("org/springframework/**/*.jsp", "org/springframework/web/views/hello.jsp")); + //assertTrue(pathMatcher.match("org/springframework/**/*.jsp", "org/springframework/web/default.jsp")); + //assertTrue(pathMatcher.match("org/springframework/**/*.jsp", "org/springframework/default.jsp")); + //assertTrue(pathMatcher.match("org/**/servlet/bla.jsp", "org/springframework/servlet/bla.jsp")); + //assertTrue(pathMatcher.match("org/**/servlet/bla.jsp", "org/springframework/testing/servlet/bla.jsp")); + //assertTrue(pathMatcher.match("org/**/servlet/bla.jsp", "org/servlet/bla.jsp")); + //assertTrue(pathMatcher.match("**/hello.jsp", "org/springframework/servlet/web/views/hello.jsp")); + //assertTrue(pathMatcher.match("**/**/hello.jsp", "org/springframework/servlet/web/views/hello.jsp")); + + assertFalse(pathMatcher.match("/foo/bar/**", "/foo /bar")); + assertFalse(pathMatcher.match("/foo/bar/**", "/foo /bar")); + assertFalse(pathMatcher.match("/foo/bar/**", "/foo / bar")); + assertFalse(pathMatcher.match("/foo/bar/**", " / foo / bar")); + //assertFalse(pathMatcher.match("org/**/servlet/bla.jsp", " org / servlet / bla . jsp")); + } + + @Disabled("no custom path separator") + @Test + public void isMatchWithCustomSeparator() throws Exception { + final PathPatternParser pathMatcher = new PathPatternParser(); + //pathMatcher.withPathSeparator('.'); + + assertTrue(pathMatcher.match(".foo.bar.**", ".foo.bar")); + assertTrue(pathMatcher.match(".resource.1", ".resource.1")); + assertTrue(pathMatcher.match(".resource.*", ".resource.1")); + assertTrue(pathMatcher.match(".resource.*.", ".resource.1.")); + assertTrue(pathMatcher.match("org.springframework.**.*.jsp", "org.springframework.web.views.hello.jsp")); + assertTrue(pathMatcher.match("org.springframework.**.*.jsp", "org.springframework.web.default.jsp")); + assertTrue(pathMatcher.match("org.springframework.**.*.jsp", "org.springframework.default.jsp")); + assertTrue(pathMatcher.match("org.**.servlet.bla.jsp", "org.springframework.servlet.bla.jsp")); + assertTrue(pathMatcher.match("org.**.servlet.bla.jsp", "org.springframework.testing.servlet.bla.jsp")); + assertTrue(pathMatcher.match("org.**.servlet.bla.jsp", "org.servlet.bla.jsp")); + assertTrue(pathMatcher.match("http://example.org", "http://example.org")); + assertTrue(pathMatcher.match("**.hello.jsp", "org.springframework.servlet.web.views.hello.jsp")); + assertTrue(pathMatcher.match("**.**.hello.jsp", "org.springframework.servlet.web.views.hello.jsp")); + + // test matching with ?'s and .'s + assertTrue(pathMatcher.match(".?", ".a")); + assertTrue(pathMatcher.match(".?.a", ".a.a")); + assertTrue(pathMatcher.match(".a.?", ".a.b")); + assertTrue(pathMatcher.match(".??.a", ".aa.a")); + assertTrue(pathMatcher.match(".a.??", ".a.bb")); + assertTrue(pathMatcher.match(".?", ".a")); + + // test matching with **'s + assertTrue(pathMatcher.match(".**", ".testing.testing")); + assertTrue(pathMatcher.match(".*.**", ".testing.testing")); + assertTrue(pathMatcher.match(".**.*", ".testing.testing")); + assertTrue(pathMatcher.match(".bla.**.bla", ".bla.testing.testing.bla")); + assertTrue(pathMatcher.match(".bla.**.bla", ".bla.testing.testing.bla.bla")); + assertTrue(pathMatcher.match(".**.test", ".bla.bla.test")); + assertTrue(pathMatcher.match(".bla.**.**.bla", ".bla.bla.bla.bla.bla.bla")); + assertFalse(pathMatcher.match(".bla*bla.test", ".blaXXXbl.test")); + assertFalse(pathMatcher.match(".*bla.test", "XXXblab.test")); + assertFalse(pathMatcher.match(".*bla.test", "XXXbl.test")); + } + + @Disabled("no ignore case") + @Test + public void isMatchWithIgnoreCase() throws Exception { + final PathPatternParser pathMatcher = new PathPatternParser(); + // withIgnoreCase(); + + assertTrue(pathMatcher.match("/foo/bar/**", "/FoO/baR")); + assertTrue(pathMatcher.match("org/springframework/**/*.jsp", "ORG/SpringFramework/web/views/hello.JSP")); + assertTrue(pathMatcher.match("org/**/servlet/bla.jsp", "Org/SERVLET/bla.jsp")); + assertTrue(pathMatcher.match("/?", "/A")); + assertTrue(pathMatcher.match("/?/a", "/a/A")); + assertTrue(pathMatcher.match("/a/??", "/a/Bb")); + assertTrue(pathMatcher.match("/?", "/a")); + assertTrue(pathMatcher.match("/**", "/testing/teSting")); + assertTrue(pathMatcher.match("/*/**", "/testing/testing")); + assertTrue(pathMatcher.match("/**/*", "/tEsting/testinG")); + assertTrue(pathMatcher.match("http://example.org", "HtTp://exAmple.org")); + assertTrue(pathMatcher.match("HTTP://EXAMPLE.ORG", "HtTp://exAmple.org")); + } + + @Disabled("no ignore case, no custom separator") + @Test + public void isMatchWithIgnoreCaseWithCustomPathSeparator() throws Exception { + final PathPatternParser pathMatcher = new PathPatternParser(); + //final AntPathMatcher pathMatcher = AntPathMatcher.builder() + // .withIgnoreCase() + // .withPathSeparator('.').build(); + + assertTrue(pathMatcher.match(".foo.bar.**", ".FoO.baR")); + assertTrue(pathMatcher.match("org.springframework.**.*.jsp", "ORG.SpringFramework.web.views.hello.JSP")); + assertTrue(pathMatcher.match("org.**.servlet.bla.jsp", "Org.SERVLET.bla.jsp")); + assertTrue(pathMatcher.match(".?", ".A")); + assertTrue(pathMatcher.match(".?.a", ".a.A")); + assertTrue(pathMatcher.match(".a.??", ".a.Bb")); + assertTrue(pathMatcher.match(".?", ".a")); + assertTrue(pathMatcher.match(".**", ".testing.teSting")); + assertTrue(pathMatcher.match(".*.**", ".testing.testing")); + assertTrue(pathMatcher.match(".**.*", ".tEsting.testinG")); + assertTrue(pathMatcher.match("http:..example.org", "HtTp:..exAmple.org")); + assertTrue(pathMatcher.match("HTTP:..EXAMPLE.ORG", "HtTp:..exAmple.org")); + } + + @Disabled + @Test + public void isMatchWithMatchStart() { + final PathPatternParser pathMatcher = new PathPatternParser(); + //final AntPathMatcher pathMatcher = AntPathMatcher.builder().withMatchStart().build(); + + // test exact matching + assertTrue(pathMatcher.match("test", "test")); + assertTrue(pathMatcher.match("/test", "/test")); + assertFalse(pathMatcher.match("/test.jpg", "test.jpg")); + assertFalse(pathMatcher.match("test", "/test")); + assertFalse(pathMatcher.match("/test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.match("t?st", "test")); + assertTrue(pathMatcher.match("??st", "test")); + assertTrue(pathMatcher.match("tes?", "test")); + assertTrue(pathMatcher.match("te??", "test")); + assertTrue(pathMatcher.match("?es?", "test")); + assertFalse(pathMatcher.match("tes?", "tes")); + assertFalse(pathMatcher.match("tes?", "testt")); + assertFalse(pathMatcher.match("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.match("*", "test")); + assertTrue(pathMatcher.match("test*", "test")); + assertTrue(pathMatcher.match("test*", "testTest")); + assertTrue(pathMatcher.match("test/*", "test/Test")); + assertTrue(pathMatcher.match("test/*", "test/t")); + assertTrue(pathMatcher.match("test/*", "test/")); + assertTrue(pathMatcher.match("*test*", "AnothertestTest")); + assertTrue(pathMatcher.match("*test", "Anothertest")); + assertTrue(pathMatcher.match("*.*", "test.")); + assertTrue(pathMatcher.match("*.*", "test.test")); + assertTrue(pathMatcher.match("*.*", "test.test.test")); + assertTrue(pathMatcher.match("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.match("test*", "tst")); + assertFalse(pathMatcher.match("test*", "test/")); + assertFalse(pathMatcher.match("test*", "tsttest")); + assertFalse(pathMatcher.match("test*", "test/")); + assertFalse(pathMatcher.match("test*", "test/t")); + assertTrue(pathMatcher.match("test/*", "test")); + assertTrue(pathMatcher.match("test/t*.txt", "test")); + assertFalse(pathMatcher.match("*test*", "tsttst")); + assertFalse(pathMatcher.match("*test", "tsttst")); + assertFalse(pathMatcher.match("*.*", "tsttst")); + assertFalse(pathMatcher.match("test*aaa", "test")); + assertFalse(pathMatcher.match("test*aaa", "testblaaab")); + + // test matching with ?'s and /'s + assertTrue(pathMatcher.match("/?", "/a")); + assertTrue(pathMatcher.match("/?/a", "/a/a")); + assertTrue(pathMatcher.match("/a/?", "/a/b")); + assertTrue(pathMatcher.match("/??/a", "/aa/a")); + assertTrue(pathMatcher.match("/a/??", "/a/bb")); + assertTrue(pathMatcher.match("/?", "/a")); + + // test matching with **'s + assertTrue(pathMatcher.match("/**", "/testing/testing")); + assertTrue(pathMatcher.match("/*/**", "/testing/testing")); + assertTrue(pathMatcher.match("/**/*", "/testing/testing")); + assertTrue(pathMatcher.match("test*/**", "test/")); + assertTrue(pathMatcher.match("test*/**", "test/t")); + assertTrue(pathMatcher.match("/bla/**/bla", "/bla/testing/testing/bla")); + assertTrue(pathMatcher.match("/bla/**/bla", "/bla/testing/testing/bla/bla")); + assertTrue(pathMatcher.match("/**/test", "/bla/bla/test")); + assertTrue(pathMatcher.match("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla")); + assertTrue(pathMatcher.match("/bla*bla/test", "/blaXXXbla/test")); + assertTrue(pathMatcher.match("/*bla/test", "/XXXbla/test")); + assertFalse(pathMatcher.match("/bla*bla/test", "/blaXXXbl/test")); + assertFalse(pathMatcher.match("/*bla/test", "XXXblab/test")); + assertFalse(pathMatcher.match("/*bla/test", "XXXbl/test")); + + assertFalse(pathMatcher.match("/????", "/bala/bla")); + assertTrue(pathMatcher.match("/**/*bla", "/bla/bla/bla/bbb")); + + assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.match("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg")); + + assertTrue(pathMatcher.match("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.match("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.match("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.match("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing")); + + assertTrue(pathMatcher.match("/x/x/**/bla", "/x/x/x/")); + + assertTrue(pathMatcher.match("", "")); + } + + @Disabled + @Test + public void isMatchWithTrimTokens() { + final PathPatternParser pathMatcher = new PathPatternParser(); + //final AntPathMatcher pathMatcher = AntPathMatcher.builder().withTrimTokens().build(); + + assertTrue(pathMatcher.match("/foo/bar/**", "/foo /bar")); + assertTrue(pathMatcher.match("/foo/bar/**", "/foo /bar")); + assertTrue(pathMatcher.match("/foo/bar/**", "/foo / bar")); + assertTrue(pathMatcher.match("/foo/bar/**", " / foo / bar")); + assertTrue(pathMatcher.match("/**/*", "/ testing / testing ")); + assertTrue(pathMatcher.match("org/**/servlet/bla.jsp", " org / servlet / bla . jsp")); + } + + @Disabled + @Test + public void isMatchWithIgnoreCaseWithCustomPathSeparatorWithTrimTokens() throws Exception { + final PathPatternParser pathMatcher = new PathPatternParser(); + //final AntPathMatcher pathMatcher = AntPathMatcher.builder() + // .withIgnoreCase() + // .withTrimTokens() + // .withPathSeparator('.').build(); + + assertTrue(pathMatcher.match(".foo.bar.**", ".FoO.baR")); + assertTrue(pathMatcher.match("org.springframework.**.*.jsp", "ORG. SpringFramework.web.views.hello . JSP")); + assertTrue(pathMatcher.match("org.**.servlet.bla.jsp", "Org .SERVLET . bla.jsp")); + assertTrue(pathMatcher.match(".?", ". A")); + assertTrue(pathMatcher.match(".?.a", ".a.A")); + assertTrue(pathMatcher.match(".a.??", ". a . B b")); + assertTrue(pathMatcher.match(".?", ".a")); + assertTrue(pathMatcher.match(".**", ".testing . teSting")); + assertTrue(pathMatcher.match(".*.**", ".testing.testing")); + assertTrue(pathMatcher.match(".**.*", " . tEsting . testinG")); + assertTrue(pathMatcher.match("http:..example.org", " H t T p : . . exAmple . org")); + assertTrue(pathMatcher.match("HTTP:..EXAMPLE.ORG", "HtTp:..exAmple.org")); + } +} diff --git a/net-path/src/test/java/org/xbib/net/path/structure/PathMatcherTest.java b/net-path/src/test/java/org/xbib/net/path/structure/PathMatcherTest.java new file mode 100644 index 0000000..a0b11e7 --- /dev/null +++ b/net-path/src/test/java/org/xbib/net/path/structure/PathMatcherTest.java @@ -0,0 +1,163 @@ +package org.xbib.net.path.structure; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PathMatcherTest { + + @Test + void match() { + PathMatcher pathMatcher = new PathMatcher(); + // test exact matching + assertTrue(pathMatcher.match("test", "test")); + assertTrue(pathMatcher.match("/test", "/test")); + assertFalse(pathMatcher.match("/test.jpg", "test.jpg")); + assertFalse(pathMatcher.match("test", "/test")); + assertFalse(pathMatcher.match("/test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.match("t?st", "test")); + assertTrue(pathMatcher.match("??st", "test")); + assertTrue(pathMatcher.match("tes?", "test")); + assertTrue(pathMatcher.match("te??", "test")); + assertTrue(pathMatcher.match("?es?", "test")); + assertFalse(pathMatcher.match("tes?", "tes")); + assertFalse(pathMatcher.match("tes?", "testt")); + assertFalse(pathMatcher.match("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.match("*", "test")); + assertTrue(pathMatcher.match("test*", "test")); + assertTrue(pathMatcher.match("test*", "testTest")); + assertTrue(pathMatcher.match("test/*", "test/Test")); + assertTrue(pathMatcher.match("test/*", "test/t")); + assertTrue(pathMatcher.match("test/*", "test/")); + assertTrue(pathMatcher.match("*test*", "AnothertestTest")); + assertTrue(pathMatcher.match("*test", "Anothertest")); + assertTrue(pathMatcher.match("*.*", "test.")); + assertTrue(pathMatcher.match("*.*", "test.test")); + assertTrue(pathMatcher.match("*.*", "test.test.test")); + assertTrue(pathMatcher.match("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.match("test*", "tst")); + assertFalse(pathMatcher.match("test*", "tsttest")); + assertFalse(pathMatcher.match("test*", "test/")); + assertFalse(pathMatcher.match("test*", "test/t")); + assertFalse(pathMatcher.match("test/*", "test")); + assertFalse(pathMatcher.match("*test*", "tsttst")); + assertFalse(pathMatcher.match("*test", "tsttst")); + assertFalse(pathMatcher.match("*.*", "tsttst")); + assertFalse(pathMatcher.match("test*aaa", "test")); + assertFalse(pathMatcher.match("test*aaa", "testblaaab")); + + // test matching with ?'s and /'s + assertTrue(pathMatcher.match("/?", "/a")); + assertTrue(pathMatcher.match("/?/a", "/a/a")); + assertTrue(pathMatcher.match("/a/?", "/a/b")); + assertTrue(pathMatcher.match("/??/a", "/aa/a")); + assertTrue(pathMatcher.match("/a/??", "/a/bb")); + assertTrue(pathMatcher.match("/?", "/a")); + + // test matching with **'s + assertTrue(pathMatcher.match("/**", "/testing/testing")); + assertTrue(pathMatcher.match("/*/**", "/testing/testing")); + assertTrue(pathMatcher.match("/**", "/testing/testing")); + assertTrue(pathMatcher.match("/bla/**", "/bla/testing/testing/bla")); + assertTrue(pathMatcher.match("/bla/**", "/bla/testing/testing/bla/bla")); + assertTrue(pathMatcher.match("/**/test", "/bla/bla/test")); + assertTrue(pathMatcher.match("/bla/**", "/bla/bla/bla/bla/bla/bla")); + assertTrue(pathMatcher.match("/bla*bla/test", "/blaXXXbla/test")); + assertTrue(pathMatcher.match("/*bla/test", "/XXXbla/test")); + assertFalse(pathMatcher.match("/bla*bla/test", "/blaXXXbl/test")); + assertFalse(pathMatcher.match("/*bla/test", "XXXblab/test")); + assertFalse(pathMatcher.match("/*bla/test", "XXXbl/test")); + + assertFalse(pathMatcher.match("/????", "/bala/bla")); + assertTrue(pathMatcher.match("/**", "/bla/bla/bla/bbb")); + assertTrue(pathMatcher.match("/*bla*/**", "/XXXblaXXXX/testing/testing/bla/testing")); + + assertTrue(pathMatcher.match("*bla*/**", "XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.match("*bla*/**", "XXXblaXXXX/testing/testing/bla/testing/testing")); + + assertTrue(pathMatcher.match("", "")); + assertTrue(pathMatcher.match("/{bla}.*", "/testing.html")); + assertEquals("[bla=testing.html]", pathMatcher.getParameter().toString()); + + assertTrue(pathMatcher.match("/**/*.jpg", "/.jpg")); + assertTrue(pathMatcher.match("/**/*.jpg", "/test.jpg")); + assertTrue(pathMatcher.match("/**/*.jpg", "/test/test.jpg")); + assertFalse(pathMatcher.match("/**/*.jpg", "/")); + assertFalse(pathMatcher.match("/**/*.jpg", "/.png")); + assertFalse(pathMatcher.match("/**/*.jpg", "/test.png")); + assertFalse(pathMatcher.match("/**/*.jpg", "/test/test.png")); + } + + @Test + void uniqueDeliminator() { + PathMatcher pathMatcher = new PathMatcher(); + + pathMatcher.setPathSeparator("."); + + // test exact matching + assertTrue(pathMatcher.match("test", "test")); + assertTrue(pathMatcher.match(".test", ".test")); + assertFalse(pathMatcher.match(".test/jpg", "test/jpg")); + assertFalse(pathMatcher.match("test", ".test")); + assertFalse(pathMatcher.match(".test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.match("t?st", "test")); + assertTrue(pathMatcher.match("??st", "test")); + assertTrue(pathMatcher.match("tes?", "test")); + assertTrue(pathMatcher.match("te??", "test")); + assertTrue(pathMatcher.match("?es?", "test")); + assertFalse(pathMatcher.match("tes?", "tes")); + assertFalse(pathMatcher.match("tes?", "testt")); + assertFalse(pathMatcher.match("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.match("*.gtpl", "test.gtpl")); + assertTrue(pathMatcher.match("*", "test")); + assertTrue(pathMatcher.match("test*", "test")); + assertTrue(pathMatcher.match("test*", "testTest")); + assertTrue(pathMatcher.match("*test*", "AnothertestTest")); + assertTrue(pathMatcher.match("*test", "Anothertest")); + assertTrue(pathMatcher.match("*/*", "test/")); + assertTrue(pathMatcher.match("*/*", "test/test")); + assertTrue(pathMatcher.match("*/*", "test/test/test")); + assertTrue(pathMatcher.match("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.match("test*", "tst")); + assertFalse(pathMatcher.match("test*", "tsttest")); + assertFalse(pathMatcher.match("*test*", "tsttst")); + assertFalse(pathMatcher.match("*test", "tsttst")); + assertFalse(pathMatcher.match("*/*", "tsttst")); + assertFalse(pathMatcher.match("test*aaa", "test")); + assertFalse(pathMatcher.match("test*aaa", "testblaaab")); + + // test matching with ?'s and .'s + assertTrue(pathMatcher.match(".?", ".a")); + assertTrue(pathMatcher.match(".?.a", ".a.a")); + assertTrue(pathMatcher.match(".a.?", ".a.b")); + assertTrue(pathMatcher.match(".??.a", ".aa.a")); + assertTrue(pathMatcher.match(".a.??", ".a.bb")); + assertTrue(pathMatcher.match(".?", ".a")); + + // test matching with ** + assertTrue(pathMatcher.match(".**", ".testing.testing")); + assertTrue(pathMatcher.match(".*.**", ".testing.testing")); + assertTrue(pathMatcher.match(".**.*", ".testing.testing")); + assertTrue(pathMatcher.match(".bla.**.bla", ".bla.testing.testing.bla")); + assertTrue(pathMatcher.match(".bla.**.bla", ".bla.testing.testing.bla.bla")); + assertTrue(pathMatcher.match(".**.test", ".bla.bla.test")); + assertTrue(pathMatcher.match(".bla.**.**.bla", ".bla.bla.bla.bla.bla.bla")); + assertTrue(pathMatcher.match(".bla*bla.test", ".blaXXXbla.test")); + assertTrue(pathMatcher.match(".*bla.test", ".XXXbla.test")); + assertFalse(pathMatcher.match(".bla*bla.test", ".blaXXXbl.test")); + assertFalse(pathMatcher.match(".*bla.test", "XXXblab.test")); + assertFalse(pathMatcher.match(".*bla.test", "XXXbl.test")); + } + +} + diff --git a/net-path/src/test/java/org/xbib/net/path/structure/PathResolverTest.java b/net-path/src/test/java/org/xbib/net/path/structure/PathResolverTest.java new file mode 100644 index 0000000..929b7d5 --- /dev/null +++ b/net-path/src/test/java/org/xbib/net/path/structure/PathResolverTest.java @@ -0,0 +1,253 @@ +package org.xbib.net.path.structure; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.net.Parameter; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +class PathResolverTest { + + @Test + void example() { + PathResolver pathResolver = PathResolver.builder() + .add( "GET", "/static/{file}", 1234) + .build(); + assertSuccessfulResolution(pathResolver, "GET", "/static/test.txt", 1234, + Parameter.of("PATH", Map.of("file", "test.txt"))); + } + + @Test + void simple() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "explorer", 1234) + .build(); + assertSuccessfulResolution(pathResolver, "explorer", 1234); + assertFailedGetResolution(pathResolver, ""); + assertFailedGetResolution(pathResolver, "test"); + } + + @Test + void sharedPrefix() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "discovery/v1/rest", 1234) + .add("GET", "discovery/v2/rest", 4321) + .build(); + assertSuccessfulResolution(pathResolver, "discovery/v1/rest", 1234); + assertSuccessfulResolution(pathResolver, "discovery/v2/rest", 4321); + assertFailedGetResolution(pathResolver, ""); + assertFailedGetResolution(pathResolver, "discovery"); + assertFailedGetResolution(pathResolver, "discovery/v1"); + } + + @Test + void prefix() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "discovery", 1234) + .add("GET", "discovery/v1", 4321) + .build(); + assertSuccessfulResolution(pathResolver, "discovery", 1234); + assertSuccessfulResolution(pathResolver, "discovery/v1", 4321); + assertFailedGetResolution(pathResolver, ""); + } + + @Test + void parameter() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "discovery/{version}/rest", 1234) + .build(); + assertSuccessfulResolution(pathResolver, "GET", "discovery/v1/rest", 1234, + Parameter.of("PATH", Map.of("version", "v1"))); + } + + @Test + void multipleParameters() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "discovery/{discovery_version}/apis/{api}/{format}", 1234) + .build(); + assertSuccessfulResolution(pathResolver, "GET", "discovery/v1/apis/test/rest", 1234, + Parameter.of("PATH", Map.of("discovery_version", "v1", "api", "test", "format", "rest"))); + } + + @Test + void sharedParameterPrefix() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "discovery/{version}/rest", 1234) + .add("GET", "discovery/{version}/rpc", 4321) + .build(); + assertSuccessfulResolution(pathResolver, "GET", "discovery/v1/rest", 1234, + Parameter.of("PATH", Map.of("version", "v1"))); + assertSuccessfulResolution(pathResolver, "GET", "discovery/v2/rest", 1234, + Parameter.of("PATH", Map.of("version", "v2"))); + assertSuccessfulResolution(pathResolver, "GET", "discovery/v1/rpc", 4321, + Parameter.of("PATH", Map.of("version", "v1"))); + assertSuccessfulResolution(pathResolver, "GET", "discovery/v2/rpc", 4321, + Parameter.of("PATH", Map.of("version", "v2"))); + } + + @Test + void testResolveParameterAfterLiteral() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "{one}/three", 1234) + .add("GET", "one/two", 4321) + .build(); + assertSuccessfulResolution(pathResolver, "GET", "one/three", 1234, + Parameter.of("PATH", Map.of("one", "one"))); + assertSuccessfulResolution(pathResolver, "one/two", 4321); + } + + @Test + void testResolveBacktrack() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "{one}/{two}/three/{four}", 1234) + .add("GET", "one/two/{three}/four", 4321) + .build(); + pathResolver.resolve("GET", "one/two/three/four", result -> { + assertThat(result.getValue(), anyOf(equalTo(1234), equalTo(4321))); + if (result.getParameter().containsKey("PATH", "three")) { + assertThat(result.getParameter(), is(Parameter.of("PATH", Map.of("three", "three")))); + } else { + assertThat(result.getParameter(), is(Parameter.of("PATH", Map.of("one", "one", "two", "two", "four", "four")))); + } + }); + pathResolver.resolve("GET", "one/two/three/five", result -> { + assertThat(result.getValue(), equalTo(1234)); + assertThat(result.getParameter(), is(Parameter.of("PATH", Map.of("one", "one", "two", "two", "four", "five")))); + }); + } + + @Test + void pathMethodsWithDifferentParameterNames() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "test/{one}", 1234) + .add("GET", "test/{two}", 4321) + .build(); + AtomicInteger count = new AtomicInteger(); + pathResolver.resolve("GET", "test/foo", result -> { + assertThat(result.getValue(), anyOf(equalTo(1234), equalTo(4321))); + if (result.getValue() == 1234) { + assertThat(result.getParameter(), is(Parameter.of("PATH", Map.of("one", "foo")))); + } + if (result.getValue() == 4321) { + assertThat(result.getParameter(), is(Parameter.of("PATH", Map.of("two", "foo")))); + } + count.incrementAndGet(); + }); + assertThat(count.get(), is(2)); + } + + @Test + void duplicatePathParams() { + PathResolver pathResolver = PathResolver.builder() + .add("GET", "test/{one}", 1234) + .add("GET", "test/{two}", 4321) + .build(); + AtomicInteger count = new AtomicInteger(); + pathResolver.resolve("GET", "test/foo", result -> { + assertThat(result.getValue(), anyOf(equalTo(1234), equalTo(4321))); + if (result.getParameter().containsKey("PATH", "one")) { + assertThat(result.getParameter().get("PATH", "one"), is("foo")); + } + if (result.getParameter().containsKey("PATH", "two")) { + assertThat(result.getParameter().get("PATH", "two"), is("foo")); + } + count.incrementAndGet(); + }); + assertThat(count.get(), is(2)); + } + + @Test + void builderNullPath() { + try { + PathResolver.builder() + .add("GET", null, 1234); + fail("expected NullPointerException"); + } catch (NullPointerException e) { + // expected + } + } + + @Test + void builderNullValue() { + try { + PathResolver.builder() + .add("GET", "throws/an/exception", null); + fail("expected NullPointerException"); + } catch (NullPointerException e) { + // expected + } + } + + @Test + void resolveNullPath() { + try { + PathResolver trie = PathResolver.builder().build(); + trie.resolve(null, null, r -> {}); + fail("expected NullPointerException"); + } catch (NullPointerException e) { + // expected + } + } + + @Test + void testFallback() { + AtomicInteger counter = new AtomicInteger(0); + PathResolver trie = PathResolver.builder() + .add( "GET", "/test/{one}", 1) + .add( "GET", "/**", 2) + .build(); + trie.resolve("GET", "/test/foo", result -> { + assertTrue( result.getValue() == 1 || result.getValue() == 2); + counter.incrementAndGet(); + }); + assertThat(counter.get(), equalTo(1)); + } + + @Disabled + @Test + void testSuffixCatchAll() { + AtomicInteger counter = new AtomicInteger(0); + PathResolver trie = PathResolver.builder() + .add( "GET", "/**/*.test", 1) + .add( "GET", "/**", 2) + .build(); + trie.resolve("GET", "/test/test.test", result -> { + assertTrue( result.getValue() == 1 || result.getValue() == 2); + counter.incrementAndGet(); + }); + assertThat(counter.get(), equalTo(2)); + } + + private void assertSuccessfulResolution(PathResolver pathResolver, String path, Integer value) { + assertSuccessfulResolution(pathResolver, "GET", path, value, Parameter.of("PATH")); + } + + private void assertSuccessfulResolution(PathResolver pathResolver, String method, String path, Integer value, + Parameter parameter) { + AtomicBoolean found = new AtomicBoolean(false); + pathResolver.resolve(method, path, result -> { + assertThat(result, notNullValue()); + assertThat(result.getMethod(), is(method)); + assertThat(result.getValue(), is(value)); + assertThat(result.getParameter(), is(parameter)); + found.set(true); + }); + assertTrue(found.get()); + } + + private void assertFailedGetResolution(PathResolver pathResolver, String path) { + pathResolver.resolve("GET", path, r -> assertThat(r, nullValue())); + } +} diff --git a/net-path/src/test/java/org/xbib/net/path/structure/PathTest.java b/net-path/src/test/java/org/xbib/net/path/structure/PathTest.java new file mode 100644 index 0000000..85b9c09 --- /dev/null +++ b/net-path/src/test/java/org/xbib/net/path/structure/PathTest.java @@ -0,0 +1,145 @@ +package org.xbib.net.path.structure; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class PathTest { + + @Test + void testComparator() { + Comparator comparator = new PathComparator("/hotels/new"); + assertEquals(0, comparator.compare(null, null)); + assertEquals(1, comparator.compare(null, Path.of("/hotels/new"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/new"), null)); + assertEquals(0, comparator.compare(Path.of("/hotels/new"), Path.of("/hotels/new"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/new"), Path.of("/hotels/*"))); + assertEquals(1, comparator.compare(Path.of("/hotels/*"), Path.of("/hotels/new"))); + assertEquals(0, comparator.compare(Path.of("/hotels/*"), Path.of("/hotels/*"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/new"), Path.of("/hotels/{hotel}"))); + assertEquals(1, comparator.compare(Path.of("/hotels/{hotel}"), Path.of("/hotels/new"))); + assertEquals(0, comparator.compare(Path.of("/hotels/{hotel}"), Path.of("/hotels/{hotel}"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/{hotel}/booking"), Path.of("/hotels/{hotel}/bookings/{booking}"))); + assertEquals(1, comparator.compare(Path.of("/hotels/{hotel}/bookings/{booking}"), Path.of("/hotels/{hotel}/booking"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"), Path.of("/**"))); + assertEquals(1, comparator.compare(Path.of("/**"), Path.of("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"))); + assertEquals(0, comparator.compare(Path.of("/**"), Path.of("/**"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/{hotel}"), Path.of("/hotels/*"))); + assertEquals(1, comparator.compare(Path.of("/hotels/*"), Path.of("/hotels/{hotel}"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/*"), Path.of("/hotels/*/**"))); + assertEquals(1, comparator.compare(Path.of("/hotels/*/**"), Path.of("/hotels/*"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/new"), Path.of("/hotels/new.*"))); + assertEquals(2, comparator.compare(Path.of("/hotels/{hotel}"), Path.of("/hotels/{hotel}.*"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"), Path.of("/hotels/**"))); + assertEquals(1, comparator.compare(Path.of("/hotels/**"), Path.of("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"))); + assertEquals(1, comparator.compare(Path.of("/hotels/foo/bar/**"), Path.of("/hotels/{hotel}"))); + assertEquals(-1, comparator.compare(Path.of("/hotels/{hotel}"), Path.of("/hotels/foo/bar/**"))); + assertEquals(2, comparator.compare(Path.of("/hotels/**/bookings/**"), Path.of("/hotels/**"))); + assertEquals(-2, comparator.compare(Path.of("/hotels/**"), Path.of("/hotels/**/bookings/**"))); + assertEquals(1, comparator.compare(Path.of("/**"), Path.of("/hotels/{hotel}"))); + assertEquals(1, comparator.compare(Path.of("/hotels"), Path.of("/hotels2"))); + assertEquals(-1, comparator.compare(Path.of("*"), Path.of("*/**"))); + assertEquals(1, comparator.compare(Path.of("*/**"), Path.of("*"))); + } + + @Test + void testPatternComparatorSort() { + Comparator comparator = new PathComparator("/hotels/new"); + + List paths = new ArrayList<>(); + paths.add(null); + paths.add(Path.of("/hotels/new")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPathSpec()); + assertNull(paths.get(1)); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/new")); + paths.add(null); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPathSpec()); + assertNull(paths.get(1)); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/*")); + paths.add(Path.of("/hotels/new")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPathSpec()); + assertEquals("/hotels/*", paths.get(1).getPathSpec()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/new")); + paths.add(Path.of("/hotels/*")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPathSpec()); + assertEquals("/hotels/*", paths.get(1).getPathSpec()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/**")); + paths.add(Path.of("/hotels/*")); + paths.sort(comparator); + assertEquals("/hotels/*", paths.get(0).getPathSpec()); + assertEquals("/hotels/**", paths.get(1).getPathSpec()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/*")); + paths.add(Path.of("/hotels/**")); + paths.sort(comparator); + assertEquals("/hotels/*", paths.get(0).getPathSpec()); + assertEquals("/hotels/**", paths.get(1).getPathSpec()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/{hotel}")); + paths.add(Path.of("/hotels/new")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPathSpec()); + assertEquals("/hotels/{hotel}", paths.get(1).getPathSpec()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/new")); + paths.add(Path.of("/hotels/{hotel}")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPathSpec()); + assertEquals("/hotels/{hotel}", paths.get(1).getPathSpec()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/*")); + paths.add(Path.of("/hotels/{hotel}")); + paths.add(Path.of("/hotels/new")); + paths.sort(comparator); + assertEquals("/hotels/new", paths.get(0).getPathSpec()); + assertEquals("/hotels/{hotel}", paths.get(1).getPathSpec()); + assertEquals("/hotels/*", paths.get(2).getPathSpec()); + + paths = new ArrayList<>(); + paths.add(Path.of("/hotels/ne*")); + paths.add(Path.of("/hotels/n*")); + Collections.shuffle(paths); + paths.sort(comparator); + assertEquals("/hotels/ne*", paths.get(0).getPathSpec()); + assertEquals("/hotels/n*", paths.get(1).getPathSpec()); + + paths = new ArrayList<>(); + comparator = new PathComparator("/hotels/new.html"); + paths.add(Path.of("/hotels/new.*")); + paths.add(Path.of("/hotels/{hotel}")); + Collections.shuffle(paths); + paths.sort(comparator); + assertEquals("/hotels/new.*", paths.get(0).getPathSpec()); + assertEquals("/hotels/{hotel}", paths.get(1).getPathSpec()); + + paths = new ArrayList<>(); + comparator = new PathComparator("/web/endUser/action/login.html"); + paths.add(Path.of("/**/login.*")); + paths.add(Path.of("/**/endUser/action/login.*")); + paths.sort(comparator); + assertEquals("/**/endUser/action/login.*", paths.get(0).getPathSpec()); + assertEquals("/**/login.*", paths.get(1).getPathSpec()); + } +} diff --git a/net-security/NOTICE.txt b/net-security/NOTICE.txt new file mode 100644 index 0000000..8e53476 --- /dev/null +++ b/net-security/NOTICE.txt @@ -0,0 +1,10 @@ + +All credits for the private key reader go to the original author Zhihong Zhang + +org.xbib.net.security.ssl is based on + +https://github.com/Hakky54/sslcontext-kickstart + +by Hakan Altındağ + +Apache License 2.0 diff --git a/net-security/build.gradle b/net-security/build.gradle new file mode 100644 index 0000000..bd5180c --- /dev/null +++ b/net-security/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':net') + testImplementation project(':net-bouncycastle') +} diff --git a/net-security/src/main/java/module-info.java b/net-security/src/main/java/module-info.java new file mode 100644 index 0000000..cee7b1e --- /dev/null +++ b/net-security/src/main/java/module-info.java @@ -0,0 +1,15 @@ +module org.xbib.net.security { + requires org.xbib.net; + provides java.security.Provider with org.xbib.net.security.eddsa.EdDSASecurityProvider; + provides org.xbib.net.security.CertificateProvider with org.xbib.net.security.DefaultCertificateProvider; + exports org.xbib.net.security; + exports org.xbib.net.security.cookie; + exports org.xbib.net.security.eddsa; + exports org.xbib.net.security.eddsa.math; + exports org.xbib.net.security.eddsa.math.bigint; + exports org.xbib.net.security.eddsa.math.ed25519; + exports org.xbib.net.security.eddsa.spec; + exports org.xbib.net.security.signatures; + exports org.xbib.net.security.ssl; + exports org.xbib.net.security.util; +} diff --git a/net-security/src/main/java/org/xbib/net/security/CertificateProvider.java b/net-security/src/main/java/org/xbib/net/security/CertificateProvider.java new file mode 100644 index 0000000..c08594e --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/CertificateProvider.java @@ -0,0 +1,18 @@ +package org.xbib.net.security; + +import java.io.IOException; +import java.io.InputStream; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Map; + +public interface CertificateProvider { + + Map.Entry> provide(InputStream key, String password, InputStream chain) + throws CertificateException, IOException; + + Map.Entry> provideSelfSigned(String fqdn) + throws CertificateException, IOException; +} diff --git a/net-security/src/main/java/org/xbib/net/security/CertificateReader.java b/net-security/src/main/java/org/xbib/net/security/CertificateReader.java new file mode 100644 index 0000000..67f1bcd --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/CertificateReader.java @@ -0,0 +1,209 @@ +package org.xbib.net.security; + +import org.xbib.net.security.util.DistinguishedNameParser; + +import javax.crypto.NoSuchPaddingException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.List; +import java.util.Scanner; + +public class CertificateReader { + + private static final String BEGIN_MARKER = "-----BEGIN CERTIFICATE-----"; + + private static final String END_MARKER = "-----END CERTIFICATE-----"; + + private static final PrivateKeyReader privateKeyReader = new PrivateKeyReader(); + + public CertificateReader() { + } + + public PrivateKey providePrivateKey(InputStream pem, String password) + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, + InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException { + return privateKeyReader.readPrivateKey(pem, password); + } + + public X509Certificate readCertificate(String pem) throws CertificateException, IOException { + return readCertificate(new ByteArrayInputStream(readMaterial(pem, BEGIN_MARKER, END_MARKER))); + } + + public X509Certificate readCertificate(InputStream pem) throws CertificateException, IOException { + return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(pem); + } + + public X509Certificate readCertificate(byte[] der) throws CertificateException { + return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(der)); + } + + public String getCertificateInPemFormat(X509Certificate certificate) throws CertificateEncodingException { + return derToPem(certificate.getEncoded(), BEGIN_MARKER, END_MARKER); + } + + public byte[] getCertificateInDerFormat(X509Certificate certificate) throws CertificateEncodingException { + return certificate.getEncoded(); + } + + public BigInteger getModulus(X509Certificate certificate) { + RSAPublicKey rsaPublicKey = (RSAPublicKey) certificate.getPublicKey(); + return rsaPublicKey.getModulus(); + } + + public String getSha1Fingerprint(X509Certificate certificate) throws CertificateEncodingException, NoSuchAlgorithmException { + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + sha1.update(getCertificateInDerFormat(certificate)); + return toHex(sha1.digest()); + } + + public String getSha256Fingerprint(X509Certificate certificate) throws NoSuchAlgorithmException, CertificateEncodingException { + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + sha256.update(getCertificateInDerFormat(certificate)); + return toHex(sha256.digest()); + } + + @SuppressWarnings("unchecked") + public Collection readChain(InputStream keyCertChainInputStream) + throws CertificateException { + return (Collection) CertificateFactory.getInstance("X509") + .generateCertificates(keyCertChainInputStream); + } + + public static String getServerName(X509Certificate certificate) { + if (certificate == null) { + return null; + } + return new DistinguishedNameParser(certificate.getSubjectX500Principal()) + .findMostSpecific("CN"); + } + + public static List getAlternativeServerNames(X509Certificate certificate) + throws CertificateParsingException { + List list = new ArrayList<>(); + Collection> altNames = certificate.getSubjectAlternativeNames(); + if (altNames != null) { + for (List altName : altNames) { + Integer type = (Integer) altName.get(0); + if (type == 2) { // Type = DNS + String string = altName.get(1).toString(); + list.add(string); + } + } + } + return list; + } + + public static List parseCertificateChain(String pemChain) + throws CertificateException, IOException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + List list = new ArrayList<>(); + String[] pemCerts = pemChain.split(END_MARKER); + for (String pemCert : pemCerts) { + CertificateReader certificateReader = new CertificateReader(); + X509Certificate certificate = certificateReader.readCertificate(pemCert + END_MARKER); + list.add(certificate); + } + return orderCertificateChain(list); + } + + public static List orderCertificateChain(Collection chain) + throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + List unorderedCertificateList = new ArrayList<>(chain); + List orderedCertificateList = new ArrayList<>(); + X509Certificate topCertificate = findTopCertificate(unorderedCertificateList); + orderedCertificateList.add(topCertificate); + unorderedCertificateList.remove(topCertificate); + int count = unorderedCertificateList.size(); + for (int i = 0; i < count; i++) { + X509Certificate nextCertificate = findNextCertificate(orderedCertificateList.get(0), unorderedCertificateList); + orderedCertificateList.add(0, nextCertificate); + unorderedCertificateList.remove(nextCertificate); + } + return orderedCertificateList; + } + + private static X509Certificate findNextCertificate(X509Certificate certificate, Collection certificateList) + throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + for (X509Certificate currentCert : certificateList) { + try { + currentCert.verify(certificate.getPublicKey()); + return currentCert; + } catch (SignatureException e) { + // skip + } + } + throw new CertificateException("chain doesn't contain a certificate that was signed by " + certificate); + } + + private static X509Certificate findTopCertificate(Collection certificateList) + throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + for (X509Certificate currentCert : certificateList) { + boolean signerFound = false; + for (X509Certificate certificate : certificateList) { + if (currentCert != certificate) { + try { + currentCert.verify(certificate.getPublicKey()); + signerFound = true; + break; + } catch (SignatureException e) { + // + } + } + } + if (!signerFound) { + return currentCert; + } + } + throw new CertificateException("could not find the top certificate of the chain"); + } + + private byte[] readMaterial(String base64WithMarkers, String beginMarker, String endMarker) throws IOException { + String line; + StringBuilder sb = new StringBuilder(); + Scanner scanner = new Scanner(base64WithMarkers); + while (scanner.hasNextLine()) { + line = scanner.nextLine(); + if (line.contains(beginMarker)) { + continue; + } + if (line.contains(endMarker)) { + return Base64.getMimeDecoder().decode(sb.toString()); + } + sb.append(line.trim()); + } + throw new IOException("Invalid PEM file: No end marker"); + } + + private static String derToPem(byte[] der, String beginMarker, String endMarker) { + String base64 = Base64.getEncoder().encodeToString(der); + String[] lines = base64.split("(?<=\\G.{64})"); + StringBuilder result = new StringBuilder(beginMarker + "\n"); + for (String line : lines) { + result.append(line).append("\n"); + } + result.append(endMarker); + return result.toString(); + } + + private static String toHex(byte[] bytes) { + return String.format("%0" + (bytes.length << 1) + "X", new BigInteger(1, bytes)); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/DefaultCertificateProvider.java b/net-security/src/main/java/org/xbib/net/security/DefaultCertificateProvider.java new file mode 100644 index 0000000..9a341d9 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/DefaultCertificateProvider.java @@ -0,0 +1,40 @@ +package org.xbib.net.security; + +import javax.crypto.NoSuchPaddingException; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.Collection; +import java.util.Map; + +public class DefaultCertificateProvider implements CertificateProvider { + + public DefaultCertificateProvider() { + } + + @Override + public Map.Entry> provide(InputStream keyInputStream, String password, InputStream chainInputStream) + throws CertificateException, IOException { + try { + PrivateKeyReader privateKeyReader = new PrivateKeyReader(); + PrivateKey privateKey = privateKeyReader.readPrivateKey(keyInputStream, password); + CertificateReader certificateReader = new CertificateReader(); + Collection chain = certificateReader.readChain(chainInputStream); + return Map.entry(privateKey, chain); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeySpecException | + NoSuchPaddingException | InvalidKeyException e) { + throw new IOException(e); + } + } + + @Override + public Map.Entry> provideSelfSigned(String fqdn) throws CertificateException { + throw new CertificateException(); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/PemReader.java b/net-security/src/main/java/org/xbib/net/security/PemReader.java new file mode 100644 index 0000000..cc78e36 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/PemReader.java @@ -0,0 +1,346 @@ +package org.xbib.net.security; + +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.security.auth.x500.X500Principal; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.DSAKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.util.Base64.getMimeDecoder; +import static java.util.Locale.US; +import static java.util.regex.Pattern.CASE_INSENSITIVE; +import static javax.crypto.Cipher.DECRYPT_MODE; +import static org.xbib.net.security.util.DerUtils.decodeSequence; +import static org.xbib.net.security.util.DerUtils.decodeSequenceOptionalElement; +import static org.xbib.net.security.util.DerUtils.encodeBitString; +import static org.xbib.net.security.util.DerUtils.encodeOctetString; +import static org.xbib.net.security.util.DerUtils.encodeOID; +import static org.xbib.net.security.util.DerUtils.encodeSequence; + +public final class PemReader { + + private static final Pattern PRIVATE_KEY_PATTERN = Pattern.compile( + "-+BEGIN\\s+(?:(.*)\\s+)?PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header + "([a-z0-9+/=\\r\\n]+)" + // Base64 text + "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer + CASE_INSENSITIVE); + + private static final Pattern PUBLIC_KEY_PATTERN = Pattern.compile( + "-+BEGIN\\s+(?:(.*)\\s+)?PUBLIC\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header + "([a-z0-9+/=\\r\\n]+)" + // Base64 text + "-+END\\s+.*PUBLIC\\s+KEY[^-]*-+", // Footer + CASE_INSENSITIVE); + + private static final Pattern CERT_PATTERN = Pattern.compile( + "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+" + // Header + "([a-z0-9+/=\\r\\n]+)" + // Base64 text + "-+END\\s+.*CERTIFICATE[^-]*-+", // Footer + CASE_INSENSITIVE); + + // test data must be exactly 20 bytes for DSA + private static final byte[] TEST_SIGNATURE_DATA = "01234567890123456789".getBytes(US_ASCII); + private static final Set SUPPORTED_KEY_TYPES = Set.of("RSA", "EC", "DSA"); + + private static final byte[] VERSION_0_ENCODED = new byte[]{2, 1, 0}; + private static final byte[] RSA_KEY_OID = encodeOID("1.2.840.113549.1.1.1"); + private static final byte[] DSA_KEY_OID = encodeOID("1.2.840.10040.4.1"); + private static final byte[] EC_KEY_OID = encodeOID("1.2.840.10045.2.1"); + private static final byte[] DER_NULL = new byte[]{5, 0}; + + private PemReader() { + } + + public static boolean isPem(String data) { + return isPemCertificate(data) || + isPemPublicKey(data) || + isPemPrivateKey(data); + } + + private static boolean isPemPrivateKey(String data) { + return PRIVATE_KEY_PATTERN.matcher(data).find(); + } + + private static boolean isPemPublicKey(String data) { + return PUBLIC_KEY_PATTERN.matcher(data).find(); + } + + private static boolean isPemCertificate(String data) { + return CERT_PATTERN.matcher(data).find(); + } + + public static KeyStore loadTrustStore(InputStream inputStream) + throws IOException, GeneralSecurityException { + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + List certificateChain = readCertificateChain(inputStream); + for (X509Certificate certificate : certificateChain) { + X500Principal principal = certificate.getSubjectX500Principal(); + keyStore.setCertificateEntry(principal.getName("RFC2253"), certificate); + } + return keyStore; + } + + public static KeyStore loadKeyStore(InputStream certificateChainInputStream, + InputStream privateKeyInputStream, + String keyPassword) + throws IOException, GeneralSecurityException { + return loadKeyStore(certificateChainInputStream, privateKeyInputStream, keyPassword, false); + } + + public static KeyStore loadKeyStore(InputStream certificateChainInputStream, + InputStream privateKeyInputStream, + String keyPassword, boolean storeKeyWithPassword) + throws IOException, GeneralSecurityException { + PrivateKey key = loadPrivateKey(privateKeyInputStream, keyPassword); + List certificateChain = readCertificateChain(certificateChainInputStream); + if (certificateChain.isEmpty()) { + throw new CertificateException("Certificate file does not contain any certificates"); + } + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + Certificate[] certificates = certificateChain.toArray(new Certificate[0]); + boolean foundMatchingCertificate = false; + for (int i = 0; i < certificates.length; i++) { + Certificate certificate = certificates[i]; + if (matches(key, certificate)) { + foundMatchingCertificate = true; + certificates[i] = certificates[0]; + certificates[0] = certificate; + break; + } + } + if (!foundMatchingCertificate) { + throw new KeyStoreException("Private key does not match the public key of any certificate"); + } + char[] password = (storeKeyWithPassword ? keyPassword : "").toCharArray(); + keyStore.setKeyEntry("key", key, password, certificates); + return keyStore; + } + + public static List readCertificateChain(InputStream certificateChain) + throws IOException, GeneralSecurityException { + return readCertificateChain(new String(certificateChain.readAllBytes(), StandardCharsets.US_ASCII)); + } + + public static List readCertificateChain(String certificateChain) + throws CertificateException { + Matcher matcher = CERT_PATTERN.matcher(certificateChain); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + List certificates = new ArrayList<>(); + int start = 0; + while (matcher.find(start)) { + byte[] buffer = base64Decode(matcher.group(1)); + certificates.add((X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(buffer))); + start = matcher.end(); + } + + return certificates; + } + + public static PrivateKey loadPrivateKey(InputStream inputStream, String keyPassword) + throws IOException, GeneralSecurityException { + return loadPrivateKey(new String(inputStream.readAllBytes(), US_ASCII), keyPassword); + } + + public static PrivateKey loadPrivateKey(String privateKey, String keyPassword) + throws IOException, GeneralSecurityException { + Matcher matcher = PRIVATE_KEY_PATTERN.matcher(privateKey); + if (!matcher.find()) { + throw new KeyStoreException("did not find a private key"); + } + String keyType = matcher.group(1); + String base64Key = matcher.group(2); + if (base64Key.toLowerCase(US).startsWith("proc-type")) { + throw new InvalidKeySpecException("Password protected PKCS#1 private keys are not supported"); + } + byte[] encodedKey = base64Decode(base64Key); + PKCS8EncodedKeySpec encodedKeySpec; + if (keyType == null) { + encodedKeySpec = new PKCS8EncodedKeySpec(encodedKey); + } else if ("ENCRYPTED".equals(keyType)) { + if (keyPassword == null) { + throw new KeyStoreException("Private key is encrypted, but no password was provided"); + } + EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); + SecretKey secretKey = keyFactory.generateSecret(new PBEKeySpec(keyPassword.toCharArray())); + Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName()); + cipher.init(DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters()); + encodedKeySpec = encryptedPrivateKeyInfo.getKeySpec(cipher); + } else { + return loadPkcs1PrivateKey(keyType, encodedKey); + } + // this code requires a key in PKCS8 format which is not the default openssl format + // to convert to the PKCS8 format you use : openssl pkcs8 -topk8 ... + Set algorithms = Set.of("RSA", "EC", "DSA"); + for (String algorithm : algorithms) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(algorithm); + return keyFactory.generatePrivate(encodedKeySpec); + } catch (InvalidKeySpecException ignore) { + } + } + throw new InvalidKeySpecException("Key type must be one of " + algorithms); + } + + private static PrivateKey loadPkcs1PrivateKey(String pkcs1KeyType, byte[] pkcs1Key) + throws GeneralSecurityException, IOException { + byte[] pkcs8Key; + switch (pkcs1KeyType) { + case "RSA": + pkcs8Key = rsaPkcs1ToPkcs8(pkcs1Key); + break; + case "DSA": + pkcs8Key = dsaPkcs1ToPkcs8(pkcs1Key); + break; + case "EC": + pkcs8Key = ecPkcs1ToPkcs8(pkcs1Key); + break; + default: + throw new InvalidKeySpecException(pkcs1KeyType + " private key in PKCS 1 format is not supported"); + } + try { + KeyFactory keyFactory = KeyFactory.getInstance(pkcs1KeyType); + return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Key)); + } catch (InvalidKeySpecException e) { + throw new InvalidKeySpecException(format("Invalid %s private key in PKCS 1 format", pkcs1KeyType), e); + } + } + + static byte[] rsaPublicKeyPkcs1ToPkcs8(byte[] pkcs1) throws IOException { + byte[] keyIdentifier = encodeSequence(RSA_KEY_OID, DER_NULL); + return encodeSequence(keyIdentifier, encodeBitString(0, pkcs1)); + } + + static byte[] rsaPkcs1ToPkcs8(byte[] pkcs1) throws IOException { + byte[] keyIdentifier = encodeSequence(RSA_KEY_OID, DER_NULL); + return encodeSequence(VERSION_0_ENCODED, keyIdentifier, encodeOctetString(pkcs1)); + } + + static byte[] dsaPkcs1ToPkcs8(byte[] pkcs1) + throws InvalidKeySpecException, IOException { + List elements = decodeSequence(pkcs1); + if (elements.size() != 6) { + throw new InvalidKeySpecException("Expected DSA key to have 6 elements"); + } + byte[] keyIdentifier = encodeSequence(DSA_KEY_OID, encodeSequence(elements.get(1), elements.get(2), elements.get(3))); + return encodeSequence(VERSION_0_ENCODED, keyIdentifier, encodeOctetString(elements.get(5))); + } + + static byte[] ecPkcs1ToPkcs8(byte[] pkcs1) + throws InvalidKeySpecException, IOException { + List elements = decodeSequence(pkcs1); + if (elements.size() != 4) { + throw new InvalidKeySpecException("Expected EC key to have 4 elements"); + } + byte[] curveOid = decodeSequenceOptionalElement(elements.get(2)); + byte[] keyIdentifier = encodeSequence(EC_KEY_OID, curveOid); + return encodeSequence(VERSION_0_ENCODED, keyIdentifier, encodeOctetString(encodeSequence(elements.get(0), elements.get(1), elements.get(3)))); + } + + public static PublicKey loadPublicKey(InputStream publicKeyFile) + throws IOException, GeneralSecurityException { + return loadPublicKey(new String(publicKeyFile.readAllBytes(), US_ASCII)); + } + + public static PublicKey loadPublicKey(String publicKey) + throws GeneralSecurityException { + Matcher matcher = PUBLIC_KEY_PATTERN.matcher(publicKey); + if (!matcher.find()) { + throw new KeyStoreException("did not find a public key"); + } + String keyType = matcher.group(1); + String base64Key = matcher.group(2); + byte[] encodedKey = base64Decode(base64Key); + + if (keyType == null) { + X509EncodedKeySpec encodedKeySpec = new X509EncodedKeySpec(encodedKey); + for (String algorithm : SUPPORTED_KEY_TYPES) { + try { + KeyFactory keyFactory = KeyFactory.getInstance(algorithm); + return keyFactory.generatePublic(encodedKeySpec); + } catch (InvalidKeySpecException ignore) { + } + } + throw new InvalidKeySpecException("Key type must be one of " + SUPPORTED_KEY_TYPES); + } + + if (!"RSA".equals(keyType)) { + throw new InvalidKeySpecException(format("%s public key in PKCS 1 format is not supported", keyType)); + } + try { + byte[] pkcs8Key = rsaPublicKeyPkcs1ToPkcs8(encodedKey); + KeyFactory keyFactory = KeyFactory.getInstance(keyType); + return keyFactory.generatePublic(new X509EncodedKeySpec(pkcs8Key)); + } catch (InvalidKeySpecException | IOException e) { + throw new InvalidKeySpecException(format("Invalid %s private key in PKCS 1 format", keyType), e); + } + } + + private static boolean matches(PrivateKey privateKey, Certificate certificate) { + try { + PublicKey publicKey = certificate.getPublicKey(); + + Signature signer = createSignature(privateKey, publicKey); + + signer.initSign(privateKey); + signer.update(TEST_SIGNATURE_DATA); + byte[] signature = signer.sign(); + + signer.initVerify(publicKey); + signer.update(TEST_SIGNATURE_DATA); + return signer.verify(signature); + } catch (GeneralSecurityException ignored) { + return false; + } + } + + private static Signature createSignature(PrivateKey privateKey, PublicKey publicKey) + throws GeneralSecurityException { + if (privateKey instanceof RSAPrivateKey && publicKey instanceof RSAPublicKey) { + return Signature.getInstance("NONEwithRSA"); + } + if (privateKey instanceof ECPrivateKey && publicKey instanceof ECPublicKey) { + return Signature.getInstance("NONEwithECDSA"); + } + if (privateKey instanceof DSAKey && publicKey instanceof DSAKey) { + return Signature.getInstance("NONEwithDSA"); + } + throw new InvalidKeySpecException("Key type must be one of " + SUPPORTED_KEY_TYPES); + } + + public static byte[] base64Decode(String base64) { + return getMimeDecoder().decode(base64.getBytes(US_ASCII)); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/PemReader2.java b/net-security/src/main/java/org/xbib/net/security/PemReader2.java new file mode 100644 index 0000000..e32b823 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/PemReader2.java @@ -0,0 +1,126 @@ +package org.xbib.net.security; + +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.security.auth.x500.X500Principal; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.util.regex.Pattern.CASE_INSENSITIVE; +import static javax.crypto.Cipher.DECRYPT_MODE; + +public final class PemReader2 { + + private static final Pattern CERT_PATTERN = Pattern.compile( + "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+" + // Header + "([a-z0-9+/=\\r\\n]+)" + // Base64 text + "-+END\\s+.*CERTIFICATE[^-]*-+", // Footer + CASE_INSENSITIVE); + + private static final Pattern KEY_PATTERN = Pattern.compile( + "-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header + "([a-z0-9+/=\\r\\n]+)" + // Base64 text + "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer + CASE_INSENSITIVE); + + private PemReader2() { + } + + public static KeyStore loadTrustStore(InputStream certificateChainInputStream) + throws IOException, GeneralSecurityException { + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + + List certificateChain = readCertificateChain(certificateChainInputStream); + for (X509Certificate certificate : certificateChain) { + X500Principal principal = certificate.getSubjectX500Principal(); + keyStore.setCertificateEntry(principal.getName("RFC2253"), certificate); + } + return keyStore; + } + + public static KeyStore loadKeyStore(InputStream certificateChainInputStream, + InputStream privateKeyInputStream, + String keyPassword) + throws IOException, GeneralSecurityException { + PKCS8EncodedKeySpec encodedKeySpec = readPrivateKey(privateKeyInputStream, keyPassword); + PrivateKey key; + try { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + key = keyFactory.generatePrivate(encodedKeySpec); + } catch (InvalidKeySpecException ignore) { + KeyFactory keyFactory = KeyFactory.getInstance("DSA"); + key = keyFactory.generatePrivate(encodedKeySpec); + } + List certificateChain = readCertificateChain(certificateChainInputStream); + if (certificateChain.isEmpty()) { + throw new CertificateException("Certificate file does not contain any certificates: " + certificateChainInputStream); + } + + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + char[] password = (keyPassword != null ? keyPassword : "").toCharArray(); + keyStore.setKeyEntry("key", key, password, certificateChain.toArray(Certificate[]::new)); + return keyStore; + } + + private static List readCertificateChain(InputStream certificateChainInputStream) + throws IOException, GeneralSecurityException { + String contents = new String(certificateChainInputStream.readAllBytes(), US_ASCII); + Matcher matcher = CERT_PATTERN.matcher(contents); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + List certificates = new ArrayList<>(); + int start = 0; + while (matcher.find(start)) { + byte[] buffer = base64Decode(matcher.group(1)); + certificates.add((X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(buffer))); + start = matcher.end(); + } + + return certificates; + } + + private static PKCS8EncodedKeySpec readPrivateKey(InputStream inputStream, String keyPassword) + throws IOException, GeneralSecurityException { + String content = new String(inputStream.readAllBytes(), US_ASCII); + Matcher matcher = KEY_PATTERN.matcher(content); + if (!matcher.find()) { + throw new KeyStoreException("found no private key"); + } + byte[] encodedKey = base64Decode(matcher.group(1)); + if (keyPassword == null) { + return new PKCS8EncodedKeySpec(encodedKey); + } + EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); + SecretKey secretKey = keyFactory.generateSecret(new PBEKeySpec(keyPassword.toCharArray())); + Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName()); + cipher.init(DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters()); + return encryptedPrivateKeyInfo.getKeySpec(cipher); + } + + private static byte[] base64Decode(String base64) { + return Base64.getMimeDecoder().decode(base64.getBytes(US_ASCII)); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/PrivateKeyReader.java b/net-security/src/main/java/org/xbib/net/security/PrivateKeyReader.java new file mode 100644 index 0000000..0bec8ec --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/PrivateKeyReader.java @@ -0,0 +1,384 @@ +package org.xbib.net.security; + +import org.xbib.net.security.eddsa.EdDSAPrivateKey; +import org.xbib.net.security.eddsa.spec.EdDSANamedCurveTable; +import org.xbib.net.security.eddsa.spec.EdDSAPrivateKeySpec; +import org.xbib.net.security.util.Asn1Object; +import org.xbib.net.security.util.DerParser; +import org.xbib.net.security.util.DerUtils; + +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.ECField; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Class for reading RSA private key from PEM formatted text. + * It can read PEM files with PKCS#8 or PKCS#1 encodings. + * It doesn't support encrypted PEM files. + */ +public class PrivateKeyReader { + + private static final byte[] BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] END_PRIVATE_KEY = "-----END PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] BEGIN_RSA_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] END_RSA_PRIVATE_KEY = "-----END RSA PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] BEGIN_DSA_PRIVATE_KEY = "-----BEGIN DSA PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] END_DSA_PRIVATE_KEY = "-----END DSA PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] BEGIN_EC_PRIVATE_KEY = "-----BEGIN EC PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] END_EC_PRIVATE_KEY = "-----END EC PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] BEGIN_OPENSSH_PRIVATE_KEY = "-----BEGIN OPENSSH PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + + private static final byte[] END_OPENSSH_PRIVATE_KEY = "-----END OPENSSH PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + + public PrivateKeyReader() { + } + + public PrivateKey readPrivateKey(InputStream inputStream, String password) + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, + InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException { + Objects.requireNonNull(inputStream); + byte[] key = inputStream.readAllBytes(); + if (indexOf(key, BEGIN_PRIVATE_KEY,0, key.length) >= 0) { + byte[] keyBytes = extract(key, BEGIN_PRIVATE_KEY, END_PRIVATE_KEY); + EncodedKeySpec keySpec = generateKeySpec(keyBytes, password != null ? password.toCharArray() : null); + return KeyFactory.getInstance("RSA").generatePrivate(keySpec); + } + if (indexOf(key, BEGIN_RSA_PRIVATE_KEY,0, key.length) >= 0) { + byte[] keyBytes = extract(key, BEGIN_RSA_PRIVATE_KEY, END_RSA_PRIVATE_KEY); + RSAPrivateCrtKeySpec keySpec = getRSAKeySpec(keyBytes); + return KeyFactory.getInstance("RSA").generatePrivate(keySpec); + } + if (indexOf(key, BEGIN_DSA_PRIVATE_KEY,0, key.length) >= 0) { + byte[] keyBytes = extract(key, BEGIN_DSA_PRIVATE_KEY, END_DSA_PRIVATE_KEY); + DSAPrivateKeySpec keySpec = getDSAKeySpec(keyBytes); + return KeyFactory.getInstance("DSA").generatePrivate(keySpec); + } + if (indexOf(key, BEGIN_EC_PRIVATE_KEY,0, key.length) >= 0) { + byte[] keyBytes = extract(key, BEGIN_EC_PRIVATE_KEY, END_EC_PRIVATE_KEY); + ECPrivateKeySpec keySpec = getECKeySpec(keyBytes); + return KeyFactory.getInstance("EC").generatePrivate(keySpec); + } + if (indexOf(key, BEGIN_OPENSSH_PRIVATE_KEY,0, key.length) >= 0) { + byte[] keyBytes = extract(key, BEGIN_OPENSSH_PRIVATE_KEY, END_OPENSSH_PRIVATE_KEY); + byte[] sk = Arrays.copyOfRange(keyBytes, 0, 32); + return new EdDSAPrivateKey(new EdDSAPrivateKeySpec(sk, EdDSANamedCurveTable.getByName("Ed25519"))); + } + throw new IOException("invalid PEM"); + } + + /** + * Convert PKCS#1 encoded private key into RSAPrivateCrtKeySpec. + * The ASN.1 syntax for the private key with CRT is + *
+     * --
+     * -- Representation of RSA private key with information for the CRT algorithm.
+     * --
+     * RSAPrivateKey ::= SEQUENCE {
+     *   version           Version,
+     *   modulus           INTEGER,  -- n
+     *   publicExponent    INTEGER,  -- e
+     *   privateExponent   INTEGER,  -- d
+     *   prime1            INTEGER,  -- p
+     *   prime2            INTEGER,  -- q
+     *   exponent1         INTEGER,  -- d mod (p-1)
+     *   exponent2         INTEGER,  -- d mod (q-1)
+     *   coefficient       INTEGER,  -- (inverse of q) mod p
+     *   otherPrimeInfos   OtherPrimeInfos OPTIONAL
+     * }
+     * 
+ * + * @param keyBytes PKCS#1 encoded key + * @return KeySpec + * @throws IOException if failure + */ + private static RSAPrivateCrtKeySpec getRSAKeySpec(byte[] keyBytes) throws IOException { + DerParser parser = new DerParser(keyBytes); + Asn1Object sequence = parser.read(); + if (sequence.getType() != DerParser.SEQUENCE) { + throw new IOException("invalid DER: not a sequence"); + } + parser = sequence.getParser(); + parser.read(); // skip version + BigInteger modulus = parser.read().getInteger(); + BigInteger publicExp = parser.read().getInteger(); + BigInteger privateExp = parser.read().getInteger(); + BigInteger prime1 = parser.read().getInteger(); + BigInteger prime2 = parser.read().getInteger(); + BigInteger exp1 = parser.read().getInteger(); + BigInteger exp2 = parser.read().getInteger(); + BigInteger crtCoef = parser.read().getInteger(); + return new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crtCoef); + } + + /** + * Read DSA key in PKCS#1 spec. + * + * @param keyBytes PKCS#1 encoded key + * @return DSA private key spec + * @throws IOException if ASN.1 parsing fails + */ + private DSAPrivateKeySpec getDSAKeySpec(byte[] keyBytes) throws IOException { + DerParser parser = new DerParser(keyBytes); + Asn1Object sequence = parser.read(); + if (sequence.getType() != DerParser.SEQUENCE) { + throw new IOException("invalid DER: not a sequence"); + } + parser = sequence.getParser(); + parser.read(); // skip version + BigInteger p = parser.read().getInteger(); + BigInteger q = parser.read().getInteger(); + BigInteger g = parser.read().getInteger(); + BigInteger pub = parser.read().getInteger(); + BigInteger prv = parser.read().getInteger(); + return new DSAPrivateKeySpec(prv, p, q, g); + } + + /** + * Read EC private key in PKCS#1 format. + * + * ECPrivateKey ::= SEQUENCE { + * version INTEGER { ecPrivkeyVer1(1) }, + * privateKey OCTET STRING, + * parameters [0] EXPLICIT ECDomainParameters OPTIONAL, + * publicKey [1] EXPLICIT BIT STRING OPTIONAL + * } + * + * openssl asn1parse -i -in net-security/src/test/resources/ec.key + * 0:d=0 hl=2 l= 119 cons: SEQUENCE + * 2:d=1 hl=2 l= 1 prim: INTEGER :01 + * 5:d=1 hl=2 l= 32 prim: OCTET STRING [HEX DUMP]:7D9A378C22E17F85643D6D8B4EC14931220329FF5D03D20F4E15095BE40890F7 + * 39:d=1 hl=2 l= 10 cons: cont [ 0 ] + * 41:d=2 hl=2 l= 8 prim: OBJECT :prime256v1 + * 51:d=1 hl=2 l= 68 cons: cont [ 1 ] + * 53:d=2 hl=2 l= 66 prim: BIT STRING + * + * OID 1.2.840.10045.3.1.7 = "prime256v1" + * + * @param keyBytes PKCS#1 encoded key + * @return the EC private key spec + * @throws IOException if ASN.1 parsing fails + */ + private ECPrivateKeySpec getECKeySpec(byte[] keyBytes) throws IOException { + DerParser parser = new DerParser(keyBytes); + Asn1Object sequence = parser.read(); + if (sequence.getType() != DerParser.SEQUENCE) { + throw new IOException("invalid DER: not a sequence"); + } + parser = sequence.getParser(); + parser.read(); // skip version + byte[] privateKey = parser.read().getValue(); + Asn1Object asn1Object = parser.read(); + if (asn1Object.getType() != DerParser.ANY) { + throw new IOException("invalid DER: not any: " + asn1Object.getType()); + } + int[] oid = DerUtils.decodeOID(asn1Object.getValue()); + BigInteger bigInteger = new BigInteger(1, privateKey); + String oidString = DerUtils.oidToString(oid); + if (SECP256R1.getObjectId().equals(oidString)) { + return new ECPrivateKeySpec(bigInteger, SECP256R1); + } else if (SECP384R1.getObjectId().equals(oidString)) { + return new ECPrivateKeySpec(bigInteger, SECP384R1); + } else if (SECP521R1.getObjectId().equals(oidString)) { + return new ECPrivateKeySpec(bigInteger, SECP521R1); + } else { + throw new IOException("invalid DER: unknown algo: " + oidString); + } + } + + private static final Curve SECP256R1 = initializeCurve( + "secp256r1 [NIST P-256, X9.62 prime256v1]", + "1.2.840.10045.3.1.7", + "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", + "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", + "5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", + "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", + 1 + ); + + private static final Curve SECP384R1 = initializeCurve( + "secp384r1 [NIST P-384]", + "1.3.132.0.34", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", + "B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", + "AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", + "3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", + 1 + ); + + private static final Curve SECP521R1 = initializeCurve( + "secp521r1 [NIST P-521]", + "1.3.132.0.35", + "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC", + "0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00", + "00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66", + "011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650", + "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409", + 1 + ); + + private static Curve initializeCurve(String name, String oid, + String sfield, String a, String b, + String x, String y, String n, int h) { + BigInteger p = bigInt(sfield); + ECField field = new ECFieldFp(p); + EllipticCurve curve = new EllipticCurve(field, bigInt(a),bigInt(b)); + ECPoint g = new ECPoint(bigInt(x), bigInt(y)); + return new Curve(name, oid, curve, g, bigInt(n), h); + } + + static final class Curve extends ECParameterSpec { + private final String name; + private final String oid; + + Curve(String name, String oid, EllipticCurve curve, + ECPoint g, BigInteger n, int h) { + super(curve, g, n, h); + this.name = name; + this.oid = oid; + } + private String getName() { + return name; + } + private String getObjectId() { + return oid; + } + } + + private static BigInteger bigInt(String s) { + return new BigInteger(s, 16); + } + + private static PKCS8EncodedKeySpec generateKeySpec(byte[] key, char[] password) + throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, + InvalidKeyException, InvalidAlgorithmParameterException { + if (password == null) { + return new PKCS8EncodedKeySpec(key); + } + EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(key); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); + PBEKeySpec pbeKeySpec = new PBEKeySpec(password); + SecretKey pbeKey = keyFactory.generateSecret(pbeKeySpec); + Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName()); + cipher.init(Cipher.DECRYPT_MODE, pbeKey, encryptedPrivateKeyInfo.getAlgParameters()); + return encryptedPrivateKeyInfo.getKeySpec(cipher); + } + + private static int indexOf(byte[] array, byte[] target, int start, int end) { + if (target.length == 0) { + return 0; + } + outer: + for (int i = start; i < end - target.length + 1; i++) { + for (int j = 0; j < target.length; j++) { + if (array[i + j] != target[j]) { + continue outer; + } + } + return i; + } + return -1; + } + + private static byte[] extract(byte[] array, byte[] b1, byte[] b2) { + int i1 = indexOf(array, b1, 0, array.length); + if (i1 < 0) { + throw new IllegalArgumentException("unable to extract: not found"); + } + int i2 = indexOf(array, b2, 0, array.length); + if (i2 < 0) { + throw new IllegalArgumentException("unable to extract: not found"); + } + int start = i1 + b1.length; + byte[] b = new byte[i2 - start]; + System.arraycopy(array, start, b, 0, b.length); + return Base64.getMimeDecoder().decode(b); + } + + private static final String[] KEY_TYPES = { + "RSA", "DSA", "EC" + }; + + private static final Pattern KEY_PATTERN = + Pattern.compile("-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + + "([a-z0-9+/=\\r\\n]+)" + "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", Pattern.CASE_INSENSITIVE); + + public static PrivateKey toPrivateKey(InputStream keyInputStream, String keyPassword) + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, + InvalidAlgorithmParameterException, KeyException, IOException { + if (keyInputStream == null) { + return null; + } + return getPrivateKey(readPrivateKey(keyInputStream), keyPassword); + } + + public static PrivateKey getPrivateKey(byte[] key, String keyPassword) + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, + InvalidAlgorithmParameterException, KeyException, IOException { + PKCS8EncodedKeySpec encodedKeySpec = + generateKeySpec(key, keyPassword == null ? null : keyPassword.toCharArray()); + for (String keyType : KEY_TYPES) { + try { + return KeyFactory.getInstance(keyType).generatePrivate(encodedKeySpec); + } catch (InvalidKeySpecException e) { + // ignore + } + } + throw new InvalidKeySpecException("Neither RSA, DSA nor EC worked"); + } + + private static byte[] readPrivateKey(InputStream inputStream) throws KeyException, IOException { + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.US_ASCII))) { + String string = bufferedReader.lines().collect(Collectors.joining(System.lineSeparator())); + Matcher m = KEY_PATTERN.matcher(string); + if (!m.find()) { + throw new KeyException("could not find a PKCS #8 private key in input stream"); + } + return Base64.getMimeDecoder().decode(m.group(1)); + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/SSLUtil.java b/net-security/src/main/java/org/xbib/net/security/SSLUtil.java new file mode 100644 index 0000000..9f2b8e2 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/SSLUtil.java @@ -0,0 +1,103 @@ +package org.xbib.net.security; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +public final class SSLUtil { + + private SSLUtil() { + } + + public static void createSslContextFactory(File cert, File privateKey) { + try { + byte[][] certBytes = parseDERFromPEM(Files.readAllBytes(cert.toPath()), + "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----"); + byte[][] keyBytes = parseDERFromPEM(Files.readAllBytes(privateKey.toPath()), + "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----"); + X509Certificate[] xCerts = new X509Certificate[certBytes.length]; + RSAPrivateKey key = generatePrivateKeyFromDER(keyBytes[0]); + for (int i = 0; i < certBytes.length; i++) { + xCerts[i] = generateCertificateFromDER(certBytes[i]); + } + KeyStore keystore = KeyStore.getInstance("PKCS12"); + keystore.load(null); + keystore.setCertificateEntry("cert-alias", xCerts[0]); + keystore.setKeyEntry("key-alias", key, null, xCerts); + /*SslContextFactory sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStore(keystore); + sslContextFactory.setKeyStorePassword(TEMP_PW); + return sslContextFactory;*/ + } catch (IOException | KeyStoreException | InvalidKeySpecException + | NoSuchAlgorithmException | CertificateException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Parses the given PEM certificate (chain) to DER certificates. + * + * @param pem The PEM certificate (chain) to convert. + * @param beginDelimiter The begin delimiter of the certificate. + * @param endDelimiter The end delimiter of the certificate. + * @return An array containing all certificates as binary (byte[]) data. + */ + private static byte[][] parseDERFromPEM(byte[] pem, String beginDelimiter, String endDelimiter) { + String data = new String(pem); + String[] elements = data.split(beginDelimiter); + List newTokens = new ArrayList<>(); + for (int i = 1; i < elements.length; i++) { + newTokens.add(elements[i].split(endDelimiter)[0]); + } + byte[][] ders = new byte[2][]; + for (int i = 0; i < newTokens.size(); i++) { + String string = newTokens.get(i); + ders[i] = Base64.getMimeDecoder().decode(string); + } + return ders; + } + + /** + * Generates a {@link RSAPrivateKey} from the given DER key. + * + * @param keyBytes The private key as binary (byte[]) data. + * @return An {@link RSAPrivateKey} instance representing the key. + * @throws InvalidKeySpecException The key is inappropriate. + * @throws NoSuchAlgorithmException No provider supports RSA. + */ + private static RSAPrivateKey generatePrivateKeyFromDER(byte[] keyBytes) + throws InvalidKeySpecException, NoSuchAlgorithmException { + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory factory = KeyFactory.getInstance("RSA"); + return (RSAPrivateKey) factory.generatePrivate(spec); + } + + /** + * Generates an {@link X509Certificate} from the given DER certificate. + * + * @param certBytes The certificate as binary (byte[]) data. + * @return An {@link X509Certificate} instance representing the certificate. + * @throws CertificateException No provider supports X.509. + */ + private static X509Certificate generateCertificateFromDER(byte[] certBytes) + throws CertificateException { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate( + new ByteArrayInputStream(certBytes)); + } + +} \ No newline at end of file diff --git a/net-security/src/main/java/org/xbib/net/security/SecureSocketFactory.java b/net-security/src/main/java/org/xbib/net/security/SecureSocketFactory.java new file mode 100644 index 0000000..29fc291 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/SecureSocketFactory.java @@ -0,0 +1,92 @@ +package org.xbib.net.security; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public class SecureSocketFactory extends SSLSocketFactory { + + private final SSLSocketFactory sslSocketFactory; + + private static volatile SecureSocketFactory secureSocketFactory; + + private SecureSocketFactory() throws KeyManagementException, KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { + sslSocketFactory = loadTrustStore(); + } + + public static SocketFactory getInstance() { + try { + if (secureSocketFactory == null) { + secureSocketFactory = new SecureSocketFactory(); + } + return secureSocketFactory; + } catch (Exception e) { + throw new IllegalStateException("Failed create socket factory. Exception: " + e.getClass().getName() + ". Reason: " + e.getMessage(), e); + } + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { + return sslSocketFactory.createSocket(socket, host, port, autoClose); + } + + @Override + public String[] getDefaultCipherSuites() { + return sslSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sslSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return sslSocketFactory.createSocket(host, port); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return sslSocketFactory.createSocket(host, port); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + return sslSocketFactory.createSocket(host, port, localHost, localPort); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return sslSocketFactory.createSocket(address, port, localAddress, localPort); + } + + private SSLSocketFactory loadTrustStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, KeyManagementException, CertificateException { + String keyStoreType = System.getProperty("truststore.type"); + String keyStorePath = System.getProperty("truststore.path"); + String password = System.getProperty("truststore.password"); + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + Path path = Paths.get(keyStorePath); + try (InputStream inputStream = Files.newInputStream(path)) { + keyStore.load(inputStream, password != null ? password.toCharArray() : null); + } + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + String sslContextProtocol = System.getProperty("truststore.ssl.protocol"); + SSLContext sslContext = sslContextProtocol != null ? SSLContext.getInstance(sslContextProtocol) : SSLContext.getDefault(); + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + return sslContext.getSocketFactory(); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/cookie/Algo.java b/net-security/src/main/java/org/xbib/net/security/cookie/Algo.java new file mode 100644 index 0000000..53c5d58 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/cookie/Algo.java @@ -0,0 +1,20 @@ +package org.xbib.net.security.cookie; + +public enum Algo { + MD5("MD5", "md5"), + SHA("SHA", "sha"), + SHA256("SHA-256", "sha256"), + SHA512("SHA-512", "sha512"), + SSHA("SHA1", "ssha"), + SSHA256("SHA-256", "ssha"), + SSHA512("SHA-512", "ssha"); + + String algo; + + String prefix; + + Algo(String algo, String prefix) { + this.algo = algo; + this.prefix = prefix; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/cookie/Codec.java b/net-security/src/main/java/org/xbib/net/security/cookie/Codec.java new file mode 100644 index 0000000..e057336 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/cookie/Codec.java @@ -0,0 +1,5 @@ +package org.xbib.net.security.cookie; + +public enum Codec { + BASE64, HEX +} diff --git a/net-security/src/main/java/org/xbib/net/security/cookie/CookieSigner.java b/net-security/src/main/java/org/xbib/net/security/cookie/CookieSigner.java new file mode 100644 index 0000000..7751e6c --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/cookie/CookieSigner.java @@ -0,0 +1,166 @@ +package org.xbib.net.security.cookie; + +import org.xbib.net.PercentDecoder; +import org.xbib.net.PercentEncoder; +import org.xbib.net.PercentEncoders; + +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +public class CookieSigner { + + private final String signature; + + private final String publicValue; + + private final String privateValue; + + private final String cookieValue; + + /** + * Construct cookie for signing. + * + * @param charset the character set + * @param hmac the HMAC code + * @param codec the codec for the private value + * @param privateValue the private value + * @param publicValue the public value + * @param secret the secret + * @throws MalformedInputException if signing cookie fails + * @throws UnmappableCharacterException if signing cookie fails + * @throws NoSuchAlgorithmException if signing cookie fails + * @throws InvalidKeyException if signing cookie fails + */ + private CookieSigner(Charset charset, HMac hmac, Codec codec, String privateValue, String publicValue, String secret) + throws MalformedInputException, UnmappableCharacterException, NoSuchAlgorithmException, InvalidKeyException { + PercentEncoder percentEncoder = PercentEncoders.getCookieEncoder(charset); + this.privateValue = privateValue; + this.publicValue = publicValue; + this.signature = CryptUtil.hmac(charset, hmac, codec, privateValue, secret); + this.cookieValue = percentEncoder.encode(String.join(":", publicValue, privateValue, signature)); + } + + /** + * Parse signed cookie value. + * + * @param charset the character set + * @param hmac the HMAC code + * @param codec the codec for the private value + * @param rawValue the raw value for parsing + * @param secret the secret + * @throws MalformedInputException if parsing failed + * @throws UnmappableCharacterException if parsing failed + * @throws NoSuchAlgorithmException if parsing failed + * @throws InvalidKeyException if parsing failed + * @throws SignatureException if signature is invalid + */ + private CookieSigner(Charset charset, HMac hmac, Codec codec, String rawValue, String secret) + throws MalformedInputException, UnmappableCharacterException, NoSuchAlgorithmException, InvalidKeyException, + SignatureException { + PercentDecoder persentDecoder = new PercentDecoder(charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + ); + String[] s = persentDecoder.decode(rawValue).split(":", 3); + if (s.length != 3) { + throw new IllegalStateException("unable to find three colon-separated components in cookie value"); + } + this.signature = CryptUtil.hmac(charset, hmac, codec, s[1], secret); + if (!s[2].equals(signature)) { + throw new SignatureException("HMAC signature does not match"); + } + this.publicValue = s[0]; + this.privateValue = s[1]; + this.cookieValue = rawValue; + } + + public static Builder builder() { + return new Builder(); + } + + public String getPublicValue() { + return publicValue; + } + + public String getPrivateValue() { + return privateValue; + } + + public String getCookieValue() { + return cookieValue; + } + + public static class Builder { + + private Charset charset; + + private HMac hmac; + + private Codec codec; + + private String privateValue; + + private String publicValue; + + private String secret; + + private String rawValue; + + public Builder() { + this.charset = StandardCharsets.UTF_8; + this.hmac = HMac.HMAC_SHA1; + this.codec = Codec.BASE64; + } + + public Builder withCharset(Charset charset) { + this.charset = charset; + return this; + } + + public Builder withHMac(HMac hmac) { + this.hmac = hmac; + return this; + } + + public Builder withCodec(Codec codec) { + this.codec = codec; + return this; + } + + public Builder withPrivateValue(String privateValue) { + this.privateValue = privateValue; + return this; + } + + public Builder withPublicValue(String publicValue) { + this.publicValue = publicValue; + return this; + } + + public Builder withSecret(String secret) { + this.secret = secret; + return this; + } + + public Builder withRawValue(String rawValue) { + this.rawValue = rawValue; + return this; + } + + public CookieSigner build() { + try { + return rawValue != null ? + new CookieSigner(charset, hmac, codec, rawValue, secret) : + new CookieSigner(charset, hmac, codec, privateValue, publicValue, secret); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/cookie/CryptUtil.java b/net-security/src/main/java/org/xbib/net/security/cookie/CryptUtil.java new file mode 100644 index 0000000..4ac82db --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/cookie/CryptUtil.java @@ -0,0 +1,151 @@ +package org.xbib.net.security.cookie; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Objects; +import java.util.Random; + +/** + * A utility class for invoking encryption methods and returning password strings, + * using {@link MessageDigest} and {@link Mac}. + */ +public class CryptUtil { + + private static final Random random = new SecureRandom(); + + private CryptUtil() { + } + + public static String randomHex(int length) { + byte[] b = new byte[length]; + random.nextBytes(b); + return encodeHex(b); + } + + public static String sha(String plainText) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), null, Algo.SHA.algo, Algo.SHA.prefix); + } + + public static String sha256(String plainText) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), null, Algo.SHA256.algo, Algo.SHA256.prefix); + } + + public static String sha512(String plainText) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), null, Algo.SHA512.algo, Algo.SHA512.prefix); + } + + public static String ssha(String plainText, byte[] salt) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), salt, Algo.SSHA.algo, Algo.SSHA.prefix); + } + + public static String ssha256(String plainText, byte[] salt) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), salt, Algo.SSHA256.algo, Algo.SSHA256.prefix); + } + + public static String ssha512(String plainText, byte[] salt) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), salt, Algo.SSHA512.algo, Algo.SSHA512.prefix); + } + + public static String hmacSHA1(Charset charset, String plainText, String secret) throws NoSuchAlgorithmException, InvalidKeyException { + return hmac(HMac.HMAC_SHA1, Codec.BASE64, plainText.getBytes(charset), secret.getBytes(charset)); + } + + public static String hmacSHA1(Charset charset, byte[] plainText, String secret) throws InvalidKeyException, NoSuchAlgorithmException { + return hmac(HMac.HMAC_SHA1, Codec.BASE64, plainText, secret.getBytes(charset)); + } + + public static String hmacSHA1(byte[] plainText, byte[] secret) throws InvalidKeyException, NoSuchAlgorithmException { + return hmac(HMac.HMAC_SHA1, Codec.BASE64, plainText, secret); + } + + public static String hmacSHA256(Charset charset, String plainText, String secret) throws NoSuchAlgorithmException, InvalidKeyException { + return hmac(HMac.HMAC_SHA256, Codec.BASE64, plainText.getBytes(charset), secret.getBytes(charset)); + } + + public static String hmacSHA256(Charset charset, byte[] plainText, String secret) throws InvalidKeyException, NoSuchAlgorithmException { + return hmac(HMac.HMAC_SHA256, Codec.BASE64, plainText, secret.getBytes(charset)); + } + + public static String hmacSHA256(byte[] plainText, byte[] secret) throws InvalidKeyException, NoSuchAlgorithmException { + return hmac(HMac.HMAC_SHA256, Codec.BASE64, plainText, secret); + } + + public static String hmac(Charset charset, HMac hmac, Codec codec, String plainText, String secret) throws InvalidKeyException, NoSuchAlgorithmException { + return hmac(hmac, codec, plainText.getBytes(charset), secret.getBytes(charset)); + } + + public static String digest(Codec codec, byte[] plainText, byte[] salt, String algo, String prefix) throws NoSuchAlgorithmException { + Objects.requireNonNull(plainText); + MessageDigest digest = MessageDigest.getInstance(algo); + digest.update(plainText); + byte[] bytes = digest.digest(); + if (salt != null) { + digest.update(salt); + byte[] hash = digest.digest(); + bytes = new byte[salt.length + hash.length]; + System.arraycopy(hash, 0, bytes, 0, hash.length); + System.arraycopy(salt, 0, bytes, hash.length, salt.length); + } + return '{' + prefix + '}' + + (codec == Codec.BASE64 ? Base64.getEncoder().encodeToString(bytes) : + codec == Codec.HEX ? encodeHex(bytes) : null); + } + + public static String hmac(HMac hmac, Codec codec, byte[] plainText, byte[] secret) throws NoSuchAlgorithmException, InvalidKeyException { + Objects.requireNonNull(plainText); + Objects.requireNonNull(secret); + Mac mac = Mac.getInstance(hmac.getAlgo()); + SecretKeySpec secretKeySpec = new SecretKeySpec(secret, hmac.getAlgo()); + mac.init(secretKeySpec); + return codec == Codec.BASE64 ? Base64.getEncoder().encodeToString(mac.doFinal(plainText)) : + codec == Codec.HEX ? encodeHex(mac.doFinal(plainText)) : null; + } + + public static String encodeHex(byte[] bytes) { + StringBuilder stringBuilder = new StringBuilder(); + for (byte b : bytes) { + stringBuilder.append(Integer.toHexString((int) b & 0xFF)); + } + return stringBuilder.toString(); + } + + /** + * Decodes the hex-encoded bytes and returns their value a byte string. + * + * @param hex hexidecimal code + * @return string + */ + public static byte[] decodeHex(String hex) { + Objects.requireNonNull(hex); + if (hex.length() % 2 != 0) { + throw new IllegalArgumentException("unexpected hex string " + hex); + } + byte[] result = new byte[hex.length() / 2]; + for (int i = 0; i < result.length; i++) { + int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4; + int d2 = decodeHexDigit(hex.charAt(i * 2 + 1)); + result[i] = (byte) (d1 + d2); + } + return result; + } + + private static int decodeHexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + throw new IllegalArgumentException("unexpected hex digit " + c); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/cookie/HMac.java b/net-security/src/main/java/org/xbib/net/security/cookie/HMac.java new file mode 100644 index 0000000..cbbf152 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/cookie/HMac.java @@ -0,0 +1,16 @@ +package org.xbib.net.security.cookie; + +public enum HMac { + HMAC_SHA1("HMacSHA1"), + HMAC_SHA256("HMacSHA256"); + + String algo; + + HMac(String algo) { + this.algo = algo; + } + + public String getAlgo() { + return algo; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAEngine.java b/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAEngine.java new file mode 100644 index 0000000..ae91e23 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAEngine.java @@ -0,0 +1,457 @@ +package org.xbib.net.security.eddsa; + +import org.xbib.net.security.eddsa.math.Curve; +import org.xbib.net.security.eddsa.math.GroupElement; +import org.xbib.net.security.eddsa.math.ScalarOps; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; + +/** + * Signing and verification for EdDSA. + *

+ * The EdDSA sign and verify algorithms do not interact well with + * the Java Signature API, as one or more update() methods must be + * called before sign() or verify(). Using the standard API, + * this implementation must copy and buffer all data passed in + * via update(). + *

+ * This implementation offers two ways to avoid this copying, + * but only if all data to be signed or verified is available + * in a single byte array. + *

+ * Option 1: + *

    + *
  1. Call initSign() or initVerify() as usual. + *
  2. Call setParameter(ONE_SHOT_MODE) + *
  3. Call update(byte[]) or update(byte[], int, int) exactly once + *
  4. Call sign() or verify() as usual. + *
  5. If doing additional one-shot signs or verifies with this object, you must + * call setParameter(ONE_SHOT_MODE) each time + *
+ *

+ * Option 2: + *

    + *
  1. Call initSign() or initVerify() as usual. + *
  2. Call one of the signOneShot() or verifyOneShot() methods. + *
  3. If doing additional one-shot signs or verifies with this object, + * just call signOneShot() or verifyOneShot() again. + *
+ */ +public final class EdDSAEngine extends Signature { + public static final String SIGNATURE_ALGORITHM = "NONEwithEdDSA"; + /** + * To efficiently sign or verify data in one shot, pass this to setParameters() + * after initSign() or initVerify() but BEFORE THE FIRST AND ONLY + * update(data) or update(data, off, len). The data reference will be saved + * and then used in sign() or verify() without copying the data. + * Violate these rules and you will get a SignatureException. + */ + public static final AlgorithmParameterSpec ONE_SHOT_MODE = new OneShotSpec(); + private MessageDigest digest; + private ByteArrayOutputStream baos; + private EdDSAKey key; + private boolean oneShotMode; + private byte[] oneShotBytes; + private int oneShotOffset; + private int oneShotLength; + + /** + * No specific EdDSA-internal hash requested, allows any EdDSA key. + */ + public EdDSAEngine() { + super(SIGNATURE_ALGORITHM); + } + + /** + * Specific EdDSA-internal hash requested, only matching keys will be allowed. + * + * @param digest the hash algorithm that keys must have to sign or verify. + */ + public EdDSAEngine(MessageDigest digest) { + this(); + this.digest = digest; + } + + private void reset() { + if (digest != null) { + digest.reset(); + } + if (baos != null) { + baos.reset(); + } + oneShotMode = false; + oneShotBytes = null; + } + + @Override + protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { + reset(); + if (privateKey instanceof EdDSAPrivateKey) { + EdDSAPrivateKey privKey = (EdDSAPrivateKey) privateKey; + key = privKey; + if (digest == null) { + try { + digest = MessageDigest.getInstance(key.getParams().getHashAlgorithm()); + } catch (NoSuchAlgorithmException e) { + throw new InvalidKeyException("cannot get required digest " + key.getParams().getHashAlgorithm() + " for private key."); + } + } else if (!key.getParams().getHashAlgorithm().equals(digest.getAlgorithm())) + throw new InvalidKeyException("Key hash algorithm does not match chosen digest"); + digestInitSign(privKey); + } else { + throw new InvalidKeyException("cannot identify EdDSA private key: " + privateKey.getClass()); + } + } + + private void digestInitSign(EdDSAPrivateKey privKey) { + // Preparing for hash + // r = H(h_b,...,h_2b-1,M) + int b = privKey.getParams().getCurve().getField().getb(); + digest.update(privKey.getH(), b / 8, b / 4 - b / 8); + } + + @Override + protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException { + reset(); + if (publicKey instanceof EdDSAPublicKey) { + key = (EdDSAPublicKey) publicKey; + if (digest == null) { + try { + digest = MessageDigest.getInstance(key.getParams().getHashAlgorithm()); + } catch (NoSuchAlgorithmException e) { + throw new InvalidKeyException("cannot get required digest " + key.getParams().getHashAlgorithm() + " for private key."); + } + } else if (!key.getParams().getHashAlgorithm().equals(digest.getAlgorithm())) + throw new InvalidKeyException("Key hash algorithm does not match chosen digest"); + } else { + throw new InvalidKeyException("cannot identify EdDSA public key: " + publicKey.getClass()); + } + } + + /** + * @throws SignatureException if in one-shot mode + */ + @Override + protected void engineUpdate(byte b) throws SignatureException { + if (oneShotMode) + throw new SignatureException("unsupported in one-shot mode"); + if (baos == null) + baos = new ByteArrayOutputStream(256); + baos.write(b); + } + + /** + * @throws SignatureException if one-shot rules are violated + */ + @Override + protected void engineUpdate(byte[] b, int off, int len) + throws SignatureException { + if (oneShotMode) { + if (oneShotBytes != null) + throw new SignatureException("update() already called"); + oneShotBytes = b; + oneShotOffset = off; + oneShotLength = len; + } else { + if (baos == null) + baos = new ByteArrayOutputStream(256); + baos.write(b, off, len); + } + } + + @Override + protected byte[] engineSign() throws SignatureException { + try { + return x_engineSign(); + } finally { + reset(); + // must leave the object ready to sign again with + // the same key, as required by the API + EdDSAPrivateKey privKey = (EdDSAPrivateKey) key; + digestInitSign(privKey); + } + } + + private byte[] x_engineSign() throws SignatureException { + Curve curve = key.getParams().getCurve(); + ScalarOps sc = key.getParams().getScalarOps(); + byte[] a = ((EdDSAPrivateKey) key).geta(); + + byte[] message; + int offset, length; + if (oneShotMode) { + if (oneShotBytes == null) + throw new SignatureException("update() not called first"); + message = oneShotBytes; + offset = oneShotOffset; + length = oneShotLength; + } else { + if (baos == null) + message = new byte[0]; + else + message = baos.toByteArray(); + offset = 0; + length = message.length; + } + // r = H(h_b,...,h_2b-1,M) + digest.update(message, offset, length); + byte[] r = digest.digest(); + + // r mod l + // Reduces r from 64 bytes to 32 bytes + r = sc.reduce(r); + + // R = rB + GroupElement R = key.getParams().getB().scalarMultiply(r); + byte[] Rbyte = R.toByteArray(); + + // S = (r + H(Rbar,Abar,M)*a) mod l + digest.update(Rbyte); + digest.update(((EdDSAPrivateKey) key).getAbyte()); + digest.update(message, offset, length); + byte[] h = digest.digest(); + h = sc.reduce(h); + byte[] S = sc.multiplyAndAdd(h, a, r); + + // R+S + int b = curve.getField().getb(); + ByteBuffer out = ByteBuffer.allocate(b / 4); + out.put(Rbyte).put(S); + return out.array(); + } + + @Override + protected boolean engineVerify(byte[] sigBytes) throws SignatureException { + try { + return x_engineVerify(sigBytes); + } finally { + reset(); + } + } + + private boolean x_engineVerify(byte[] sigBytes) throws SignatureException { + Curve curve = key.getParams().getCurve(); + int b = curve.getField().getb(); + if (sigBytes.length != b / 4) + throw new SignatureException("signature length is wrong"); + + // R is first b/8 bytes of sigBytes, S is second b/8 bytes + digest.update(sigBytes, 0, b / 8); + digest.update(((EdDSAPublicKey) key).getAbyte()); + // h = H(Rbar,Abar,M) + byte[] message; + int offset, length; + if (oneShotMode) { + if (oneShotBytes == null) + throw new SignatureException("update() not called first"); + message = oneShotBytes; + offset = oneShotOffset; + length = oneShotLength; + } else { + if (baos == null) + message = new byte[0]; + else + message = baos.toByteArray(); + offset = 0; + length = message.length; + } + digest.update(message, offset, length); + byte[] h = digest.digest(); + + // h mod l + h = key.getParams().getScalarOps().reduce(h); + + byte[] Sbyte = Arrays.copyOfRange(sigBytes, b / 8, b / 4); + // R = SB - H(Rbar,Abar,M)A + GroupElement R = key.getParams().getB().doubleScalarMultiplyVariableTime( + ((EdDSAPublicKey) key).getNegativeA(), h, Sbyte); + + // Variable time. This should be okay, because there are no secret + // values used anywhere in verification. + byte[] Rcalc = R.toByteArray(); + for (int i = 0; i < Rcalc.length; i++) { + if (Rcalc[i] != sigBytes[i]) + return false; + } + return true; + } + + /** + * To efficiently sign all the data in one shot, if it is available, + * use this method, which will avoid copying the data. + *

+ * Same as: + *

+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data)
+     *  sig = sign()
+     * 
+ * + * @param data the message to be signed + * @return the signature + * @throws SignatureException if update() already called + * @see #ONE_SHOT_MODE + */ + public byte[] signOneShot(byte[] data) throws SignatureException { + return signOneShot(data, 0, data.length); + } + + /** + * To efficiently sign all the data in one shot, if it is available, + * use this method, which will avoid copying the data. + *

+ * Same as: + *

+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data, off, len)
+     *  sig = sign()
+     * 
+ * + * @param data byte array containing the message to be signed + * @param off the start of the message inside data + * @param len the length of the message + * @return the signature + * @throws SignatureException if update() already called + * @see #ONE_SHOT_MODE + */ + public byte[] signOneShot(byte[] data, int off, int len) throws SignatureException { + oneShotMode = true; + update(data, off, len); + return sign(); + } + + /** + * To efficiently verify all the data in one shot, if it is available, + * use this method, which will avoid copying the data. + *

+ * Same as: + *

+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data)
+     *  ok = verify(signature)
+     * 
+ * + * @param data the message that was signed + * @param signature of the message + * @return true if the signature is valid, false otherwise + * @throws SignatureException if update() already called + * @see #ONE_SHOT_MODE + */ + public boolean verifyOneShot(byte[] data, byte[] signature) throws SignatureException { + return verifyOneShot(data, 0, data.length, signature, 0, signature.length); + } + + /** + * To efficiently verify all the data in one shot, if it is available, + * use this method, which will avoid copying the data. + *

+ * Same as: + *

+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data, off, len)
+     *  ok = verify(signature)
+     * 
+ * + * @param data byte array containing the message that was signed + * @param off the start of the message inside data + * @param len the length of the message + * @param signature of the message + * @return true if the signature is valid, false otherwise + * @throws SignatureException if update() already called + * @see #ONE_SHOT_MODE + */ + public boolean verifyOneShot(byte[] data, int off, int len, byte[] signature) throws SignatureException { + return verifyOneShot(data, off, len, signature, 0, signature.length); + } + + /** + * To efficiently verify all the data in one shot, if it is available, + * use this method, which will avoid copying the data. + *

+ * Same as: + *

+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data)
+     *  ok = verify(signature, sigoff, siglen)
+     * 
+ * + * @param data the message that was signed + * @param signature byte array containing the signature + * @param sigoff the start of the signature + * @param siglen the length of the signature + * @return true if the signature is valid, false otherwise + * @throws SignatureException if update() already called + * @see #ONE_SHOT_MODE + */ + public boolean verifyOneShot(byte[] data, byte[] signature, int sigoff, int siglen) throws SignatureException { + return verifyOneShot(data, 0, data.length, signature, sigoff, siglen); + } + + /** + * To efficiently verify all the data in one shot, if it is available, + * use this method, which will avoid copying the data. + *

+ * Same as: + *

+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data, off, len)
+     *  ok = verify(signature, sigoff, siglen)
+     * 
+ * + * @param data byte array containing the message that was signed + * @param off the start of the message inside data + * @param len the length of the message + * @param signature byte array containing the signature + * @param sigoff the start of the signature + * @param siglen the length of the signature + * @return true if the signature is valid, false otherwise + * @throws SignatureException if update() already called + * @see #ONE_SHOT_MODE + */ + public boolean verifyOneShot(byte[] data, int off, int len, byte[] signature, int sigoff, int siglen) throws SignatureException { + oneShotMode = true; + update(data, off, len); + return verify(signature, sigoff, siglen); + } + + /** + * @throws InvalidAlgorithmParameterException if spec is ONE_SHOT_MODE and update() already called + * @see #ONE_SHOT_MODE + */ + @Override + protected void engineSetParameter(AlgorithmParameterSpec spec) throws InvalidAlgorithmParameterException { + if (spec.equals(ONE_SHOT_MODE)) { + if (oneShotBytes != null || (baos != null && baos.size() > 0)) + throw new InvalidAlgorithmParameterException("update() already called"); + oneShotMode = true; + } else { + super.engineSetParameter(spec); + } + } + + @Override + @SuppressWarnings("deprecation") + protected void engineSetParameter(String param, Object value) { + throw new UnsupportedOperationException("engineSetParameter unsupported"); + } + + @Override + @SuppressWarnings("deprecation") + protected Object engineGetParameter(String param) { + throw new UnsupportedOperationException("engineSetParameter unsupported"); + } + + private static class OneShotSpec implements AlgorithmParameterSpec { + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAKey.java b/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAKey.java new file mode 100644 index 0000000..7e7c582 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAKey.java @@ -0,0 +1,19 @@ +package org.xbib.net.security.eddsa; + +import org.xbib.net.security.eddsa.spec.EdDSAParameterSpec; + +/** + * Common interface for all EdDSA keys. + */ +public interface EdDSAKey { + /** + * The reported key algorithm for all EdDSA keys + */ + String KEY_ALGORITHM = "EdDSA"; + + /** + * @return a parameter specification representing the EdDSA domain + * parameters for the key. + */ + EdDSAParameterSpec getParams(); +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAPrivateKey.java b/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAPrivateKey.java new file mode 100644 index 0000000..8329844 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAPrivateKey.java @@ -0,0 +1,319 @@ +package org.xbib.net.security.eddsa; + +import org.xbib.net.security.eddsa.math.GroupElement; +import org.xbib.net.security.eddsa.spec.EdDSANamedCurveTable; +import org.xbib.net.security.eddsa.spec.EdDSAParameterSpec; +import org.xbib.net.security.eddsa.spec.EdDSAPrivateKeySpec; + +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; + +/** + * An EdDSA private key. + * Warning: Private key encoding is based on the current curdle WG draft, + * and is subject to change. See getEncoded(). + *

+ * For compatibility with older releases, decoding supports both the old and new + * draft specifications. See decode(). + *

+ * Ref: IETF draft + *

+ * Old Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04 + *

+ */ +@SuppressWarnings("serial") +public class EdDSAPrivateKey implements EdDSAKey, PrivateKey { + + // OID 1.3.101.xxx + private static final int OID_OLD = 100; + private static final int OID_ED25519 = 112; + private static final int OID_BYTE = 11; + private static final int IDLEN_BYTE = 6; + private final byte[] seed; + private final byte[] h; + private final byte[] a; + private final GroupElement A; + private final byte[] Abyte; + private final EdDSAParameterSpec edDsaSpec; + + public EdDSAPrivateKey(EdDSAPrivateKeySpec spec) { + this.seed = spec.getSeed(); + this.h = spec.getH(); + this.a = spec.geta(); + this.A = spec.getA(); + this.Abyte = this.A.toByteArray(); + this.edDsaSpec = spec.getParams(); + } + + public EdDSAPrivateKey(PKCS8EncodedKeySpec spec) throws InvalidKeySpecException { + this(new EdDSAPrivateKeySpec(decode(spec.getEncoded()), + EdDSANamedCurveTable.getByName("Ed25519"))); + } + + /** + * Extracts the private key bytes from the provided encoding. + *

+ * This will decode data conforming to the current spec at + * https://tools.ietf.org/html/draft-ietf-curdle-pkix-04 + * or as inferred from the old spec at + * https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04. + *

+ * Contrary to draft-ietf-curdle-pkix-04, it WILL accept a parameter value + * of NULL, as it is required for interoperability with the default Java + * keystore. Other implementations MUST NOT copy this behaviour from here + * unless they also need to read keys from the default Java keystore. + *

+ * This is really dumb for now. It does not use a general-purpose ASN.1 decoder. + * See also getEncoded(). + * + * @return 32 bytes for Ed25519, throws for other curves + */ + private static byte[] decode(byte[] d) throws InvalidKeySpecException { + try { + // + // Setup and OID check + // + int totlen = 48; + int idlen = 5; + int doid = d[OID_BYTE]; + if (doid == OID_OLD) { + totlen = 49; + idlen = 8; + } else if (doid == OID_ED25519) { + // Detect parameter value of NULL + if (d[IDLEN_BYTE] == 7) { + totlen = 50; + idlen = 7; + } + } else { + throw new InvalidKeySpecException("unsupported key spec"); + } + + // + // Pre-decoding check + // + if (d.length != totlen) { + throw new InvalidKeySpecException("invalid key spec length"); + } + + // + // Decoding + // + int idx = 0; + if (d[idx++] != 0x30 || + d[idx++] != (totlen - 2) || + d[idx++] != 0x02 || + d[idx++] != 1 || + d[idx++] != 0 || + d[idx++] != 0x30 || + d[idx++] != idlen || + d[idx++] != 0x06 || + d[idx++] != 3 || + d[idx++] != (1 * 40) + 3 || + d[idx++] != 101) { + throw new InvalidKeySpecException("unsupported key spec"); + } + idx++; // OID, checked above + // parameters only with old OID + if (doid == OID_OLD) { + if (d[idx++] != 0x0a || + d[idx++] != 1 || + d[idx++] != 1) { + throw new InvalidKeySpecException("unsupported key spec"); + } + } else { + // Handle parameter value of NULL + // + // Quote https://tools.ietf.org/html/draft-ietf-curdle-pkix-04 : + // For all of the OIDs, the parameters MUST be absent. + // Regardless of the defect in the original 1997 syntax, + // implementations MUST NOT accept a parameters value of NULL. + // + // But Java's default keystore puts it in (when decoding as + // PKCS8 and then re-encoding to pass on), so we must accept it. + if (idlen == 7) { + if (d[idx++] != 0x05 || + d[idx++] != 0) { + throw new InvalidKeySpecException("unsupported key spec"); + } + } + // PrivateKey wrapping the CurvePrivateKey + if (d[idx++] != 0x04 || + d[idx++] != 34) { + throw new InvalidKeySpecException("unsupported key spec"); + } + } + if (d[idx++] != 0x04 || + d[idx++] != 32) { + throw new InvalidKeySpecException("unsupported key spec"); + } + byte[] rv = new byte[32]; + System.arraycopy(d, idx, rv, 0, 32); + return rv; + } catch (IndexOutOfBoundsException ioobe) { + throw new InvalidKeySpecException(ioobe); + } + } + + @Override + public String getAlgorithm() { + return KEY_ALGORITHM; + } + + @Override + public String getFormat() { + return "PKCS#8"; + } + + /** + * Returns the public key in its canonical encoding. + * This implements the following specs: + *

    + *
  • General encoding: https://tools.ietf.org/html/draft-ietf-curdle-pkix-04
  • + *
  • Key encoding: https://tools.ietf.org/html/rfc8032
  • + *
+ *

+ * This encodes the seed. It will return null if constructed from + * a spec which was directly constructed from H, in which case seed is null. + *

+ * For keys in older formats, decoding and then re-encoding is sufficient to + * migrate them to the canonical encoding. + *

+ * Relevant spec quotes: + *
+     *  OneAsymmetricKey ::= SEQUENCE {
+     *    version Version,
+     *    privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
+     *    privateKey PrivateKey,
+     *    attributes [0] Attributes OPTIONAL,
+     *    ...,
+     *    [[2: publicKey [1] PublicKey OPTIONAL ]],
+     *    ...
+     *  }
+     *
+     *  Version ::= INTEGER
+     *  PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
+     *  PrivateKey ::= OCTET STRING
+     *  PublicKey ::= OCTET STRING
+     *  Attributes ::= SET OF Attribute
+     * 
+ *
+     *  ... when encoding a OneAsymmetricKey object, the private key is wrapped
+     *  in a CurvePrivateKey object and wrapped by the OCTET STRING of the
+     *  'privateKey' field.
+     *
+     *  CurvePrivateKey ::= OCTET STRING
+     * 
+ *
+     *  AlgorithmIdentifier  ::=  SEQUENCE  {
+     *    algorithm   OBJECT IDENTIFIER,
+     *    parameters  ANY DEFINED BY algorithm OPTIONAL
+     *  }
+     *
+     *  For all of the OIDs, the parameters MUST be absent.
+     * 
+ *
+     *  id-Ed25519   OBJECT IDENTIFIER ::= { 1 3 101 112 }
+     * 
+ * + * @return 48 bytes for Ed25519, null for other curves + */ + @Override + public byte[] getEncoded() { + if (!edDsaSpec.equals(EdDSANamedCurveTable.getByName("Ed25519"))) + return null; + if (seed == null) + return null; + int totlen = 16 + seed.length; + byte[] rv = new byte[totlen]; + int idx = 0; + // sequence + rv[idx++] = 0x30; + rv[idx++] = (byte) (totlen - 2); + // version + rv[idx++] = 0x02; + rv[idx++] = 1; + // v1 - no public key included + rv[idx++] = 0; + // Algorithm Identifier + // sequence + rv[idx++] = 0x30; + rv[idx++] = 5; + // OID + // https://msdn.microsoft.com/en-us/library/windows/desktop/bb540809%28v=vs.85%29.aspx + rv[idx++] = 0x06; + rv[idx++] = 3; + rv[idx++] = (1 * 40) + 3; + rv[idx++] = 101; + rv[idx++] = (byte) OID_ED25519; + // params - absent + // PrivateKey + rv[idx++] = 0x04; // octet string + rv[idx++] = (byte) (2 + seed.length); + // CurvePrivateKey + rv[idx++] = 0x04; // octet string + rv[idx++] = (byte) seed.length; + // the key + System.arraycopy(seed, 0, rv, idx, seed.length); + return rv; + } + + @Override + public EdDSAParameterSpec getParams() { + return edDsaSpec; + } + + /** + * @return will be null if constructed from a spec which was + * directly constructed from H + */ + public byte[] getSeed() { + return seed; + } + + /** + * @return the hash of the seed + */ + public byte[] getH() { + return h; + } + + /** + * @return the private key + */ + public byte[] geta() { + return a; + } + + /** + * @return the public key + */ + public GroupElement getA() { + return A; + } + + /** + * @return the public key + */ + public byte[] getAbyte() { + return Abyte; + } + + @Override + public int hashCode() { + return Arrays.hashCode(seed); + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof EdDSAPrivateKey)) + return false; + EdDSAPrivateKey pk = (EdDSAPrivateKey) o; + return Arrays.equals(seed, pk.getSeed()) && + edDsaSpec.equals(pk.getParams()); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAPublicKey.java b/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAPublicKey.java new file mode 100644 index 0000000..2d48a36 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSAPublicKey.java @@ -0,0 +1,258 @@ +package org.xbib.net.security.eddsa; + +import org.xbib.net.security.eddsa.math.GroupElement; +import org.xbib.net.security.eddsa.spec.EdDSANamedCurveTable; +import org.xbib.net.security.eddsa.spec.EdDSAParameterSpec; +import org.xbib.net.security.eddsa.spec.EdDSAPublicKeySpec; + +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; + +/** + * An EdDSA public key. + *

+ * Warning: Public key encoding is is based on the current curdle WG draft, + * and is subject to change. See getEncoded(). + *

+ * For compatibility with older releases, decoding supports both the old and new + * draft specifications. See decode(). + *

+ * Ref: IETF draft + *

+ * Old Ref: IETF old draft + *

+ */ +@SuppressWarnings("serial") +public class EdDSAPublicKey implements EdDSAKey, PublicKey { + + // OID 1.3.101.xxx + private static final int OID_OLD = 100; + private static final int OID_ED25519 = 112; + private static final int OID_BYTE = 8; + private static final int IDLEN_BYTE = 3; + private final GroupElement A; + private final GroupElement Aneg; + private final byte[] Abyte; + private final EdDSAParameterSpec edDsaSpec; + + public EdDSAPublicKey(EdDSAPublicKeySpec spec) { + this.A = spec.getA(); + this.Aneg = spec.getNegativeA(); + this.Abyte = this.A.toByteArray(); + this.edDsaSpec = spec.getParams(); + } + + public EdDSAPublicKey(X509EncodedKeySpec spec) throws InvalidKeySpecException { + this(new EdDSAPublicKeySpec(decode(spec.getEncoded()), + EdDSANamedCurveTable.getByName("Ed25519"))); + } + + /** + * Extracts the public key bytes from the provided encoding. + *

+ * This will decode data conforming to the current spec at + * https://tools.ietf.org/html/draft-ietf-curdle-pkix-04 + * or the old spec at + * https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04. + *

+ * Contrary to draft-ietf-curdle-pkix-04, it WILL accept a parameter value + * of NULL, as it is required for interoperability with the default Java + * keystore. Other implementations MUST NOT copy this behaviour from here + * unless they also need to read keys from the default Java keystore. + *

+ * This is really dumb for now. It does not use a general-purpose ASN.1 decoder. + * See also getEncoded(). + *

+ * + * @return 32 bytes for Ed25519, throws for other curves + */ + private static byte[] decode(byte[] d) throws InvalidKeySpecException { + try { + // + // Setup and OID check + // + int totlen = 44; + int idlen = 5; + int doid = d[OID_BYTE]; + if (doid == OID_OLD) { + totlen = 47; + idlen = 8; + } else if (doid == OID_ED25519) { + // Detect parameter value of NULL + if (d[IDLEN_BYTE] == 7) { + totlen = 46; + idlen = 7; + } + } else { + throw new InvalidKeySpecException("unsupported key spec"); + } + + // + // Pre-decoding check + // + if (d.length != totlen) { + throw new InvalidKeySpecException("invalid key spec length"); + } + + // + // Decoding + // + int idx = 0; + if (d[idx++] != 0x30 || + d[idx++] != (totlen - 2) || + d[idx++] != 0x30 || + d[idx++] != idlen || + d[idx++] != 0x06 || + d[idx++] != 3 || + d[idx++] != (1 * 40) + 3 || + d[idx++] != 101) { + throw new InvalidKeySpecException("unsupported key spec"); + } + idx++; // OID, checked above + // parameters only with old OID + if (doid == OID_OLD) { + if (d[idx++] != 0x0a || + d[idx++] != 1 || + d[idx++] != 1) { + throw new InvalidKeySpecException("unsupported key spec"); + } + } else { + // Handle parameter value of NULL + // + // Quote https://tools.ietf.org/html/draft-ietf-curdle-pkix-04 : + // For all of the OIDs, the parameters MUST be absent. + // Regardless of the defect in the original 1997 syntax, + // implementations MUST NOT accept a parameters value of NULL. + // + // But Java's default keystore puts it in (when decoding as + // PKCS8 and then re-encoding to pass on), so we must accept it. + if (idlen == 7) { + if (d[idx++] != 0x05 || + d[idx++] != 0) { + throw new InvalidKeySpecException("unsupported key spec"); + } + } + } + if (d[idx++] != 0x03 || + d[idx++] != 33 || + d[idx++] != 0) { + throw new InvalidKeySpecException("unsupported key spec"); + } + byte[] rv = new byte[32]; + System.arraycopy(d, idx, rv, 0, 32); + return rv; + } catch (IndexOutOfBoundsException ioobe) { + throw new InvalidKeySpecException(ioobe); + } + } + + @Override + public String getAlgorithm() { + return KEY_ALGORITHM; + } + + @Override + public String getFormat() { + return "X.509"; + } + + /** + * Returns the public key in its canonical encoding. + * This implements the following specs: + *
    + *
  • General encoding: https://tools.ietf.org/html/draft-ietf-curdle-pkix-04
  • + *
  • Key encoding: https://tools.ietf.org/html/rfc8032
  • + *
+ *

+ * For keys in older formats, decoding and then re-encoding is sufficient to + * migrate them to the canonical encoding. + *

+ * Relevant spec quotes: + *
+     *  In the X.509 certificate, the subjectPublicKeyInfo field has the
+     *  SubjectPublicKeyInfo type, which has the following ASN.1 syntax:
+     *
+     *  SubjectPublicKeyInfo  ::=  SEQUENCE  {
+     *    algorithm         AlgorithmIdentifier,
+     *    subjectPublicKey  BIT STRING
+     *  }
+     * 
+ *
+     *  AlgorithmIdentifier  ::=  SEQUENCE  {
+     *    algorithm   OBJECT IDENTIFIER,
+     *    parameters  ANY DEFINED BY algorithm OPTIONAL
+     *  }
+     *
+     *  For all of the OIDs, the parameters MUST be absent.
+     * 
+ *
+     *  id-Ed25519   OBJECT IDENTIFIER ::= { 1 3 101 112 }
+     * 
+ * + * @return 44 bytes for Ed25519, null for other curves + */ + @Override + public byte[] getEncoded() { + if (!edDsaSpec.equals(EdDSANamedCurveTable.getByName("Ed25519"))) + return null; + int totlen = 12 + Abyte.length; + byte[] rv = new byte[totlen]; + int idx = 0; + // sequence + rv[idx++] = 0x30; + rv[idx++] = (byte) (totlen - 2); + // Algorithm Identifier + // sequence + rv[idx++] = 0x30; + rv[idx++] = 5; + // OID + // https://msdn.microsoft.com/en-us/library/windows/desktop/bb540809%28v=vs.85%29.aspx + rv[idx++] = 0x06; + rv[idx++] = 3; + rv[idx++] = (1 * 40) + 3; + rv[idx++] = 101; + rv[idx++] = (byte) OID_ED25519; + // params - absent + // the key + rv[idx++] = 0x03; // bit string + rv[idx++] = (byte) (1 + Abyte.length); + rv[idx++] = 0; // number of trailing unused bits + System.arraycopy(Abyte, 0, rv, idx, Abyte.length); + return rv; + } + + @Override + public EdDSAParameterSpec getParams() { + return edDsaSpec; + } + + public GroupElement getA() { + return A; + } + + public GroupElement getNegativeA() { + return Aneg; + } + + public byte[] getAbyte() { + return Abyte; + } + + @Override + public int hashCode() { + return Arrays.hashCode(Abyte); + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof EdDSAPublicKey)) + return false; + EdDSAPublicKey pk = (EdDSAPublicKey) o; + return Arrays.equals(Abyte, pk.getAbyte()) && + edDsaSpec.equals(pk.getParams()); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSASecurityProvider.java b/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSASecurityProvider.java new file mode 100644 index 0000000..eaa65f3 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/EdDSASecurityProvider.java @@ -0,0 +1,38 @@ +package org.xbib.net.security.eddsa; + +import java.security.Provider; +import java.security.Security; + +/** + * A security {@link Provider} that can be registered via {@link Security#addProvider(Provider)}. + */ +@SuppressWarnings("serial") +public class EdDSASecurityProvider extends Provider { + + public static final String PROVIDER_NAME = "EdDSA"; + + public EdDSASecurityProvider() { + super(PROVIDER_NAME, "0.1", "xbib " + PROVIDER_NAME + " security provider wrapper"); + setup(); + } + + protected void setup() { + // See https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/HowToImplAProvider.html + put("KeyFactory." + EdDSAKey.KEY_ALGORITHM, "org.xbib.io.sshd.eddsa.KeyFactory"); + put("KeyPairGenerator." + EdDSAKey.KEY_ALGORITHM, "org.xbib.io.sshd.eddsa.KeyPairGenerator"); + put("Signature." + EdDSAEngine.SIGNATURE_ALGORITHM, "org.xbib.io.sshd.eddsa.EdDSAEngine"); + + // OID Mappings + // See section "Mapping from OID to name". + // The Key* -> OID mappings correspond to the default algorithm in KeyPairGenerator. + // + // From draft-ieft-curdle-pkix-04: + // id-Ed25519 OBJECT IDENTIFIER ::= { 1 3 101 112 } + put("Alg.Alias.KeyFactory.1.3.101.112", EdDSAKey.KEY_ALGORITHM); + put("Alg.Alias.KeyFactory.OID.1.3.101.112", EdDSAKey.KEY_ALGORITHM); + put("Alg.Alias.KeyPairGenerator.1.3.101.112", EdDSAKey.KEY_ALGORITHM); + put("Alg.Alias.KeyPairGenerator.OID.1.3.101.112", EdDSAKey.KEY_ALGORITHM); + put("Alg.Alias.Signature.1.3.101.112", EdDSAEngine.SIGNATURE_ALGORITHM); + put("Alg.Alias.Signature.OID.1.3.101.112", EdDSAEngine.SIGNATURE_ALGORITHM); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/KeyFactory.java b/net-security/src/main/java/org/xbib/net/security/eddsa/KeyFactory.java new file mode 100644 index 0000000..a66489c --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/KeyFactory.java @@ -0,0 +1,63 @@ +package org.xbib.net.security.eddsa; + +import org.xbib.net.security.eddsa.spec.EdDSAPrivateKeySpec; +import org.xbib.net.security.eddsa.spec.EdDSAPublicKeySpec; + +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactorySpi; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +public final class KeyFactory extends KeyFactorySpi { + + public KeyFactory() { + } + + protected PrivateKey engineGeneratePrivate(KeySpec keySpec) + throws InvalidKeySpecException { + if (keySpec instanceof EdDSAPrivateKeySpec) { + return new EdDSAPrivateKey((EdDSAPrivateKeySpec) keySpec); + } + if (keySpec instanceof PKCS8EncodedKeySpec) { + return new EdDSAPrivateKey((PKCS8EncodedKeySpec) keySpec); + } + throw new InvalidKeySpecException("key spec not recognised: " + keySpec.getClass()); + } + + protected PublicKey engineGeneratePublic(KeySpec keySpec) + throws InvalidKeySpecException { + if (keySpec instanceof EdDSAPublicKeySpec) { + return new EdDSAPublicKey((EdDSAPublicKeySpec) keySpec); + } + if (keySpec instanceof X509EncodedKeySpec) { + return new EdDSAPublicKey((X509EncodedKeySpec) keySpec); + } + throw new InvalidKeySpecException("key spec not recognised: " + keySpec.getClass()); + } + + @SuppressWarnings("unchecked") + protected T engineGetKeySpec(Key key, Class keySpec) + throws InvalidKeySpecException { + if (keySpec.isAssignableFrom(EdDSAPublicKeySpec.class) && key instanceof EdDSAPublicKey) { + EdDSAPublicKey k = (EdDSAPublicKey) key; + if (k.getParams() != null) { + return (T) new EdDSAPublicKeySpec(k.getA(), k.getParams()); + } + } else if (keySpec.isAssignableFrom(EdDSAPrivateKeySpec.class) && key instanceof EdDSAPrivateKey) { + EdDSAPrivateKey k = (EdDSAPrivateKey) key; + if (k.getParams() != null) { + return (T) new EdDSAPrivateKeySpec(k.getSeed(), k.getH(), k.geta(), k.getA(), k.getParams()); + } + } + throw new InvalidKeySpecException("not implemented yet " + key + " " + keySpec); + } + + protected Key engineTranslateKey(Key key) throws InvalidKeyException { + throw new InvalidKeyException("No other EdDSA key providers known"); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/KeyPairGenerator.java b/net-security/src/main/java/org/xbib/net/security/eddsa/KeyPairGenerator.java new file mode 100644 index 0000000..fc14793 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/KeyPairGenerator.java @@ -0,0 +1,93 @@ +package org.xbib.net.security.eddsa; + +import org.xbib.net.security.eddsa.spec.EdDSAGenParameterSpec; +import org.xbib.net.security.eddsa.spec.EdDSANamedCurveSpec; +import org.xbib.net.security.eddsa.spec.EdDSANamedCurveTable; +import org.xbib.net.security.eddsa.spec.EdDSAParameterSpec; +import org.xbib.net.security.eddsa.spec.EdDSAPrivateKeySpec; +import org.xbib.net.security.eddsa.spec.EdDSAPublicKeySpec; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidParameterException; +import java.security.KeyPair; +import java.security.KeyPairGeneratorSpi; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Hashtable; + +/** + * Default keysize is 256 (Ed25519). + */ +public final class KeyPairGenerator extends KeyPairGeneratorSpi { + + private static final int DEFAULT_KEYSIZE = 256; + + private static final Hashtable edParameters; + + static { + edParameters = new Hashtable<>(); + edParameters.put(256, new EdDSAGenParameterSpec("Ed25519")); + } + + private EdDSAParameterSpec edParams; + + private SecureRandom random; + + private boolean initialized; + + public KeyPairGenerator() { + } + + public void initialize(int keysize, SecureRandom random) { + AlgorithmParameterSpec edParams = edParameters.get(keysize); + if (edParams == null) + throw new InvalidParameterException("unknown key type."); + try { + initialize(edParams, random); + } catch (InvalidAlgorithmParameterException e) { + throw new InvalidParameterException("key type not configurable."); + } + } + + @Override + public void initialize(AlgorithmParameterSpec params, SecureRandom random) throws InvalidAlgorithmParameterException { + if (params instanceof EdDSAParameterSpec) { + edParams = (EdDSAParameterSpec) params; + } else if (params instanceof EdDSAGenParameterSpec) { + edParams = createNamedCurveSpec(((EdDSAGenParameterSpec) params).getName()); + } else + throw new InvalidAlgorithmParameterException("parameter object not a EdDSAParameterSpec"); + + this.random = random; + initialized = true; + } + + public KeyPair generateKeyPair() { + if (!initialized) + initialize(DEFAULT_KEYSIZE, new SecureRandom()); + + byte[] seed = new byte[edParams.getCurve().getField().getb() / 8]; + random.nextBytes(seed); + + EdDSAPrivateKeySpec privKey = new EdDSAPrivateKeySpec(seed, edParams); + EdDSAPublicKeySpec pubKey = new EdDSAPublicKeySpec(privKey.getA(), edParams); + + return new KeyPair(new EdDSAPublicKey(pubKey), new EdDSAPrivateKey(privKey)); + } + + /** + * Create an EdDSANamedCurveSpec from the provided curve name. The current + * implementation fetches the pre-created curve spec from a table. + * + * @param curveName the EdDSA named curve. + * @return the specification for the named curve. + * @throws InvalidAlgorithmParameterException if the named curve is unknown. + */ + protected EdDSANamedCurveSpec createNamedCurveSpec(String curveName) throws InvalidAlgorithmParameterException { + EdDSANamedCurveSpec spec = EdDSANamedCurveTable.getByName(curveName); + if (spec == null) { + throw new InvalidAlgorithmParameterException("unknown curve name: " + curveName); + } + return spec; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/Utils.java b/net-security/src/main/java/org/xbib/net/security/eddsa/Utils.java new file mode 100644 index 0000000..2b7da47 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/Utils.java @@ -0,0 +1,99 @@ +package org.xbib.net.security.eddsa; + +/** + * Basic utilities for EdDSA. + * Not for external use, not maintained as a public API. + */ +public class Utils { + + private Utils() { + } + + /** + * Constant-time byte comparison. + * + * @param b a byte + * @param c a byte + * @return 1 if b and c are equal, 0 otherwise. + */ + public static int equal(int b, int c) { + int result = 0; + int xor = b ^ c; + for (int i = 0; i < 8; i++) { + result |= xor >> i; + } + return (result ^ 0x01) & 0x01; + } + + /** + * Constant-time byte[] comparison. + * + * @param b a byte[] + * @param c a byte[] + * @return 1 if b and c are equal, 0 otherwise. + */ + public static int equal(byte[] b, byte[] c) { + int result = 0; + for (int i = 0; i < 32; i++) { + result |= b[i] ^ c[i]; + } + + return equal(result, 0); + } + + /** + * Constant-time determine if byte is negative. + * + * @param b the byte to check. + * @return 1 if the byte is negative, 0 otherwise. + */ + public static int negative(int b) { + return (b >> 8) & 1; + } + + /** + * Get the i'th bit of a byte array. + * + * @param h the byte array. + * @param i the bit index. + * @return 0 or 1, the value of the i'th bit in h + */ + public static int bit(byte[] h, int i) { + return (h[i >> 3] >> (i & 7)) & 1; + } + + /** + * Converts a hex string to bytes. + * + * @param s the hex string to be converted. + * @return the byte[] + */ + public static byte[] hexToBytes(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } + + /** + * Converts bytes to a hex string. + * + * @param raw the byte[] to be converted. + * @return the hex representation as a string. + */ + public static String bytesToHex(byte[] raw) { + if (raw == null) { + return null; + } + final StringBuilder hex = new StringBuilder(2 * raw.length); + for (final byte b : raw) { + hex.append(Character.forDigit((b & 0xF0) >> 4, 16)) + .append(Character.forDigit((b & 0x0F), 16)); + } + return hex.toString(); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/Constants.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/Constants.java new file mode 100644 index 0000000..1186d89 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/Constants.java @@ -0,0 +1,15 @@ +package org.xbib.net.security.eddsa.math; + +import org.xbib.net.security.eddsa.Utils; + +/** + * + */ +final class Constants { + public static final byte[] ZERO = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000"); + public static final byte[] ONE = Utils.hexToBytes("0100000000000000000000000000000000000000000000000000000000000000"); + public static final byte[] TWO = Utils.hexToBytes("0200000000000000000000000000000000000000000000000000000000000000"); + public static final byte[] FOUR = Utils.hexToBytes("0400000000000000000000000000000000000000000000000000000000000000"); + public static final byte[] FIVE = Utils.hexToBytes("0500000000000000000000000000000000000000000000000000000000000000"); + public static final byte[] EIGHT = Utils.hexToBytes("0800000000000000000000000000000000000000000000000000000000000000"); +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/Curve.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/Curve.java new file mode 100644 index 0000000..0721b55 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/Curve.java @@ -0,0 +1,84 @@ +package org.xbib.net.security.eddsa.math; + +/** + * A twisted Edwards curve. + * Points on the curve satisfy $-x^2 + y^2 = 1 + d x^2y^2$ + */ +public class Curve { + private final Field f; + private final FieldElement d; + private final FieldElement d2; + private final FieldElement I; + + private final GroupElement zeroP2; + private final GroupElement zeroP3; + private final GroupElement zeroPrecomp; + + public Curve(Field f, byte[] d, FieldElement I) { + this.f = f; + this.d = f.fromByteArray(d); + this.d2 = this.d.add(this.d); + this.I = I; + + FieldElement zero = f.ZERO; + FieldElement one = f.ONE; + zeroP2 = GroupElement.p2(this, zero, one, one); + zeroP3 = GroupElement.p3(this, zero, one, one, zero); + zeroPrecomp = GroupElement.precomp(this, one, one, zero); + } + + public Field getField() { + return f; + } + + public FieldElement getD() { + return d; + } + + public FieldElement get2D() { + return d2; + } + + public FieldElement getI() { + return I; + } + + public GroupElement getZero(GroupElement.Representation repr) { + switch (repr) { + case P2: + return zeroP2; + case P3: + return zeroP3; + case PRECOMP: + return zeroPrecomp; + default: + return null; + } + } + + public GroupElement createPoint(byte[] P, boolean precompute) { + GroupElement ge = new GroupElement(this, P); + if (precompute) + ge.precompute(true); + return ge; + } + + @Override + public int hashCode() { + return f.hashCode() ^ + d.hashCode() ^ + I.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof Curve)) + return false; + Curve c = (Curve) o; + return f.equals(c.getField()) && + d.equals(c.getD()) && + I.equals(c.getI()); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/Encoding.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/Encoding.java new file mode 100644 index 0000000..9a83fda --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/Encoding.java @@ -0,0 +1,47 @@ +package org.xbib.net.security.eddsa.math; + +/** + * Common interface for all $(b-1)$-bit encodings of elements of EdDSA finite fields. + */ +public abstract class Encoding { + + protected Field f; + + protected Encoding() { + } + + public synchronized void setField(Field f) { + if (this.f != null) + throw new IllegalStateException("already set"); + this.f = f; + } + + /** + * Encode a FieldElement in its $(b-1)$-bit encoding. + * + * @param x the FieldElement to encode + * @return the $(b-1)$-bit encoding of this FieldElement. + */ + public abstract byte[] encode(FieldElement x); + + /** + * Decode a FieldElement from its $(b-1)$-bit encoding. + * The highest bit is masked out. + * + * @param in the $(b-1)$-bit encoding of a FieldElement. + * @return the FieldElement represented by 'val'. + */ + public abstract FieldElement decode(byte[] in); + + /** + * From the Ed25519 paper:
+ * $x$ is negative if the $(b-1)$-bit encoding of $x$ is lexicographically larger + * than the $(b-1)$-bit encoding of -x. If $q$ is an odd prime and the encoding + * is the little-endian representation of $\{0, 1,\dots, q-1\}$ then the negative + * elements of $F_q$ are $\{1, 3, 5,\dots, q-2\}$. + * + * @param x the FieldElement to check + * @return true if negative + */ + public abstract boolean isNegative(FieldElement x); +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/Field.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/Field.java new file mode 100644 index 0000000..1ff3d47 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/Field.java @@ -0,0 +1,83 @@ +package org.xbib.net.security.eddsa.math; + +/** + * An EdDSA finite field. Includes several pre-computed values. + */ +public class Field { + + public final FieldElement ZERO; + public final FieldElement ONE; + public final FieldElement TWO; + public final FieldElement FOUR; + public final FieldElement FIVE; + public final FieldElement EIGHT; + + private final int b; + private final FieldElement q; + /** + * q-2 + */ + private final FieldElement qm2; + /** + * (q-5) / 8 + */ + private final FieldElement qm5d8; + private final Encoding enc; + + public Field(int b, byte[] q, Encoding enc) { + this.b = b; + this.enc = enc; + this.enc.setField(this); + + this.q = fromByteArray(q); + + // Set up constants + ZERO = fromByteArray(Constants.ZERO); + ONE = fromByteArray(Constants.ONE); + TWO = fromByteArray(Constants.TWO); + FOUR = fromByteArray(Constants.FOUR); + FIVE = fromByteArray(Constants.FIVE); + EIGHT = fromByteArray(Constants.EIGHT); + + // Precompute values + qm2 = this.q.subtract(TWO); + qm5d8 = this.q.subtract(FIVE).divide(EIGHT); + } + + public FieldElement fromByteArray(byte[] x) { + return enc.decode(x); + } + + public int getb() { + return b; + } + + public FieldElement getQ() { + return q; + } + + public FieldElement getQm2() { + return qm2; + } + + public FieldElement getQm5d8() { + return qm5d8; + } + + public Encoding getEncoding() { + return enc; + } + + @Override + public int hashCode() { + return q.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Field)) + return false; + Field f = (Field) obj; + return b == f.b && q.equals(f.q); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/FieldElement.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/FieldElement.java new file mode 100644 index 0000000..231205b --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/FieldElement.java @@ -0,0 +1,63 @@ +package org.xbib.net.security.eddsa.math; + +/** + * Note: concrete subclasses must implement hashCode() and equals() + */ +public abstract class FieldElement { + + protected final Field f; + + public FieldElement(Field f) { + if (null == f) { + throw new IllegalArgumentException("field cannot be null"); + } + this.f = f; + } + + /** + * Encode a FieldElement in its $(b-1)$-bit encoding. + * + * @return the $(b-1)$-bit encoding of this FieldElement. + */ + public byte[] toByteArray() { + return f.getEncoding().encode(this); + } + + public abstract boolean isNonZero(); + + public boolean isNegative() { + return f.getEncoding().isNegative(this); + } + + public abstract FieldElement add(FieldElement val); + + public FieldElement addOne() { + return add(f.ONE); + } + + public abstract FieldElement subtract(FieldElement val); + + public FieldElement subtractOne() { + return subtract(f.ONE); + } + + public abstract FieldElement negate(); + + public FieldElement divide(FieldElement val) { + return multiply(val.invert()); + } + + public abstract FieldElement multiply(FieldElement val); + + public abstract FieldElement square(); + + public abstract FieldElement squareAndDouble(); + + public abstract FieldElement invert(); + + public abstract FieldElement pow22523(); + + public abstract FieldElement cmov(FieldElement val, final int b); + + // Note: concrete subclasses must implement hashCode() and equals() +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/GroupElement.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/GroupElement.java new file mode 100644 index 0000000..de0623e --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/GroupElement.java @@ -0,0 +1,1025 @@ +package org.xbib.net.security.eddsa.math; + +import org.xbib.net.security.eddsa.Utils; + +import java.util.Arrays; + +/** + * A point $(x,y)$ on an EdDSA curve. + *

+ * Reviewed/commented by Bloody Rookie (nemproject@gmx.de) + *

+ * Literature:
+ * [1] Daniel J. Bernstein, Niels Duif, Tanja Lange, Peter Schwabe and Bo-Yin Yang : High-speed high-security signatures
+ * [2] Huseyin Hisil, Kenneth Koon-Ho Wong, Gary Carter, Ed Dawson: Twisted Edwards Curves Revisited
+ * [3] Daniel J. Bernsteina, Tanja Lange: A complete set of addition laws for incomplete Edwards curves
+ * [4] Daniel J. Bernstein, Peter Birkner, Marc Joye, Tanja Lange and Christiane Peters: Twisted Edwards Curves
+ * [5] Christiane Pascale Peters: Curves, Codes, and Cryptography (PhD thesis)
+ * [6] Daniel J. Bernstein, Peter Birkner, Tanja Lange and Christiane Peters: Optimizing double-base elliptic-curve single-scalar multiplication
+ */ +public class GroupElement { + + /** + * Variable is package private only so that tests run. + */ + final Curve curve; + /** + * Variable is package private only so that tests run. + */ + final Representation repr; + /** + * Variable is package private only so that tests run. + */ + final FieldElement X; + /** + * Variable is package private only so that tests run. + */ + final FieldElement Y; + /** + * Variable is package private only so that tests run. + */ + final FieldElement Z; + /** + * Variable is package private only so that tests run. + */ + final FieldElement T; + /** + * Precomputed table for {@link #scalarMultiply(byte[])}, + * filled if necessary. + *

+ * Variable is package private only so that tests run. + */ + GroupElement[][] precmp; + /** + * Precomputed table for {@link #doubleScalarMultiplyVariableTime(GroupElement, byte[], byte[])}, + * filled if necessary. + *

+ * Variable is package private only so that tests run. + */ + GroupElement[] dblPrecmp; + + /** + * Creates a group element for a curve. + * + * @param curve The curve. + * @param repr The representation used to represent the group element. + * @param X The $X$ coordinate. + * @param Y The $Y$ coordinate. + * @param Z The $Z$ coordinate. + * @param T The $T$ coordinate. + */ + public GroupElement( + final Curve curve, + final Representation repr, + final FieldElement X, + final FieldElement Y, + final FieldElement Z, + final FieldElement T) { + this.curve = curve; + this.repr = repr; + this.X = X; + this.Y = Y; + this.Z = Z; + this.T = T; + } + + /** + * Creates a group element for a curve from a given encoded point. + *

+ * A point $(x,y)$ is encoded by storing $y$ in bit 0 to bit 254 and the sign of $x$ in bit 255. + * $x$ is recovered in the following way: + *

    + *
  • $x = sign(x) * \sqrt{(y^2 - 1) / (d * y^2 + 1)} = sign(x) * \sqrt{u / v}$ with $u = y^2 - 1$ and $v = d * y^2 + 1$. + *
  • Setting $β = (u * v^3) * (u * v^7)^{((q - 5) / 8)}$ one has $β^2 = \pm(u / v)$. + *
  • If $v * β = -u$ multiply $β$ with $i=\sqrt{-1}$. + *
  • Set $x := β$. + *
  • If $sign(x) \ne$ bit 255 of $s$ then negate $x$. + *
+ * + * @param curve The curve. + * @param s The encoded point. + */ + public GroupElement(final Curve curve, final byte[] s) { + FieldElement x, y, yy, u, v, v3, vxx, check; + y = curve.getField().fromByteArray(s); + yy = y.square(); + + // u = y^2-1 + u = yy.subtractOne(); + + // v = dy^2+1 + v = yy.multiply(curve.getD()).addOne(); + + // v3 = v^3 + v3 = v.square().multiply(v); + + // x = (v3^2)vu, aka x = uv^7 + x = v3.square().multiply(v).multiply(u); + + // x = (uv^7)^((q-5)/8) + x = x.pow22523(); + + // x = uv^3(uv^7)^((q-5)/8) + x = v3.multiply(u).multiply(x); + + vxx = x.square().multiply(v); + check = vxx.subtract(u); // vx^2-u + if (check.isNonZero()) { + check = vxx.add(u); // vx^2+u + + if (check.isNonZero()) + throw new IllegalArgumentException("not a valid GroupElement"); + x = x.multiply(curve.getI()); + } + + if ((x.isNegative() ? 1 : 0) != Utils.bit(s, curve.getField().getb() - 1)) { + x = x.negate(); + } + + this.curve = curve; + this.repr = Representation.P3; + this.X = x; + this.Y = y; + this.Z = curve.getField().ONE; + this.T = this.X.multiply(this.Y); + } + + /** + * Creates a new group element in P2 representation. + * + * @param curve The curve. + * @param X The $X$ coordinate. + * @param Y The $Y$ coordinate. + * @param Z The $Z$ coordinate. + * @return The group element in P2 representation. + */ + public static GroupElement p2( + final Curve curve, + final FieldElement X, + final FieldElement Y, + final FieldElement Z) { + return new GroupElement(curve, Representation.P2, X, Y, Z, null); + } + + /** + * Creates a new group element in P3 representation. + * + * @param curve The curve. + * @param X The $X$ coordinate. + * @param Y The $Y$ coordinate. + * @param Z The $Z$ coordinate. + * @param T The $T$ coordinate. + * @return The group element in P3 representation. + */ + public static GroupElement p3( + final Curve curve, + final FieldElement X, + final FieldElement Y, + final FieldElement Z, + final FieldElement T) { + return new GroupElement(curve, Representation.P3, X, Y, Z, T); + } + + /** + * Creates a new group element in P1P1 representation. + * + * @param curve The curve. + * @param X The $X$ coordinate. + * @param Y The $Y$ coordinate. + * @param Z The $Z$ coordinate. + * @param T The $T$ coordinate. + * @return The group element in P1P1 representation. + */ + public static GroupElement p1p1( + final Curve curve, + final FieldElement X, + final FieldElement Y, + final FieldElement Z, + final FieldElement T) { + return new GroupElement(curve, Representation.P1P1, X, Y, Z, T); + } + + /** + * Creates a new group element in PRECOMP representation. + * + * @param curve The curve. + * @param ypx The $y + x$ value. + * @param ymx The $y - x$ value. + * @param xy2d The $2 * d * x * y$ value. + * @return The group element in PRECOMP representation. + */ + public static GroupElement precomp( + final Curve curve, + final FieldElement ypx, + final FieldElement ymx, + final FieldElement xy2d) { + return new GroupElement(curve, Representation.PRECOMP, ypx, ymx, xy2d, null); + } + + /** + * Creates a new group element in CACHED representation. + * + * @param curve The curve. + * @param YpX The $Y + X$ value. + * @param YmX The $Y - X$ value. + * @param Z The $Z$ coordinate. + * @param T2d The $2 * d * T$ value. + * @return The group element in CACHED representation. + */ + public static GroupElement cached( + final Curve curve, + final FieldElement YpX, + final FieldElement YmX, + final FieldElement Z, + final FieldElement T2d) { + return new GroupElement(curve, Representation.CACHED, YpX, YmX, Z, T2d); + } + + /** + * Convert a to radix 16. + *

+ * Method is package private only so that tests run. + * + * @param a $= a[0]+256*a[1]+...+256^{31} a[31]$ + * @return 64 bytes, each between -8 and 7 + */ + static byte[] toRadix16(final byte[] a) { + final byte[] e = new byte[64]; + int i; + // Radix 16 notation + for (i = 0; i < 32; i++) { + e[2 * i + 0] = (byte) (a[i] & 15); + e[2 * i + 1] = (byte) ((a[i] >> 4) & 15); + } + /* each e[i] is between 0 and 15 */ + /* e[63] is between 0 and 7 */ + int carry = 0; + for (i = 0; i < 63; i++) { + e[i] += carry; + carry = e[i] + 8; + carry >>= 4; + e[i] -= carry << 4; + } + e[63] += carry; + /* each e[i] is between -8 and 7 */ + return e; + } + + /** + * Calculates a sliding-windows base 2 representation for a given value $a$. + * To learn more about it see [6] page 8. + *

+ * Output: $r$ which satisfies + * $a = r0 * 2^0 + r1 * 2^1 + \dots + r255 * 2^{255}$ with $ri$ in $\{-15, -13, -11, -9, -7, -5, -3, -1, 0, 1, 3, 5, 7, 9, 11, 13, 15\}$ + *

+ * Method is package private only so that tests run. + * + * @param a $= a[0]+256*a[1]+\dots+256^{31} a[31]$. + * @return The byte array $r$ in the above described form. + */ + static byte[] slide(final byte[] a) { + byte[] r = new byte[256]; + + // Put each bit of 'a' into a separate byte, 0 or 1 + for (int i = 0; i < 256; ++i) { + r[i] = (byte) (1 & (a[i >> 3] >> (i & 7))); + } + + // Note: r[i] will always be odd. + for (int i = 0; i < 256; ++i) { + if (r[i] != 0) { + for (int b = 1; b <= 6 && i + b < 256; ++b) { + // Accumulate bits if possible + if (r[i + b] != 0) { + if (r[i] + (r[i + b] << b) <= 15) { + r[i] += r[i + b] << b; + r[i + b] = 0; + } else if (r[i] - (r[i + b] << b) >= -15) { + r[i] -= r[i + b] << b; + for (int k = i + b; k < 256; ++k) { + if (r[k] == 0) { + r[k] = 1; + break; + } + r[k] = 0; + } + } else + break; + } + } + } + } + + return r; + } + + /** + * Gets the curve of the group element. + * + * @return The curve. + */ + public Curve getCurve() { + return this.curve; + } + + /** + * Gets the representation of the group element. + * + * @return The representation. + */ + public Representation getRepresentation() { + return this.repr; + } + + /** + * Gets the $X$ value of the group element. + * This is for most representation the projective $X$ coordinate. + * + * @return The $X$ value. + */ + public FieldElement getX() { + return this.X; + } + + /** + * Gets the $Y$ value of the group element. + * This is for most representation the projective $Y$ coordinate. + * + * @return The $Y$ value. + */ + public FieldElement getY() { + return this.Y; + } + + /** + * Gets the $Z$ value of the group element. + * This is for most representation the projective $Z$ coordinate. + * + * @return The $Z$ value. + */ + public FieldElement getZ() { + return this.Z; + } + + /** + * Gets the $T$ value of the group element. + * This is for most representation the projective $T$ coordinate. + * + * @return The $T$ value. + */ + public FieldElement getT() { + return this.T; + } + + /** + * Converts the group element to an encoded point on the curve. + * + * @return The encoded point as byte array. + */ + public byte[] toByteArray() { + switch (this.repr) { + case P2: + case P3: + FieldElement recip = Z.invert(); + FieldElement x = X.multiply(recip); + FieldElement y = Y.multiply(recip); + byte[] s = y.toByteArray(); + s[s.length - 1] |= (x.isNegative() ? (byte) 0x80 : 0); + return s; + default: + return toP2().toByteArray(); + } + } + + /** + * Converts the group element to the P2 representation. + * + * @return The group element in the P2 representation. + */ + public GroupElement toP2() { + return toRep(Representation.P2); + } + + /** + * Converts the group element to the P3 representation. + * + * @return The group element in the P3 representation. + */ + public GroupElement toP3() { + return toRep(Representation.P3); + } + + /** + * Converts the group element to the CACHED representation. + * + * @return The group element in the CACHED representation. + */ + public GroupElement toCached() { + return toRep(Representation.CACHED); + } + + /** + * Convert a GroupElement from one Representation to another. + * TODO-CR: Add additional conversion? + * $r = p$ + *

+ * Supported conversions: + *

    + *
  • P3 $\rightarrow$ P2 + *
  • P3 $\rightarrow$ CACHED (1 multiply, 1 add, 1 subtract) + *
  • P1P1 $\rightarrow$ P2 (3 multiply) + *
  • P1P1 $\rightarrow$ P3 (4 multiply) + * + * @param repr The representation to convert to. + * @return A new group element in the given representation. + */ + private GroupElement toRep(final Representation repr) { + switch (this.repr) { + case P2: + switch (repr) { + case P2: + return p2(this.curve, this.X, this.Y, this.Z); + default: + throw new IllegalArgumentException(); + } + case P3: + switch (repr) { + case P2: + return p2(this.curve, this.X, this.Y, this.Z); + case P3: + return p3(this.curve, this.X, this.Y, this.Z, this.T); + case CACHED: + return cached(this.curve, this.Y.add(this.X), this.Y.subtract(this.X), this.Z, this.T.multiply(this.curve.get2D())); + default: + throw new IllegalArgumentException(); + } + case P1P1: + switch (repr) { + case P2: + return p2(this.curve, this.X.multiply(this.T), Y.multiply(this.Z), this.Z.multiply(this.T)); + case P3: + return p3(this.curve, this.X.multiply(this.T), Y.multiply(this.Z), this.Z.multiply(this.T), this.X.multiply(this.Y)); + case P1P1: + return p1p1(this.curve, this.X, this.Y, this.Z, this.T); + default: + throw new IllegalArgumentException(); + } + case PRECOMP: + switch (repr) { + case PRECOMP: + return precomp(this.curve, this.X, this.Y, this.Z); + default: + throw new IllegalArgumentException(); + } + case CACHED: + switch (repr) { + case CACHED: + return cached(this.curve, this.X, this.Y, this.Z, this.T); + default: + throw new IllegalArgumentException(); + } + default: + throw new UnsupportedOperationException(); + } + } + + /** + * Precomputes several tables. + *

    + * The precomputed tables are used for {@link #scalarMultiply(byte[])} + * and {@link #doubleScalarMultiplyVariableTime(GroupElement, byte[], byte[])}. + * + * @param precomputeSingle should the matrix for scalarMultiply() be precomputed? + */ + public synchronized void precompute(final boolean precomputeSingle) { + GroupElement Bi; + + if (precomputeSingle && this.precmp == null) { + // Precomputation for single scalar multiplication. + this.precmp = new GroupElement[32][8]; + // TODO-CR BR: check that this == base point when the method is called. + Bi = this; + for (int i = 0; i < 32; i++) { + GroupElement Bij = Bi; + for (int j = 0; j < 8; j++) { + final FieldElement recip = Bij.Z.invert(); + final FieldElement x = Bij.X.multiply(recip); + final FieldElement y = Bij.Y.multiply(recip); + this.precmp[i][j] = precomp(this.curve, y.add(x), y.subtract(x), x.multiply(y).multiply(this.curve.get2D())); + Bij = Bij.add(Bi.toCached()).toP3(); + } + // Only every second summand is precomputed (16^2 = 256) + for (int k = 0; k < 8; k++) { + Bi = Bi.add(Bi.toCached()).toP3(); + } + } + } + + // Precomputation for double scalar multiplication. + // P,3P,5P,7P,9P,11P,13P,15P + if (this.dblPrecmp != null) + return; + this.dblPrecmp = new GroupElement[8]; + Bi = this; + for (int i = 0; i < 8; i++) { + final FieldElement recip = Bi.Z.invert(); + final FieldElement x = Bi.X.multiply(recip); + final FieldElement y = Bi.Y.multiply(recip); + this.dblPrecmp[i] = precomp(this.curve, y.add(x), y.subtract(x), x.multiply(y).multiply(this.curve.get2D())); + // Bi = edwards(B,edwards(B,Bi)) + Bi = this.add(this.add(Bi.toCached()).toP3().toCached()).toP3(); + } + } + + /** + * Doubles a given group element $p$ in $P^2$ or $P^3$ representation and returns the result in $P \times P$ representation. + * $r = 2 * p$ where $p = (X : Y : Z)$ or $p = (X : Y : Z : T)$ + *

    + * $r$ in $P \times P$ representation: + *

    + * $r = ((X' : Z'), (Y' : T'))$ where + *

      + *
    • $X' = (X + Y)^2 - (Y^2 + X^2)$ + *
    • $Y' = Y^2 + X^2$ + *
    • $Z' = y^2 - X^2$ + *
    • $T' = 2 * Z^2 - (y^2 - X^2)$ + *

    + * $r$ converted from $P \times P$ to $P^2$ representation: + *

    + * $r = (X'' : Y'' : Z'')$ where + *

      + *
    • $X'' = X' * Z' = ((X + Y)^2 - Y^2 - X^2) * (2 * Z^2 - (y^2 - X^2))$ + *
    • $Y'' = Y' * T' = (Y^2 + X^2) * (2 * Z^2 - (y^2 - X^2))$ + *
    • $Z'' = Z' * T' = (y^2 - X^2) * (2 * Z^2 - (y^2 - X^2))$ + *

    + * Formula for the $P^2$ representation is in agreement with the formula given in [4] page 12 (with $a = -1$) + * up to a common factor -1 which does not matter: + *

    + * $$ + * B = (X + Y)^2; C = X^2; D = Y^2; E = -C = -X^2; F := E + D = Y^2 - X^2; H = Z^2; J = F − 2 * H; \\ + * X3 = (B − C − D) · J = X' * (-T'); \\ + * Y3 = F · (E − D) = Z' * (-Y'); \\ + * Z3 = F · J = Z' * (-T'). + * $$ + * + * @return The P1P1 representation + */ + public GroupElement dbl() { + switch (this.repr) { + case P2: + case P3: // Ignore T for P3 representation + FieldElement XX, YY, B, A, AA, Yn, Zn; + XX = this.X.square(); + YY = this.Y.square(); + B = this.Z.squareAndDouble(); + A = this.X.add(this.Y); + AA = A.square(); + Yn = YY.add(XX); + Zn = YY.subtract(XX); + return p1p1(this.curve, AA.subtract(Yn), Yn, Zn, B.subtract(Zn)); + default: + throw new UnsupportedOperationException(); + } + } + + /** + * GroupElement addition using the twisted Edwards addition law with + * extended coordinates (Hisil2008). + *

    + * this must be in $P^3$ representation and $q$ in PRECOMP representation. + * $r = p + q$ where $p = this = (X1 : Y1 : Z1 : T1), q = (q.X, q.Y, q.Z) = (Y2/Z2 + X2/Z2, Y2/Z2 - X2/Z2, 2 * d * X2/Z2 * Y2/Z2)$ + *

    + * $r$ in $P \times P$ representation: + *

    + * $r = ((X' : Z'), (Y' : T'))$ where + *

      + *
    • $X' = (Y1 + X1) * q.X - (Y1 - X1) * q.Y = ((Y1 + X1) * (Y2 + X2) - (Y1 - X1) * (Y2 - X2)) * 1/Z2$ + *
    • $Y' = (Y1 + X1) * q.X + (Y1 - X1) * q.Y = ((Y1 + X1) * (Y2 + X2) + (Y1 - X1) * (Y2 - X2)) * 1/Z2$ + *
    • $Z' = 2 * Z1 + T1 * q.Z = 2 * Z1 + T1 * 2 * d * X2 * Y2 * 1/Z2^2 = (2 * Z1 * Z2 + 2 * d * T1 * T2) * 1/Z2$ + *
    • $T' = 2 * Z1 - T1 * q.Z = 2 * Z1 - T1 * 2 * d * X2 * Y2 * 1/Z2^2 = (2 * Z1 * Z2 - 2 * d * T1 * T2) * 1/Z2$ + *

    + * Setting $A = (Y1 - X1) * (Y2 - X2), B = (Y1 + X1) * (Y2 + X2), C = 2 * d * T1 * T2, D = 2 * Z1 * Z2$ we get + *

      + *
    • $X' = (B - A) * 1/Z2$ + *
    • $Y' = (B + A) * 1/Z2$ + *
    • $Z' = (D + C) * 1/Z2$ + *
    • $T' = (D - C) * 1/Z2$ + *

    + * $r$ converted from $P \times P$ to $P^2$ representation: + *

    + * $r = (X'' : Y'' : Z'' : T'')$ where + *

      + *
    • $X'' = X' * Z' = (B - A) * (D + C) * 1/Z2^2$ + *
    • $Y'' = Y' * T' = (B + A) * (D - C) * 1/Z2^2$ + *
    • $Z'' = Z' * T' = (D + C) * (D - C) * 1/Z2^2$ + *
    • $T'' = X' * Y' = (B - A) * (B + A) * 1/Z2^2$ + *

    + * TODO-CR BR: Formula for the $P^2$ representation is not in agreement with the formula given in [2] page 6
    + * TODO-CR BR: (the common factor $1/Z2^2$ does not matter):
    + * $$ + * E = B - A, F = D - C, G = D + C, H = B + A \\ + * X3 = E * F = (B - A) * (D - C); \\ + * Y3 = G * H = (D + C) * (B + A); \\ + * Z3 = F * G = (D - C) * (D + C); \\ + * T3 = E * H = (B - A) * (B + A); + * $$ + * + * @param q the PRECOMP representation of the GroupElement to add. + * @return the P1P1 representation of the result. + */ + private GroupElement madd(GroupElement q) { + if (this.repr != Representation.P3) + throw new UnsupportedOperationException(); + if (q.repr != Representation.PRECOMP) + throw new IllegalArgumentException(); + + FieldElement YpX, YmX, A, B, C, D; + YpX = this.Y.add(this.X); + YmX = this.Y.subtract(this.X); + A = YpX.multiply(q.X); // q->y+x + B = YmX.multiply(q.Y); // q->y-x + C = q.Z.multiply(this.T); // q->2dxy + D = this.Z.add(this.Z); + return p1p1(this.curve, A.subtract(B), A.add(B), D.add(C), D.subtract(C)); + } + + /** + * GroupElement subtraction using the twisted Edwards addition law with + * extended coordinates (Hisil2008). + *

    + * this must be in $P^3$ representation and $q$ in PRECOMP representation. + * $r = p - q$ where $p = this = (X1 : Y1 : Z1 : T1), q = (q.X, q.Y, q.Z) = (Y2/Z2 + X2/Z2, Y2/Z2 - X2/Z2, 2 * d * X2/Z2 * Y2/Z2)$ + *

    + * Negating $q$ means negating the value of $X2$ and $T2$ (the latter is irrelevant here). + * The formula is in accordance to {@link #madd the above addition}. + * + * @param q the PRECOMP representation of the GroupElement to subtract. + * @return the P1P1 representation of the result. + */ + private GroupElement msub(GroupElement q) { + if (this.repr != Representation.P3) + throw new UnsupportedOperationException(); + if (q.repr != Representation.PRECOMP) + throw new IllegalArgumentException(); + + FieldElement YpX, YmX, A, B, C, D; + YpX = this.Y.add(this.X); + YmX = this.Y.subtract(this.X); + A = YpX.multiply(q.Y); // q->y-x + B = YmX.multiply(q.X); // q->y+x + C = q.Z.multiply(this.T); // q->2dxy + D = this.Z.add(this.Z); + return p1p1(this.curve, A.subtract(B), A.add(B), D.subtract(C), D.add(C)); + } + + /** + * GroupElement addition using the twisted Edwards addition law with + * extended coordinates (Hisil2008). + *

    + * this must be in $P^3$ representation and $q$ in CACHED representation. + * $r = p + q$ where $p = this = (X1 : Y1 : Z1 : T1), q = (q.X, q.Y, q.Z, q.T) = (Y2 + X2, Y2 - X2, Z2, 2 * d * T2)$ + *

    + * $r$ in $P \times P$ representation: + *

      + *
    • $X' = (Y1 + X1) * (Y2 + X2) - (Y1 - X1) * (Y2 - X2)$ + *
    • $Y' = (Y1 + X1) * (Y2 + X2) + (Y1 - X1) * (Y2 - X2)$ + *
    • $Z' = 2 * Z1 * Z2 + 2 * d * T1 * T2$ + *
    • $T' = 2 * Z1 * T2 - 2 * d * T1 * T2$ + *

    + * Setting $A = (Y1 - X1) * (Y2 - X2), B = (Y1 + X1) * (Y2 + X2), C = 2 * d * T1 * T2, D = 2 * Z1 * Z2$ we get + *

      + *
    • $X' = (B - A)$ + *
    • $Y' = (B + A)$ + *
    • $Z' = (D + C)$ + *
    • $T' = (D - C)$ + *

    + * Same result as in {@link #madd} (up to a common factor which does not matter). + * + * @param q the CACHED representation of the GroupElement to add. + * @return the P1P1 representation of the result. + */ + public GroupElement add(GroupElement q) { + if (this.repr != Representation.P3) + throw new UnsupportedOperationException(); + if (q.repr != Representation.CACHED) + throw new IllegalArgumentException(); + + FieldElement YpX, YmX, A, B, C, ZZ, D; + YpX = this.Y.add(this.X); + YmX = this.Y.subtract(this.X); + A = YpX.multiply(q.X); // q->Y+X + B = YmX.multiply(q.Y); // q->Y-X + C = q.T.multiply(this.T); // q->2dT + ZZ = this.Z.multiply(q.Z); + D = ZZ.add(ZZ); + return p1p1(this.curve, A.subtract(B), A.add(B), D.add(C), D.subtract(C)); + } + + /** + * GroupElement subtraction using the twisted Edwards addition law with + * extended coordinates (Hisil2008). + *

    + * $r = p - q$ + *

    + * Negating $q$ means negating the value of the coordinate $X2$ and $T2$. + * The formula is in accordance to {@link #add the above addition}. + * + * @param q the PRECOMP representation of the GroupElement to subtract. + * @return the P1P1 representation of the result. + */ + public GroupElement sub(GroupElement q) { + if (this.repr != Representation.P3) + throw new UnsupportedOperationException(); + if (q.repr != Representation.CACHED) + throw new IllegalArgumentException(); + + FieldElement YpX, YmX, A, B, C, ZZ, D; + YpX = Y.add(X); + YmX = Y.subtract(X); + A = YpX.multiply(q.Y); // q->Y-X + B = YmX.multiply(q.X); // q->Y+X + C = q.T.multiply(T); // q->2dT + ZZ = Z.multiply(q.Z); + D = ZZ.add(ZZ); + return p1p1(curve, A.subtract(B), A.add(B), D.subtract(C), D.add(C)); + } + + /** + * Negates this group element by subtracting it from the neutral group element. + *

    + * TODO-CR BR: why not simply negate the coordinates $X$ and $T$? + * + * @return The negative of this group element. + */ + public GroupElement negate() { + if (this.repr != Representation.P3) + throw new UnsupportedOperationException(); + return this.curve.getZero(Representation.P3).sub(toCached()).toP3(); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.toByteArray()); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (!(obj instanceof GroupElement)) + return false; + GroupElement ge = (GroupElement) obj; + if (!this.repr.equals(ge.repr)) { + try { + ge = ge.toRep(this.repr); + } catch (RuntimeException e) { + return false; + } + } + switch (this.repr) { + case P2: + case P3: + // Try easy way first + if (this.Z.equals(ge.Z)) + return this.X.equals(ge.X) && this.Y.equals(ge.Y); + // X1/Z1 = X2/Z2 --> X1*Z2 = X2*Z1 + final FieldElement x1 = this.X.multiply(ge.Z); + final FieldElement y1 = this.Y.multiply(ge.Z); + final FieldElement x2 = ge.X.multiply(this.Z); + final FieldElement y2 = ge.Y.multiply(this.Z); + return x1.equals(x2) && y1.equals(y2); + case P1P1: + return toP2().equals(ge); + case PRECOMP: + // Compare directly, PRECOMP is derived directly from x and y + return this.X.equals(ge.X) && this.Y.equals(ge.Y) && this.Z.equals(ge.Z); + case CACHED: + // Try easy way first + if (this.Z.equals(ge.Z)) + return this.X.equals(ge.X) && this.Y.equals(ge.Y) && this.T.equals(ge.T); + // (Y+X)/Z = y+x etc. + final FieldElement x3 = this.X.multiply(ge.Z); + final FieldElement y3 = this.Y.multiply(ge.Z); + final FieldElement t3 = this.T.multiply(ge.Z); + final FieldElement x4 = ge.X.multiply(this.Z); + final FieldElement y4 = ge.Y.multiply(this.Z); + final FieldElement t4 = ge.T.multiply(this.Z); + return x3.equals(x4) && y3.equals(y4) && t3.equals(t4); + default: + return false; + } + } + + /** + * Constant-time conditional move. + *

    + * Replaces this with $u$ if $b == 1$.
    + * Replaces this with this if $b == 0$. + *

    + * Method is package private only so that tests run. + * + * @param u The group element to return if $b == 1$. + * @param b in $\{0, 1\}$ + * @return $u$ if $b == 1$; this if $b == 0$. Results undefined if $b$ is not in $\{0, 1\}$. + */ + GroupElement cmov(final GroupElement u, final int b) { + return precomp(curve, X.cmov(u.X, b), Y.cmov(u.Y, b), Z.cmov(u.Z, b)); + } + + /** + * Look up $16^i r_i B$ in the precomputed table. + *

    + * No secret array indices, no secret branching. + * Constant time. + *

    + * Must have previously precomputed. + *

    + * Method is package private only so that tests run. + * + * @param pos $= i/2$ for $i$ in $\{0, 2, 4,..., 62\}$ + * @param b $= r_i$ + * @return the GroupElement + */ + GroupElement select(final int pos, final int b) { + // Is r_i negative? + final int bnegative = Utils.negative(b); + // |r_i| + final int babs = b - (((-bnegative) & b) << 1); + + // 16^i |r_i| B + final GroupElement t = this.curve.getZero(Representation.PRECOMP) + .cmov(this.precmp[pos][0], Utils.equal(babs, 1)) + .cmov(this.precmp[pos][1], Utils.equal(babs, 2)) + .cmov(this.precmp[pos][2], Utils.equal(babs, 3)) + .cmov(this.precmp[pos][3], Utils.equal(babs, 4)) + .cmov(this.precmp[pos][4], Utils.equal(babs, 5)) + .cmov(this.precmp[pos][5], Utils.equal(babs, 6)) + .cmov(this.precmp[pos][6], Utils.equal(babs, 7)) + .cmov(this.precmp[pos][7], Utils.equal(babs, 8)); + // -16^i |r_i| B + final GroupElement tminus = precomp(curve, t.Y, t.X, t.Z.negate()); + // 16^i r_i B + return t.cmov(tminus, bnegative); + } + + /** + * $h = a * B$ where $a = a[0]+256*a[1]+\dots+256^{31} a[31]$ and + * $B$ is this point. If its lookup table has not been precomputed, it + * will be at the start of the method (and cached for later calls). + * Constant time. + *

    + * Preconditions: (TODO: Check this applies here) + * $a[31] \le 127$ + * + * @param a $= a[0]+256*a[1]+\dots+256^{31} a[31]$ + * @return the GroupElement + */ + public GroupElement scalarMultiply(final byte[] a) { + GroupElement t; + int i; + + final byte[] e = toRadix16(a); + + GroupElement h = this.curve.getZero(Representation.P3); + synchronized (this) { + // TODO: Get opinion from a crypto professional. + // This should in practice never be necessary, the only point that + // this should get called on is EdDSA's B. + //precompute(); + for (i = 1; i < 64; i += 2) { + t = select(i / 2, e[i]); + h = h.madd(t).toP3(); + } + + h = h.dbl().toP2().dbl().toP2().dbl().toP2().dbl().toP3(); + + for (i = 0; i < 64; i += 2) { + t = select(i / 2, e[i]); + h = h.madd(t).toP3(); + } + } + + return h; + } + + /** + * $r = a * A + b * B$ where $a = a[0]+256*a[1]+\dots+256^{31} a[31]$, + * $b = b[0]+256*b[1]+\dots+256^{31} b[31]$ and $B$ is this point. + *

    + * $A$ must have been previously precomputed. + * + * @param A in P3 representation. + * @param a $= a[0]+256*a[1]+\dots+256^{31} a[31]$ + * @param b $= b[0]+256*b[1]+\dots+256^{31} b[31]$ + * @return the GroupElement + */ + public GroupElement doubleScalarMultiplyVariableTime(final GroupElement A, final byte[] a, final byte[] b) { + // TODO-CR BR: A check that this is the base point is needed. + final byte[] aslide = slide(a); + final byte[] bslide = slide(b); + + GroupElement r = this.curve.getZero(Representation.P2); + + int i; + for (i = 255; i >= 0; --i) { + if (aslide[i] != 0 || bslide[i] != 0) break; + } + + synchronized (this) { + // TODO-CR BR strange comment below. + // TODO: Get opinion from a crypto professional. + // This should in practice never be necessary, the only point that + // this should get called on is EdDSA's B. + //precompute(); + for (; i >= 0; --i) { + GroupElement t = r.dbl(); + + if (aslide[i] > 0) { + t = t.toP3().madd(A.dblPrecmp[aslide[i] / 2]); + } else if (aslide[i] < 0) { + t = t.toP3().msub(A.dblPrecmp[(-aslide[i]) / 2]); + } + + if (bslide[i] > 0) { + t = t.toP3().madd(this.dblPrecmp[bslide[i] / 2]); + } else if (bslide[i] < 0) { + t = t.toP3().msub(this.dblPrecmp[(-bslide[i]) / 2]); + } + + r = t.toP2(); + } + } + + return r; + } + + /** + * Verify that a point is on its curve. + * + * @return true if the point lies on its curve. + */ + public boolean isOnCurve() { + return isOnCurve(curve); + } + + /** + * Verify that a point is on the curve. + * + * @param curve The curve to check. + * @return true if the point lies on the curve. + */ + public boolean isOnCurve(Curve curve) { + switch (repr) { + case P2: + case P3: + FieldElement recip = Z.invert(); + FieldElement x = X.multiply(recip); + FieldElement y = Y.multiply(recip); + FieldElement xx = x.square(); + FieldElement yy = y.square(); + FieldElement dxxyy = curve.getD().multiply(xx).multiply(yy); + return curve.getField().ONE.add(dxxyy).add(xx).equals(yy); + + default: + return toP2().isOnCurve(curve); + } + } + + @Override + public String toString() { + return "[GroupElement\nX=" + X + "\nY=" + Y + "\nZ=" + Z + "\nT=" + T + "\n]"; + } + + /** + * Available representations for a group element. + *

      + *
    • P2: Projective representation $(X:Y:Z)$ satisfying $x=X/Z, y=Y/Z$. + *
    • P3: Extended projective representation $(X:Y:Z:T)$ satisfying $x=X/Z, y=Y/Z, XY=ZT$. + *
    • P1P1: Completed representation $((X:Z), (Y:T))$ satisfying $x=X/Z, y=Y/T$. + *
    • PRECOMP: Precomputed representation $(y+x, y-x, 2dxy)$. + *
    • CACHED: Cached representation $(Y+X, Y-X, Z, 2dT)$ + *
    + */ + public enum Representation { + /** + * Projective ($P^2$): $(X:Y:Z)$ satisfying $x=X/Z, y=Y/Z$ + */ + P2, + /** + * Extended ($P^3$): $(X:Y:Z:T)$ satisfying $x=X/Z, y=Y/Z, XY=ZT$ + */ + P3, + /** + * Completed ($P \times P$): $((X:Z),(Y:T))$ satisfying $x=X/Z, y=Y/T$ + */ + P1P1, + /** + * Precomputed (Duif): $(y+x,y-x,2dxy)$ + */ + PRECOMP, + /** + * Cached: $(Y+X,Y-X,Z,2dT)$ + */ + CACHED + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/ScalarOps.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/ScalarOps.java new file mode 100644 index 0000000..61d8334 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/ScalarOps.java @@ -0,0 +1,27 @@ +package org.xbib.net.security.eddsa.math; + +/** + * + */ +public interface ScalarOps { + /** + * Reduce the given scalar mod $l$. + * From the Ed25519 paper: + * Here we interpret $2b$-bit strings in little-endian form as integers in + * $\{0, 1,..., 2^{(2b)}-1\}$. + * + * @param s the scalar to reduce + * @return $s \bmod l$ + */ + byte[] reduce(byte[] s); + + /** + * $r = (a * b + c) \bmod l$ + * + * @param a a scalar + * @param b a scalar + * @param c a scalar + * @return $(a*b + c) \bmod l$ + */ + byte[] multiplyAndAdd(byte[] a, byte[] b, byte[] c); +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/BigIntegerFieldElement.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/BigIntegerFieldElement.java new file mode 100644 index 0000000..579d0e1 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/BigIntegerFieldElement.java @@ -0,0 +1,116 @@ +package org.xbib.net.security.eddsa.math.bigint; + +import org.xbib.net.security.eddsa.math.Field; +import org.xbib.net.security.eddsa.math.FieldElement; + +import java.math.BigInteger; + +/** + * A particular element of the field \Z/(2^255-19). + */ +public class BigIntegerFieldElement extends FieldElement { + /** + * Variable is package private for encoding. + */ + final BigInteger bi; + + public BigIntegerFieldElement(Field f, BigInteger bi) { + super(f); + this.bi = bi; + } + + public boolean isNonZero() { + return !bi.equals(BigInteger.ZERO); + } + + public FieldElement add(FieldElement val) { + return new BigIntegerFieldElement(f, bi.add(((BigIntegerFieldElement) val).bi)).mod(f.getQ()); + } + + @Override + public FieldElement addOne() { + return new BigIntegerFieldElement(f, bi.add(BigInteger.ONE)).mod(f.getQ()); + } + + public FieldElement subtract(FieldElement val) { + return new BigIntegerFieldElement(f, bi.subtract(((BigIntegerFieldElement) val).bi)).mod(f.getQ()); + } + + @Override + public FieldElement subtractOne() { + return new BigIntegerFieldElement(f, bi.subtract(BigInteger.ONE)).mod(f.getQ()); + } + + public FieldElement negate() { + return f.getQ().subtract(this); + } + + @Override + public FieldElement divide(FieldElement val) { + return divide(((BigIntegerFieldElement) val).bi); + } + + public FieldElement divide(BigInteger val) { + return new BigIntegerFieldElement(f, bi.divide(val)).mod(f.getQ()); + } + + public FieldElement multiply(FieldElement val) { + return new BigIntegerFieldElement(f, bi.multiply(((BigIntegerFieldElement) val).bi)).mod(f.getQ()); + } + + public FieldElement square() { + return multiply(this); + } + + public FieldElement squareAndDouble() { + FieldElement sq = square(); + return sq.add(sq); + } + + public FieldElement invert() { + // Euler's theorem + //return modPow(f.getQm2(), f.getQ()); + return new BigIntegerFieldElement(f, bi.modInverse(((BigIntegerFieldElement) f.getQ()).bi)); + } + + public FieldElement mod(FieldElement m) { + return new BigIntegerFieldElement(f, bi.mod(((BigIntegerFieldElement) m).bi)); + } + + public FieldElement modPow(FieldElement e, FieldElement m) { + return new BigIntegerFieldElement(f, bi.modPow(((BigIntegerFieldElement) e).bi, ((BigIntegerFieldElement) m).bi)); + } + + public FieldElement pow(FieldElement e) { + return modPow(e, f.getQ()); + } + + public FieldElement pow22523() { + return pow(f.getQm5d8()); + } + + @Override + public FieldElement cmov(FieldElement val, int b) { + // Not constant-time, but it doesn't really matter because none of the underlying BigInteger operations + // are either, so there's not much point in trying hard here ... + return b == 0 ? this : val; + } + + @Override + public int hashCode() { + return bi.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BigIntegerFieldElement)) + return false; + BigIntegerFieldElement fe = (BigIntegerFieldElement) obj; + return bi.equals(fe.bi); + } + + @Override + public String toString() { + return "[BigIntegerFieldElement val=" + bi + "]"; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/BigIntegerLittleEndianEncoding.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/BigIntegerLittleEndianEncoding.java new file mode 100644 index 0000000..7bffef5 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/BigIntegerLittleEndianEncoding.java @@ -0,0 +1,97 @@ +package org.xbib.net.security.eddsa.math.bigint; + +import org.xbib.net.security.eddsa.math.Encoding; +import org.xbib.net.security.eddsa.math.Field; +import org.xbib.net.security.eddsa.math.FieldElement; + +import java.math.BigInteger; + +public class BigIntegerLittleEndianEncoding extends Encoding { + + /** + * Mask where only the first b-1 bits are set. + */ + private BigInteger mask; + + public BigIntegerLittleEndianEncoding() { + } + + @Override + public synchronized void setField(Field f) { + super.setField(f); + mask = BigInteger.ONE.shiftLeft(f.getb() - 1).subtract(BigInteger.ONE); + } + + public byte[] encode(FieldElement x) { + return encode(((BigIntegerFieldElement) x).bi.and(mask)); + } + + /** + * Convert $x$ to little endian. + * Constant time. + * + * @param x the BigInteger value to encode + * @return array of length $b/8$ + * @throws IllegalStateException if field not set + */ + public byte[] encode(BigInteger x) { + if (f == null) { + throw new IllegalStateException("field not set"); + } + byte[] in = x.toByteArray(); + byte[] out = new byte[f.getb() / 8]; + for (int i = 0; i < in.length; i++) { + out[i] = in[in.length - 1 - i]; + } + for (int i = in.length; i < out.length; i++) { + out[i] = 0; + } + return out; + } + + /** + * Decode a FieldElement from its $(b-1)$-bit encoding. + * The highest bit is masked out. + * + * @param in the $(b-1)$-bit encoding of a FieldElement. + * @return the FieldElement represented by 'val'. + * @throws IllegalStateException if field not set + * @throws IllegalArgumentException if encoding is invalid + */ + public FieldElement decode(byte[] in) { + if (f == null) { + throw new IllegalStateException("field not set"); + } + if (in.length != f.getb() / 8) { + throw new IllegalArgumentException("Not a valid encoding"); + } + return new BigIntegerFieldElement(f, toBigInteger(in).and(mask)); + } + + /** + * Convert in to big endian + * + * @param in the $(b-1)$-bit encoding of a FieldElement. + * @return the decoded value as a BigInteger + */ + public BigInteger toBigInteger(byte[] in) { + byte[] out = new byte[in.length]; + for (int i = 0; i < in.length; i++) { + out[i] = in[in.length - 1 - i]; + } + return new BigInteger(1, out); + } + + /** + * From the Ed25519 paper:
    + * $x$ is negative if the $(b-1)$-bit encoding of $x$ is lexicographically larger + * than the $(b-1)$-bit encoding of $-x$. If $q$ is an odd prime and the encoding + * is the little-endian representation of $\{0, 1,\dots, q-1\}$ then the negative + * elements of $F_q$ are $\{1, 3, 5,\dots, q-2\}$. + * + * @return true if negative + */ + public boolean isNegative(FieldElement x) { + return ((BigIntegerFieldElement) x).bi.testBit(0); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/BigIntegerScalarOps.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/BigIntegerScalarOps.java new file mode 100644 index 0000000..f34b560 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/BigIntegerScalarOps.java @@ -0,0 +1,26 @@ +package org.xbib.net.security.eddsa.math.bigint; + +import org.xbib.net.security.eddsa.math.Field; +import org.xbib.net.security.eddsa.math.ScalarOps; + +import java.math.BigInteger; + +public class BigIntegerScalarOps implements ScalarOps { + private final BigInteger l; + private final BigIntegerLittleEndianEncoding enc; + + public BigIntegerScalarOps(Field f, BigInteger l) { + this.l = l; + enc = new BigIntegerLittleEndianEncoding(); + enc.setField(f); + } + + public byte[] reduce(byte[] s) { + return enc.encode(enc.toBigInteger(s).mod(l)); + } + + public byte[] multiplyAndAdd(byte[] a, byte[] b, byte[] c) { + return enc.encode(enc.toBigInteger(a).multiply(enc.toBigInteger(b)).add(enc.toBigInteger(c)).mod(l)); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/package-info.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/package-info.java new file mode 100644 index 0000000..943fb55 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/bigint/package-info.java @@ -0,0 +1,4 @@ +/** + * Low-level, non-optimized implementation using BigIntegers for any curve. + */ +package org.xbib.net.security.eddsa.math.bigint; \ No newline at end of file diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/Ed25519FieldElement.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/Ed25519FieldElement.java new file mode 100644 index 0000000..c28778b --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/Ed25519FieldElement.java @@ -0,0 +1,1046 @@ +package org.xbib.net.security.eddsa.math.ed25519; + +import org.xbib.net.security.eddsa.Utils; +import org.xbib.net.security.eddsa.math.Field; +import org.xbib.net.security.eddsa.math.FieldElement; + +import java.util.Arrays; + +/** + * Class to represent a field element of the finite field $p = 2^{255} - 19$ elements. + * An element $t$, entries $t[0] \dots t[9]$, represents the integer + * $t[0]+2^{26} t[1]+2^{51} t[2]+2^{77} t[3]+2^{102} t[4]+\dots+2^{230} t[9]$. + * Bounds on each $t[i]$ vary depending on context. + */ +public class Ed25519FieldElement extends FieldElement { + private static final byte[] ZERO = new byte[32]; + /** + * Variable is package private for encoding. + */ + final int[] t; + + /** + * Creates a field element. + * + * @param f The underlying field, must be the finite field with $p = 2^{255} - 19$ elements + * @param t The $2^{25.5}$ bit representation of the field element. + */ + public Ed25519FieldElement(Field f, int[] t) { + super(f); + if (t.length != 10) + throw new IllegalArgumentException("Invalid radix-2^51 representation"); + this.t = t; + } + + /** + * Gets a value indicating whether or not the field element is non-zero. + * + * @return 1 if it is non-zero, 0 otherwise. + */ + public boolean isNonZero() { + final byte[] s = toByteArray(); + return Utils.equal(s, ZERO) == 0; + } + + /** + * $h = f + g$ + *

    + * TODO-CR BR: $h$ is allocated via new, probably not a good idea. Do we need the copying into temp variables if we do that? + *

    + * Preconditions: + *

      + *
    • $|f|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc. + *
    • $|g|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc. + *

    + * Postconditions: + *

      + *
    • $|h|$ bounded by $1.1*2^{26},1.1*2^{25},1.1*2^{26},1.1*2^{25},$ etc. + *
    + * + * @param val The field element to add. + * @return The field element this + val. + */ + public FieldElement add(FieldElement val) { + int[] g = ((Ed25519FieldElement) val).t; + int[] h = new int[10]; + for (int i = 0; i < 10; i++) { + h[i] = t[i] + g[i]; + } + return new Ed25519FieldElement(f, h); + } + + /** + * $h = f - g$ + *

    + * Can overlap $h$ with $f$ or $g$. + *

    + * TODO-CR BR: See above. + *

    + * Preconditions: + *

      + *
    • $|f|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc. + *
    • $|g|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc. + *

    + * Postconditions: + *

      + *
    • $|h|$ bounded by $1.1*2^{26},1.1*2^{25},1.1*2^{26},1.1*2^{25},$ etc. + *
    + * + * @param val The field element to subtract. + * @return The field element this - val. + **/ + public FieldElement subtract(FieldElement val) { + int[] g = ((Ed25519FieldElement) val).t; + int[] h = new int[10]; + for (int i = 0; i < 10; i++) { + h[i] = t[i] - g[i]; + } + return new Ed25519FieldElement(f, h); + } + + /** + * $h = -f$ + *

    + * TODO-CR BR: see above. + *

    + * Preconditions: + *

      + *
    • $|f|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc. + *

    + * Postconditions: + *

      + *
    • $|h|$ bounded by $1.1*2^{25},1.1*2^{24},1.1*2^{25},1.1*2^{24},$ etc. + *
    + * + * @return The field element (-1) * this. + */ + public FieldElement negate() { + int[] h = new int[10]; + for (int i = 0; i < 10; i++) { + h[i] = -t[i]; + } + return new Ed25519FieldElement(f, h); + } + + /** + * $h = f * g$ + *

    + * Can overlap $h$ with $f$ or $g$. + *

    + * Preconditions: + *

      + *
    • $|f|$ bounded by + * $1.65*2^{26},1.65*2^{25},1.65*2^{26},1.65*2^{25},$ etc. + *
    • $|g|$ bounded by + * $1.65*2^{26},1.65*2^{25},1.65*2^{26},1.65*2^{25},$ etc. + *

    + * Postconditions: + *

      + *
    • $|h|$ bounded by + * $1.01*2^{25},1.01*2^{24},1.01*2^{25},1.01*2^{24},$ etc. + *

    + * Notes on implementation strategy: + *

    + * Using schoolbook multiplication. Karatsuba would save a little in some + * cost models. + *

    + * Most multiplications by 2 and 19 are 32-bit precomputations; cheaper than + * 64-bit postcomputations. + *

    + * There is one remaining multiplication by 19 in the carry chain; one *19 + * precomputation can be merged into this, but the resulting data flow is + * considerably less clean. + *

    + * There are 12 carries below. 10 of them are 2-way parallelizable and + * vectorizable. Can get away with 11 carries, but then data flow is much + * deeper. + *

    + * With tighter constraints on inputs can squeeze carries into int32. + * + * @param val The field element to multiply. + * @return The (reasonably reduced) field element this * val. + */ + public FieldElement multiply(FieldElement val) { + int[] g = ((Ed25519FieldElement) val).t; + int g1_19 = 19 * g[1]; /* 1.959375*2^29 */ + int g2_19 = 19 * g[2]; /* 1.959375*2^30; still ok */ + int g3_19 = 19 * g[3]; + int g4_19 = 19 * g[4]; + int g5_19 = 19 * g[5]; + int g6_19 = 19 * g[6]; + int g7_19 = 19 * g[7]; + int g8_19 = 19 * g[8]; + int g9_19 = 19 * g[9]; + int f1_2 = 2 * t[1]; + int f3_2 = 2 * t[3]; + int f5_2 = 2 * t[5]; + int f7_2 = 2 * t[7]; + int f9_2 = 2 * t[9]; + long f0g0 = t[0] * (long) g[0]; + long f0g1 = t[0] * (long) g[1]; + long f0g2 = t[0] * (long) g[2]; + long f0g3 = t[0] * (long) g[3]; + long f0g4 = t[0] * (long) g[4]; + long f0g5 = t[0] * (long) g[5]; + long f0g6 = t[0] * (long) g[6]; + long f0g7 = t[0] * (long) g[7]; + long f0g8 = t[0] * (long) g[8]; + long f0g9 = t[0] * (long) g[9]; + long f1g0 = t[1] * (long) g[0]; + long f1g1_2 = f1_2 * (long) g[1]; + long f1g2 = t[1] * (long) g[2]; + long f1g3_2 = f1_2 * (long) g[3]; + long f1g4 = t[1] * (long) g[4]; + long f1g5_2 = f1_2 * (long) g[5]; + long f1g6 = t[1] * (long) g[6]; + long f1g7_2 = f1_2 * (long) g[7]; + long f1g8 = t[1] * (long) g[8]; + long f1g9_38 = f1_2 * (long) g9_19; + long f2g0 = t[2] * (long) g[0]; + long f2g1 = t[2] * (long) g[1]; + long f2g2 = t[2] * (long) g[2]; + long f2g3 = t[2] * (long) g[3]; + long f2g4 = t[2] * (long) g[4]; + long f2g5 = t[2] * (long) g[5]; + long f2g6 = t[2] * (long) g[6]; + long f2g7 = t[2] * (long) g[7]; + long f2g8_19 = t[2] * (long) g8_19; + long f2g9_19 = t[2] * (long) g9_19; + long f3g0 = t[3] * (long) g[0]; + long f3g1_2 = f3_2 * (long) g[1]; + long f3g2 = t[3] * (long) g[2]; + long f3g3_2 = f3_2 * (long) g[3]; + long f3g4 = t[3] * (long) g[4]; + long f3g5_2 = f3_2 * (long) g[5]; + long f3g6 = t[3] * (long) g[6]; + long f3g7_38 = f3_2 * (long) g7_19; + long f3g8_19 = t[3] * (long) g8_19; + long f3g9_38 = f3_2 * (long) g9_19; + long f4g0 = t[4] * (long) g[0]; + long f4g1 = t[4] * (long) g[1]; + long f4g2 = t[4] * (long) g[2]; + long f4g3 = t[4] * (long) g[3]; + long f4g4 = t[4] * (long) g[4]; + long f4g5 = t[4] * (long) g[5]; + long f4g6_19 = t[4] * (long) g6_19; + long f4g7_19 = t[4] * (long) g7_19; + long f4g8_19 = t[4] * (long) g8_19; + long f4g9_19 = t[4] * (long) g9_19; + long f5g0 = t[5] * (long) g[0]; + long f5g1_2 = f5_2 * (long) g[1]; + long f5g2 = t[5] * (long) g[2]; + long f5g3_2 = f5_2 * (long) g[3]; + long f5g4 = t[5] * (long) g[4]; + long f5g5_38 = f5_2 * (long) g5_19; + long f5g6_19 = t[5] * (long) g6_19; + long f5g7_38 = f5_2 * (long) g7_19; + long f5g8_19 = t[5] * (long) g8_19; + long f5g9_38 = f5_2 * (long) g9_19; + long f6g0 = t[6] * (long) g[0]; + long f6g1 = t[6] * (long) g[1]; + long f6g2 = t[6] * (long) g[2]; + long f6g3 = t[6] * (long) g[3]; + long f6g4_19 = t[6] * (long) g4_19; + long f6g5_19 = t[6] * (long) g5_19; + long f6g6_19 = t[6] * (long) g6_19; + long f6g7_19 = t[6] * (long) g7_19; + long f6g8_19 = t[6] * (long) g8_19; + long f6g9_19 = t[6] * (long) g9_19; + long f7g0 = t[7] * (long) g[0]; + long f7g1_2 = f7_2 * (long) g[1]; + long f7g2 = t[7] * (long) g[2]; + long f7g3_38 = f7_2 * (long) g3_19; + long f7g4_19 = t[7] * (long) g4_19; + long f7g5_38 = f7_2 * (long) g5_19; + long f7g6_19 = t[7] * (long) g6_19; + long f7g7_38 = f7_2 * (long) g7_19; + long f7g8_19 = t[7] * (long) g8_19; + long f7g9_38 = f7_2 * (long) g9_19; + long f8g0 = t[8] * (long) g[0]; + long f8g1 = t[8] * (long) g[1]; + long f8g2_19 = t[8] * (long) g2_19; + long f8g3_19 = t[8] * (long) g3_19; + long f8g4_19 = t[8] * (long) g4_19; + long f8g5_19 = t[8] * (long) g5_19; + long f8g6_19 = t[8] * (long) g6_19; + long f8g7_19 = t[8] * (long) g7_19; + long f8g8_19 = t[8] * (long) g8_19; + long f8g9_19 = t[8] * (long) g9_19; + long f9g0 = t[9] * (long) g[0]; + long f9g1_38 = f9_2 * (long) g1_19; + long f9g2_19 = t[9] * (long) g2_19; + long f9g3_38 = f9_2 * (long) g3_19; + long f9g4_19 = t[9] * (long) g4_19; + long f9g5_38 = f9_2 * (long) g5_19; + long f9g6_19 = t[9] * (long) g6_19; + long f9g7_38 = f9_2 * (long) g7_19; + long f9g8_19 = t[9] * (long) g8_19; + long f9g9_38 = f9_2 * (long) g9_19; + + /** + * Remember: 2^255 congruent 19 modulo p. + * h = h0 * 2^0 + h1 * 2^26 + h2 * 2^(26+25) + h3 * 2^(26+25+26) + ... + h9 * 2^(5*26+5*25). + * So to get the real number we would have to multiply the coefficients with the corresponding powers of 2. + * To get an idea what is going on below, look at the calculation of h0: + * h0 is the coefficient to the power 2^0 so it collects (sums) all products that have the power 2^0. + * f0 * g0 really is f0 * 2^0 * g0 * 2^0 = (f0 * g0) * 2^0. + * f1 * g9 really is f1 * 2^26 * g9 * 2^230 = f1 * g9 * 2^256 = 2 * f1 * g9 * 2^255 congruent 2 * 19 * f1 * g9 * 2^0 modulo p. + * f2 * g8 really is f2 * 2^51 * g8 * 2^204 = f2 * g8 * 2^255 congruent 19 * f2 * g8 * 2^0 modulo p. + * and so on... + */ + long h0 = f0g0 + f1g9_38 + f2g8_19 + f3g7_38 + f4g6_19 + f5g5_38 + f6g4_19 + f7g3_38 + f8g2_19 + f9g1_38; + long h1 = f0g1 + f1g0 + f2g9_19 + f3g8_19 + f4g7_19 + f5g6_19 + f6g5_19 + f7g4_19 + f8g3_19 + f9g2_19; + long h2 = f0g2 + f1g1_2 + f2g0 + f3g9_38 + f4g8_19 + f5g7_38 + f6g6_19 + f7g5_38 + f8g4_19 + f9g3_38; + long h3 = f0g3 + f1g2 + f2g1 + f3g0 + f4g9_19 + f5g8_19 + f6g7_19 + f7g6_19 + f8g5_19 + f9g4_19; + long h4 = f0g4 + f1g3_2 + f2g2 + f3g1_2 + f4g0 + f5g9_38 + f6g8_19 + f7g7_38 + f8g6_19 + f9g5_38; + long h5 = f0g5 + f1g4 + f2g3 + f3g2 + f4g1 + f5g0 + f6g9_19 + f7g8_19 + f8g7_19 + f9g6_19; + long h6 = f0g6 + f1g5_2 + f2g4 + f3g3_2 + f4g2 + f5g1_2 + f6g0 + f7g9_38 + f8g8_19 + f9g7_38; + long h7 = f0g7 + f1g6 + f2g5 + f3g4 + f4g3 + f5g2 + f6g1 + f7g0 + f8g9_19 + f9g8_19; + long h8 = f0g8 + f1g7_2 + f2g6 + f3g5_2 + f4g4 + f5g3_2 + f6g2 + f7g1_2 + f8g0 + f9g9_38; + long h9 = f0g9 + f1g8 + f2g7 + f3g6 + f4g5 + f5g4 + f6g3 + f7g2 + f8g1 + f9g0; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + + /* + |h0| <= (1.65*1.65*2^52*(1+19+19+19+19)+1.65*1.65*2^50*(38+38+38+38+38)) + i.e. |h0| <= 1.4*2^60; narrower ranges for h2, h4, h6, h8 + |h1| <= (1.65*1.65*2^51*(1+1+19+19+19+19+19+19+19+19)) + i.e. |h1| <= 1.7*2^59; narrower ranges for h3, h5, h7, h9 + */ + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + /* |h0| <= 2^25 */ + /* |h4| <= 2^25 */ + /* |h1| <= 1.71*2^59 */ + /* |h5| <= 1.71*2^59 */ + + carry1 = (h1 + (long) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (long) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + /* |h1| <= 2^24; from now on fits into int32 */ + /* |h5| <= 2^24; from now on fits into int32 */ + /* |h2| <= 1.41*2^60 */ + /* |h6| <= 1.41*2^60 */ + + carry2 = (h2 + (long) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (long) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + /* |h2| <= 2^25; from now on fits into int32 unchanged */ + /* |h6| <= 2^25; from now on fits into int32 unchanged */ + /* |h3| <= 1.71*2^59 */ + /* |h7| <= 1.71*2^59 */ + + carry3 = (h3 + (long) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (long) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + /* |h3| <= 2^24; from now on fits into int32 unchanged */ + /* |h7| <= 2^24; from now on fits into int32 unchanged */ + /* |h4| <= 1.72*2^34 */ + /* |h8| <= 1.41*2^60 */ + + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (long) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + /* |h4| <= 2^25; from now on fits into int32 unchanged */ + /* |h8| <= 2^25; from now on fits into int32 unchanged */ + /* |h5| <= 1.01*2^24 */ + /* |h9| <= 1.71*2^59 */ + + carry9 = (h9 + (long) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + /* |h9| <= 2^24; from now on fits into int32 unchanged */ + /* |h0| <= 1.1*2^39 */ + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + /* |h0| <= 2^25; from now on fits into int32 unchanged */ + /* |h1| <= 1.01*2^24 */ + + int[] h = new int[10]; + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + return new Ed25519FieldElement(f, h); + } + + /** + * $h = f * f$ + *

    + * Can overlap $h$ with $f$. + *

    + * Preconditions: + *

      + *
    • $|f|$ bounded by $1.65*2^{26},1.65*2^{25},1.65*2^{26},1.65*2^{25},$ etc. + *

    + * Postconditions: + *

      + *
    • $|h|$ bounded by $1.01*2^{25},1.01*2^{24},1.01*2^{25},1.01*2^{24},$ etc. + *

    + * See {@link #multiply(FieldElement)} for discussion + * of implementation strategy. + * + * @return The (reasonably reduced) square of this field element. + */ + public FieldElement square() { + int f0 = t[0]; + int f1 = t[1]; + int f2 = t[2]; + int f3 = t[3]; + int f4 = t[4]; + int f5 = t[5]; + int f6 = t[6]; + int f7 = t[7]; + int f8 = t[8]; + int f9 = t[9]; + int f0_2 = 2 * f0; + int f1_2 = 2 * f1; + int f2_2 = 2 * f2; + int f3_2 = 2 * f3; + int f4_2 = 2 * f4; + int f5_2 = 2 * f5; + int f6_2 = 2 * f6; + int f7_2 = 2 * f7; + int f5_38 = 38 * f5; /* 1.959375*2^30 */ + int f6_19 = 19 * f6; /* 1.959375*2^30 */ + int f7_38 = 38 * f7; /* 1.959375*2^30 */ + int f8_19 = 19 * f8; /* 1.959375*2^30 */ + int f9_38 = 38 * f9; /* 1.959375*2^30 */ + long f0f0 = f0 * (long) f0; + long f0f1_2 = f0_2 * (long) f1; + long f0f2_2 = f0_2 * (long) f2; + long f0f3_2 = f0_2 * (long) f3; + long f0f4_2 = f0_2 * (long) f4; + long f0f5_2 = f0_2 * (long) f5; + long f0f6_2 = f0_2 * (long) f6; + long f0f7_2 = f0_2 * (long) f7; + long f0f8_2 = f0_2 * (long) f8; + long f0f9_2 = f0_2 * (long) f9; + long f1f1_2 = f1_2 * (long) f1; + long f1f2_2 = f1_2 * (long) f2; + long f1f3_4 = f1_2 * (long) f3_2; + long f1f4_2 = f1_2 * (long) f4; + long f1f5_4 = f1_2 * (long) f5_2; + long f1f6_2 = f1_2 * (long) f6; + long f1f7_4 = f1_2 * (long) f7_2; + long f1f8_2 = f1_2 * (long) f8; + long f1f9_76 = f1_2 * (long) f9_38; + long f2f2 = f2 * (long) f2; + long f2f3_2 = f2_2 * (long) f3; + long f2f4_2 = f2_2 * (long) f4; + long f2f5_2 = f2_2 * (long) f5; + long f2f6_2 = f2_2 * (long) f6; + long f2f7_2 = f2_2 * (long) f7; + long f2f8_38 = f2_2 * (long) f8_19; + long f2f9_38 = f2 * (long) f9_38; + long f3f3_2 = f3_2 * (long) f3; + long f3f4_2 = f3_2 * (long) f4; + long f3f5_4 = f3_2 * (long) f5_2; + long f3f6_2 = f3_2 * (long) f6; + long f3f7_76 = f3_2 * (long) f7_38; + long f3f8_38 = f3_2 * (long) f8_19; + long f3f9_76 = f3_2 * (long) f9_38; + long f4f4 = f4 * (long) f4; + long f4f5_2 = f4_2 * (long) f5; + long f4f6_38 = f4_2 * (long) f6_19; + long f4f7_38 = f4 * (long) f7_38; + long f4f8_38 = f4_2 * (long) f8_19; + long f4f9_38 = f4 * (long) f9_38; + long f5f5_38 = f5 * (long) f5_38; + long f5f6_38 = f5_2 * (long) f6_19; + long f5f7_76 = f5_2 * (long) f7_38; + long f5f8_38 = f5_2 * (long) f8_19; + long f5f9_76 = f5_2 * (long) f9_38; + long f6f6_19 = f6 * (long) f6_19; + long f6f7_38 = f6 * (long) f7_38; + long f6f8_38 = f6_2 * (long) f8_19; + long f6f9_38 = f6 * (long) f9_38; + long f7f7_38 = f7 * (long) f7_38; + long f7f8_38 = f7_2 * (long) f8_19; + long f7f9_76 = f7_2 * (long) f9_38; + long f8f8_19 = f8 * (long) f8_19; + long f8f9_38 = f8 * (long) f9_38; + long f9f9_38 = f9 * (long) f9_38; + + /** + * Same procedure as in multiply, but this time we have a higher symmetry leading to less summands. + * e.g. f1f9_76 really stands for f1 * 2^26 * f9 * 2^230 + f9 * 2^230 + f1 * 2^26 congruent 2 * 2 * 19 * f1 * f9 2^0 modulo p. + */ + long h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + long h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + long h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + long h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + long h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + long h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + long h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + long h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + long h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + long h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + + carry1 = (h1 + (long) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (long) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + + carry2 = (h2 + (long) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (long) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + + carry3 = (h3 + (long) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (long) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (long) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + carry9 = (h9 + (long) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + + int[] h = new int[10]; + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + return new Ed25519FieldElement(f, h); + } + + /** + * $h = 2 * f * f$ + *

    + * Can overlap $h$ with $f$. + *

    + * Preconditions: + *

      + *
    • $|f|$ bounded by $1.65*2^{26},1.65*2^{25},1.65*2^{26},1.65*2^{25},$ etc. + *

    + * Postconditions: + *

      + *
    • $|h|$ bounded by $1.01*2^{25},1.01*2^{24},1.01*2^{25},1.01*2^{24},$ etc. + *

    + * See {@link #multiply(FieldElement)} for discussion + * of implementation strategy. + * + * @return The (reasonably reduced) square of this field element times 2. + */ + public FieldElement squareAndDouble() { + int f0 = t[0]; + int f1 = t[1]; + int f2 = t[2]; + int f3 = t[3]; + int f4 = t[4]; + int f5 = t[5]; + int f6 = t[6]; + int f7 = t[7]; + int f8 = t[8]; + int f9 = t[9]; + int f0_2 = 2 * f0; + int f1_2 = 2 * f1; + int f2_2 = 2 * f2; + int f3_2 = 2 * f3; + int f4_2 = 2 * f4; + int f5_2 = 2 * f5; + int f6_2 = 2 * f6; + int f7_2 = 2 * f7; + int f5_38 = 38 * f5; /* 1.959375*2^30 */ + int f6_19 = 19 * f6; /* 1.959375*2^30 */ + int f7_38 = 38 * f7; /* 1.959375*2^30 */ + int f8_19 = 19 * f8; /* 1.959375*2^30 */ + int f9_38 = 38 * f9; /* 1.959375*2^30 */ + long f0f0 = f0 * (long) f0; + long f0f1_2 = f0_2 * (long) f1; + long f0f2_2 = f0_2 * (long) f2; + long f0f3_2 = f0_2 * (long) f3; + long f0f4_2 = f0_2 * (long) f4; + long f0f5_2 = f0_2 * (long) f5; + long f0f6_2 = f0_2 * (long) f6; + long f0f7_2 = f0_2 * (long) f7; + long f0f8_2 = f0_2 * (long) f8; + long f0f9_2 = f0_2 * (long) f9; + long f1f1_2 = f1_2 * (long) f1; + long f1f2_2 = f1_2 * (long) f2; + long f1f3_4 = f1_2 * (long) f3_2; + long f1f4_2 = f1_2 * (long) f4; + long f1f5_4 = f1_2 * (long) f5_2; + long f1f6_2 = f1_2 * (long) f6; + long f1f7_4 = f1_2 * (long) f7_2; + long f1f8_2 = f1_2 * (long) f8; + long f1f9_76 = f1_2 * (long) f9_38; + long f2f2 = f2 * (long) f2; + long f2f3_2 = f2_2 * (long) f3; + long f2f4_2 = f2_2 * (long) f4; + long f2f5_2 = f2_2 * (long) f5; + long f2f6_2 = f2_2 * (long) f6; + long f2f7_2 = f2_2 * (long) f7; + long f2f8_38 = f2_2 * (long) f8_19; + long f2f9_38 = f2 * (long) f9_38; + long f3f3_2 = f3_2 * (long) f3; + long f3f4_2 = f3_2 * (long) f4; + long f3f5_4 = f3_2 * (long) f5_2; + long f3f6_2 = f3_2 * (long) f6; + long f3f7_76 = f3_2 * (long) f7_38; + long f3f8_38 = f3_2 * (long) f8_19; + long f3f9_76 = f3_2 * (long) f9_38; + long f4f4 = f4 * (long) f4; + long f4f5_2 = f4_2 * (long) f5; + long f4f6_38 = f4_2 * (long) f6_19; + long f4f7_38 = f4 * (long) f7_38; + long f4f8_38 = f4_2 * (long) f8_19; + long f4f9_38 = f4 * (long) f9_38; + long f5f5_38 = f5 * (long) f5_38; + long f5f6_38 = f5_2 * (long) f6_19; + long f5f7_76 = f5_2 * (long) f7_38; + long f5f8_38 = f5_2 * (long) f8_19; + long f5f9_76 = f5_2 * (long) f9_38; + long f6f6_19 = f6 * (long) f6_19; + long f6f7_38 = f6 * (long) f7_38; + long f6f8_38 = f6_2 * (long) f8_19; + long f6f9_38 = f6 * (long) f9_38; + long f7f7_38 = f7 * (long) f7_38; + long f7f8_38 = f7_2 * (long) f8_19; + long f7f9_76 = f7_2 * (long) f9_38; + long f8f8_19 = f8 * (long) f8_19; + long f8f9_38 = f8 * (long) f9_38; + long f9f9_38 = f9 * (long) f9_38; + long h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + long h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + long h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + long h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + long h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + long h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + long h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + long h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + long h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + long h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + + h0 += h0; + h1 += h1; + h2 += h2; + h3 += h3; + h4 += h4; + h5 += h5; + h6 += h6; + h7 += h7; + h8 += h8; + h9 += h9; + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + + carry1 = (h1 + (long) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (long) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + + carry2 = (h2 + (long) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (long) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + + carry3 = (h3 + (long) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (long) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (long) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + carry9 = (h9 + (long) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + + int[] h = new int[10]; + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + return new Ed25519FieldElement(f, h); + } + + /** + * Invert this field element. + *

    + * The inverse is found via Fermat's little theorem:
    + * $a^p \cong a \mod p$ and therefore $a^{(p-2)} \cong a^{-1} \mod p$ + * + * @return The inverse of this field element. + */ + public FieldElement invert() { + FieldElement t0, t1, t2, t3; + + // 2 == 2 * 1 + t0 = square(); + + // 4 == 2 * 2 + t1 = t0.square(); + + // 8 == 2 * 4 + t1 = t1.square(); + + // 9 == 8 + 1 + t1 = multiply(t1); + + // 11 == 9 + 2 + t0 = t0.multiply(t1); + + // 22 == 2 * 11 + t2 = t0.square(); + + // 31 == 22 + 9 + t1 = t1.multiply(t2); + + // 2^6 - 2^1 + t2 = t1.square(); + + // 2^10 - 2^5 + for (int i = 1; i < 5; ++i) { + t2 = t2.square(); + } + + // 2^10 - 2^0 + t1 = t2.multiply(t1); + + // 2^11 - 2^1 + t2 = t1.square(); + + // 2^20 - 2^10 + for (int i = 1; i < 10; ++i) { + t2 = t2.square(); + } + + // 2^20 - 2^0 + t2 = t2.multiply(t1); + + // 2^21 - 2^1 + t3 = t2.square(); + + // 2^40 - 2^20 + for (int i = 1; i < 20; ++i) { + t3 = t3.square(); + } + + // 2^40 - 2^0 + t2 = t3.multiply(t2); + + // 2^41 - 2^1 + t2 = t2.square(); + + // 2^50 - 2^10 + for (int i = 1; i < 10; ++i) { + t2 = t2.square(); + } + + // 2^50 - 2^0 + t1 = t2.multiply(t1); + + // 2^51 - 2^1 + t2 = t1.square(); + + // 2^100 - 2^50 + for (int i = 1; i < 50; ++i) { + t2 = t2.square(); + } + + // 2^100 - 2^0 + t2 = t2.multiply(t1); + + // 2^101 - 2^1 + t3 = t2.square(); + + // 2^200 - 2^100 + for (int i = 1; i < 100; ++i) { + t3 = t3.square(); + } + + // 2^200 - 2^0 + t2 = t3.multiply(t2); + + // 2^201 - 2^1 + t2 = t2.square(); + + // 2^250 - 2^50 + for (int i = 1; i < 50; ++i) { + t2 = t2.square(); + } + + // 2^250 - 2^0 + t1 = t2.multiply(t1); + + // 2^251 - 2^1 + t1 = t1.square(); + + // 2^255 - 2^5 + for (int i = 1; i < 5; ++i) { + t1 = t1.square(); + } + + // 2^255 - 21 + return t1.multiply(t0); + } + + /** + * Gets this field element to the power of $(2^{252} - 3)$. + * This is a helper function for calculating the square root. + *

    + * TODO-CR BR: I think it makes sense to have a sqrt function. + * + * @return This field element to the power of $(2^{252} - 3)$. + */ + public FieldElement pow22523() { + FieldElement t0, t1, t2; + + // 2 == 2 * 1 + t0 = square(); + + // 4 == 2 * 2 + t1 = t0.square(); + + // 8 == 2 * 4 + t1 = t1.square(); + + // z9 = z1*z8 + t1 = multiply(t1); + + // 11 == 9 + 2 + t0 = t0.multiply(t1); + + // 22 == 2 * 11 + t0 = t0.square(); + + // 31 == 22 + 9 + t0 = t1.multiply(t0); + + // 2^6 - 2^1 + t1 = t0.square(); + + // 2^10 - 2^5 + for (int i = 1; i < 5; ++i) { + t1 = t1.square(); + } + + // 2^10 - 2^0 + t0 = t1.multiply(t0); + + // 2^11 - 2^1 + t1 = t0.square(); + + // 2^20 - 2^10 + for (int i = 1; i < 10; ++i) { + t1 = t1.square(); + } + + // 2^20 - 2^0 + t1 = t1.multiply(t0); + + // 2^21 - 2^1 + t2 = t1.square(); + + // 2^40 - 2^20 + for (int i = 1; i < 20; ++i) { + t2 = t2.square(); + } + + // 2^40 - 2^0 + t1 = t2.multiply(t1); + + // 2^41 - 2^1 + t1 = t1.square(); + + // 2^50 - 2^10 + for (int i = 1; i < 10; ++i) { + t1 = t1.square(); + } + + // 2^50 - 2^0 + t0 = t1.multiply(t0); + + // 2^51 - 2^1 + t1 = t0.square(); + + // 2^100 - 2^50 + for (int i = 1; i < 50; ++i) { + t1 = t1.square(); + } + + // 2^100 - 2^0 + t1 = t1.multiply(t0); + + // 2^101 - 2^1 + t2 = t1.square(); + + // 2^200 - 2^100 + for (int i = 1; i < 100; ++i) { + t2 = t2.square(); + } + + // 2^200 - 2^0 + t1 = t2.multiply(t1); + + // 2^201 - 2^1 + t1 = t1.square(); + + // 2^250 - 2^50 + for (int i = 1; i < 50; ++i) { + t1 = t1.square(); + } + + // 2^250 - 2^0 + t0 = t1.multiply(t0); + + // 2^251 - 2^1 + t0 = t0.square(); + + // 2^252 - 2^2 + t0 = t0.square(); + + // 2^252 - 3 + return multiply(t0); + } + + /** + * Constant-time conditional move. Well, actually it is a conditional copy. + * Logic is inspired by the SUPERCOP implementation at: + * https://github.com/floodyberry/supercop/blob/master/crypto_sign/ed25519/ref10/fe_cmov.c + * + * @param val the other field element. + * @param b must be 0 or 1, otherwise results are undefined. + * @return a copy of this if $b == 0$, or a copy of val if $b == 1$. + */ + @Override + public FieldElement cmov(FieldElement val, int b) { + Ed25519FieldElement that = (Ed25519FieldElement) val; + b = -b; + int[] result = new int[10]; + for (int i = 0; i < 10; i++) { + result[i] = this.t[i]; + int x = this.t[i] ^ that.t[i]; + x &= b; + result[i] ^= x; + } + return new Ed25519FieldElement(this.f, result); + } + + @Override + public int hashCode() { + return Arrays.hashCode(t); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Ed25519FieldElement)) + return false; + Ed25519FieldElement fe = (Ed25519FieldElement) obj; + return 1 == Utils.equal(toByteArray(), fe.toByteArray()); + } + + @Override + public String toString() { + return "[Ed25519FieldElement val=" + Utils.bytesToHex(toByteArray()) + "]"; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/Ed25519LittleEndianEncoding.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/Ed25519LittleEndianEncoding.java new file mode 100644 index 0000000..cf860df --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/Ed25519LittleEndianEncoding.java @@ -0,0 +1,288 @@ +package org.xbib.net.security.eddsa.math.ed25519; + +import org.xbib.net.security.eddsa.math.Encoding; +import org.xbib.net.security.eddsa.math.FieldElement; + +/** + * Helper class for encoding/decoding from/to the 32 byte representation. + * Reviewed/commented by Bloody Rookie (nemproject@gmx.de) + */ +public class Ed25519LittleEndianEncoding extends Encoding { + + static int load_3(byte[] in, int offset) { + int result = in[offset++] & 0xff; + result |= (in[offset++] & 0xff) << 8; + result |= (in[offset] & 0xff) << 16; + return result; + } + + static long load_4(byte[] in, int offset) { + int result = in[offset++] & 0xff; + result |= (in[offset++] & 0xff) << 8; + result |= (in[offset++] & 0xff) << 16; + result |= in[offset] << 24; + return ((long) result) & 0xffffffffL; + } + + public Ed25519LittleEndianEncoding() { + } + + /** + * Encodes a given field element in its 32 byte representation. This is done in two steps: + *

      + *
    1. Reduce the value of the field element modulo $p$. + *
    2. Convert the field element to the 32 byte representation. + *

    + * The idea for the modulo $p$ reduction algorithm is as follows: + *

    + *

    Assumption:

    + *
      + *
    • $p = 2^{255} - 19$ + *
    • $h = h_0 + 2^{25} * h_1 + 2^{(26+25)} * h_2 + \dots + 2^{230} * h_9$ where $0 \le |h_i| \lt 2^{27}$ for all $i=0,\dots,9$. + *
    • $h \cong r \mod p$, i.e. $h = r + q * p$ for some suitable $0 \le r \lt p$ and an integer $q$. + *

    + * Then $q = [2^{-255} * (h + 19 * 2^{-25} * h_9 + 1/2)]$ where $[x] = floor(x)$. + *

    + *

    Proof:

    + *

    + * We begin with some very raw estimation for the bounds of some expressions: + *

    + * $$ + * \begin{equation} + * |h| \lt 2^{230} * 2^{30} = 2^{260} \Rightarrow |r + q * p| \lt 2^{260} \Rightarrow |q| \lt 2^{10}. \\ + * \Rightarrow -1/4 \le a := 19^2 * 2^{-255} * q \lt 1/4. \\ + * |h - 2^{230} * h_9| = |h_0 + \dots + 2^{204} * h_8| \lt 2^{204} * 2^{30} = 2^{234}. \\ + * \Rightarrow -1/4 \le b := 19 * 2^{-255} * (h - 2^{230} * h_9) \lt 1/4 + * \end{equation} + * $$ + *

    + * Therefore $0 \lt 1/2 - a - b \lt 1$. + *

    + * Set $x := r + 19 * 2^{-255} * r + 1/2 - a - b$. Then: + *

    + * $$ + * 0 \le x \lt 255 - 20 + 19 + 1 = 2^{255} \\ + * \Rightarrow 0 \le 2^{-255} * x \lt 1. + * $$ + *

    + * Since $q$ is an integer we have + *

    + * $$ + * [q + 2^{-255} * x] = q \quad (1) + * $$ + *

    + * Have a closer look at $x$: + *

    + * $$ + * \begin{align} + * x &= h - q * (2^{255} - 19) + 19 * 2^{-255} * (h - q * (2^{255} - 19)) + 1/2 - 19^2 * 2^{-255} * q - 19 * 2^{-255} * (h - 2^{230} * h_9) \\ + * &= h - q * 2^{255} + 19 * q + 19 * 2^{-255} * h - 19 * q + 19^2 * 2^{-255} * q + 1/2 - 19^2 * 2^{-255} * q - 19 * 2^{-255} * h + 19 * 2^{-25} * h_9 \\ + * &= h + 19 * 2^{-25} * h_9 + 1/2 - q^{255}. + * \end{align} + * $$ + *

    + * Inserting the expression for $x$ into $(1)$ we get the desired expression for $q$. + */ + public byte[] encode(FieldElement x) { + int[] h = ((Ed25519FieldElement) x).t; + int h0 = h[0]; + int h1 = h[1]; + int h2 = h[2]; + int h3 = h[3]; + int h4 = h[4]; + int h5 = h[5]; + int h6 = h[6]; + int h7 = h[7]; + int h8 = h[8]; + int h9 = h[9]; + int q; + int carry0; + int carry1; + int carry2; + int carry3; + int carry4; + int carry5; + int carry6; + int carry7; + int carry8; + int carry9; + + // Step 1: + // Calculate q + q = (19 * h9 + (1 << 24)) >> 25; + q = (h0 + q) >> 26; + q = (h1 + q) >> 25; + q = (h2 + q) >> 26; + q = (h3 + q) >> 25; + q = (h4 + q) >> 26; + q = (h5 + q) >> 25; + q = (h6 + q) >> 26; + q = (h7 + q) >> 25; + q = (h8 + q) >> 26; + q = (h9 + q) >> 25; + + // r = h - q * p = h - 2^255 * q + 19 * q + // First add 19 * q then discard the bit 255 + h0 += 19 * q; + + carry0 = h0 >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry1 = h1 >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry2 = h2 >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry3 = h3 >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry4 = h4 >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry5 = h5 >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry6 = h6 >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry7 = h7 >> 25; + h8 += carry7; + h7 -= carry7 << 25; + carry8 = h8 >> 26; + h9 += carry8; + h8 -= carry8 << 26; + carry9 = h9 >> 25; + h9 -= carry9 << 25; + + // Step 2 (straight forward conversion): + byte[] s = new byte[32]; + s[0] = (byte) h0; + s[1] = (byte) (h0 >> 8); + s[2] = (byte) (h0 >> 16); + s[3] = (byte) ((h0 >> 24) | (h1 << 2)); + s[4] = (byte) (h1 >> 6); + s[5] = (byte) (h1 >> 14); + s[6] = (byte) ((h1 >> 22) | (h2 << 3)); + s[7] = (byte) (h2 >> 5); + s[8] = (byte) (h2 >> 13); + s[9] = (byte) ((h2 >> 21) | (h3 << 5)); + s[10] = (byte) (h3 >> 3); + s[11] = (byte) (h3 >> 11); + s[12] = (byte) ((h3 >> 19) | (h4 << 6)); + s[13] = (byte) (h4 >> 2); + s[14] = (byte) (h4 >> 10); + s[15] = (byte) (h4 >> 18); + s[16] = (byte) h5; + s[17] = (byte) (h5 >> 8); + s[18] = (byte) (h5 >> 16); + s[19] = (byte) ((h5 >> 24) | (h6 << 1)); + s[20] = (byte) (h6 >> 7); + s[21] = (byte) (h6 >> 15); + s[22] = (byte) ((h6 >> 23) | (h7 << 3)); + s[23] = (byte) (h7 >> 5); + s[24] = (byte) (h7 >> 13); + s[25] = (byte) ((h7 >> 21) | (h8 << 4)); + s[26] = (byte) (h8 >> 4); + s[27] = (byte) (h8 >> 12); + s[28] = (byte) ((h8 >> 20) | (h9 << 6)); + s[29] = (byte) (h9 >> 2); + s[30] = (byte) (h9 >> 10); + s[31] = (byte) (h9 >> 18); + return s; + } + + /** + * Decodes a given field element in its 10 byte $2^{25.5}$ representation. + * + * @param in The 32 byte representation. + * @return The field element in its $2^{25.5}$ bit representation. + */ + public FieldElement decode(byte[] in) { + long h0 = load_4(in, 0); + long h1 = load_3(in, 4) << 6; + long h2 = load_3(in, 7) << 5; + long h3 = load_3(in, 10) << 3; + long h4 = load_3(in, 13) << 2; + long h5 = load_4(in, 16); + long h6 = load_3(in, 20) << 7; + long h7 = load_3(in, 23) << 5; + long h8 = load_3(in, 26) << 4; + long h9 = (load_3(in, 29) & 0x7FFFFF) << 2; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + + // Remember: 2^255 congruent 19 modulo p + carry9 = (h9 + (long) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + carry1 = (h1 + (long) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry3 = (h3 + (long) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry5 = (h5 + (long) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry7 = (h7 + (long) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry2 = (h2 + (long) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry6 = (h6 + (long) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry8 = (h8 + (long) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + int[] h = new int[10]; + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + return new Ed25519FieldElement(f, h); + } + + /** + * Is the FieldElement negative in this encoding? + *

    + * Return true if $x$ is in $\{1,3,5,\dots,q-2\}$
    + * Return false if $x$ is in $\{0,2,4,\dots,q-1\}$ + *

    + * Preconditions: + *

      + *
    • $|x|$ bounded by $1.1*2^{26},1.1*2^{25},1.1*2^{26},1.1*2^{25}$, etc. + *
    + * + * @return true if $x$ is in $\{1,3,5,\dots,q-2\}$, false otherwise. + */ + public boolean isNegative(FieldElement x) { + byte[] s = encode(x); + return (s[0] & 1) != 0; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/Ed25519ScalarOps.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/Ed25519ScalarOps.java new file mode 100644 index 0000000..5c97322 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/Ed25519ScalarOps.java @@ -0,0 +1,916 @@ +package org.xbib.net.security.eddsa.math.ed25519; + +import org.xbib.net.security.eddsa.math.ScalarOps; + +import static org.xbib.net.security.eddsa.math.ed25519.Ed25519LittleEndianEncoding.load_3; +import static org.xbib.net.security.eddsa.math.ed25519.Ed25519LittleEndianEncoding.load_4; + +/** + * Class for reducing a huge integer modulo the group order q and + * doing a combined multiply plus add plus reduce operation. + *

    + * $q = 2^{252} + 27742317777372353535851937790883648493$. + *

    + * Reviewed/commented by Bloody Rookie (nemproject@gmx.de) + */ +public class Ed25519ScalarOps implements ScalarOps { + + public Ed25519ScalarOps() { + } + + /** + * Reduction modulo the group order $q$. + *

    + * Input: + * $s[0]+256*s[1]+\dots+256^{63}*s[63] = s$ + *

    + * Output: + * $s[0]+256*s[1]+\dots+256^{31}*s[31] = s \bmod q$ + * where $q = 2^{252} + 27742317777372353535851937790883648493$. + */ + public byte[] reduce(byte[] s) { + // s0,..., s22 have 21 bits, s23 has 29 bits + long s0 = 0x1FFFFF & load_3(s, 0); + long s1 = 0x1FFFFF & (load_4(s, 2) >> 5); + long s2 = 0x1FFFFF & (load_3(s, 5) >> 2); + long s3 = 0x1FFFFF & (load_4(s, 7) >> 7); + long s4 = 0x1FFFFF & (load_4(s, 10) >> 4); + long s5 = 0x1FFFFF & (load_3(s, 13) >> 1); + long s6 = 0x1FFFFF & (load_4(s, 15) >> 6); + long s7 = 0x1FFFFF & (load_3(s, 18) >> 3); + long s8 = 0x1FFFFF & load_3(s, 21); + long s9 = 0x1FFFFF & (load_4(s, 23) >> 5); + long s10 = 0x1FFFFF & (load_3(s, 26) >> 2); + long s11 = 0x1FFFFF & (load_4(s, 28) >> 7); + long s12 = 0x1FFFFF & (load_4(s, 31) >> 4); + long s13 = 0x1FFFFF & (load_3(s, 34) >> 1); + long s14 = 0x1FFFFF & (load_4(s, 36) >> 6); + long s15 = 0x1FFFFF & (load_3(s, 39) >> 3); + long s16 = 0x1FFFFF & load_3(s, 42); + long s17 = 0x1FFFFF & (load_4(s, 44) >> 5); + long s18 = 0x1FFFFF & (load_3(s, 47) >> 2); + long s19 = 0x1FFFFF & (load_4(s, 49) >> 7); + long s20 = 0x1FFFFF & (load_4(s, 52) >> 4); + long s21 = 0x1FFFFF & (load_3(s, 55) >> 1); + long s22 = 0x1FFFFF & (load_4(s, 57) >> 6); + long s23 = (load_4(s, 60) >> 3); + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + long carry10; + long carry11; + long carry12; + long carry13; + long carry14; + long carry15; + long carry16; + + /** + * Lots of magic numbers :) + * To understand what's going on below, note that + * + * (1) q = 2^252 + q0 where q0 = 27742317777372353535851937790883648493. + * (2) s11 is the coefficient of 2^(11*21), s23 is the coefficient of 2^(^23*21) and 2^252 = 2^((23-11) * 21)). + * (3) 2^252 congruent -q0 modulo q. + * (4) -q0 = 666643 * 2^0 + 470296 * 2^21 + 654183 * 2^(2*21) - 997805 * 2^(3*21) + 136657 * 2^(4*21) - 683901 * 2^(5*21) + * + * Thus + * s23 * 2^(23*11) = s23 * 2^(12*21) * 2^(11*21) = s3 * 2^252 * 2^(11*21) congruent + * s23 * (666643 * 2^0 + 470296 * 2^21 + 654183 * 2^(2*21) - 997805 * 2^(3*21) + 136657 * 2^(4*21) - 683901 * 2^(5*21)) * 2^(11*21) modulo q = + * s23 * (666643 * 2^(11*21) + 470296 * 2^(12*21) + 654183 * 2^(13*21) - 997805 * 2^(14*21) + 136657 * 2^(15*21) - 683901 * 2^(16*21)). + * + * The same procedure is then applied for s22,...,s18. + */ + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + // not used again + //s23 = 0; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + // not used again + //s22 = 0; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + // not used again + //s21 = 0; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + // not used again + //s20 = 0; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + // not used again + //s19 = 0; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + // not used again + //s18 = 0; + + /** + * Time to reduce the coefficient in order not to get an overflow. + */ + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + + /** + * Continue with above procedure. + */ + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + // not used again + //s17 = 0; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + // not used again + //s16 = 0; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + // not used again + //s15 = 0; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + // not used again + //s14 = 0; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + // not used again + //s13 = 0; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + // set below + //s12 = 0; + + /** + * Reduce coefficients again. + */ + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + //carry11 = (s11 + (1<<20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 = carry11; + s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + // set below + //s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + //carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21; + carry11 = s11 >> 21; + s12 = carry11; + s11 -= carry11 << 21; + + // TODO-CR BR: Is it really needed to do it TWO times? (it doesn't hurt, just a question). + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + // not used again + //s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + // s0, ..., s11 got 21 bits each. + byte[] result = new byte[32]; + result[0] = (byte) s0; + result[1] = (byte) (s0 >> 8); + result[2] = (byte) ((s0 >> 16) | (s1 << 5)); + result[3] = (byte) (s1 >> 3); + result[4] = (byte) (s1 >> 11); + result[5] = (byte) ((s1 >> 19) | (s2 << 2)); + result[6] = (byte) (s2 >> 6); + result[7] = (byte) ((s2 >> 14) | (s3 << 7)); + result[8] = (byte) (s3 >> 1); + result[9] = (byte) (s3 >> 9); + result[10] = (byte) ((s3 >> 17) | (s4 << 4)); + result[11] = (byte) (s4 >> 4); + result[12] = (byte) (s4 >> 12); + result[13] = (byte) ((s4 >> 20) | (s5 << 1)); + result[14] = (byte) (s5 >> 7); + result[15] = (byte) ((s5 >> 15) | (s6 << 6)); + result[16] = (byte) (s6 >> 2); + result[17] = (byte) (s6 >> 10); + result[18] = (byte) ((s6 >> 18) | (s7 << 3)); + result[19] = (byte) (s7 >> 5); + result[20] = (byte) (s7 >> 13); + result[21] = (byte) s8; + result[22] = (byte) (s8 >> 8); + result[23] = (byte) ((s8 >> 16) | (s9 << 5)); + result[24] = (byte) (s9 >> 3); + result[25] = (byte) (s9 >> 11); + result[26] = (byte) ((s9 >> 19) | (s10 << 2)); + result[27] = (byte) (s10 >> 6); + result[28] = (byte) ((s10 >> 14) | (s11 << 7)); + result[29] = (byte) (s11 >> 1); + result[30] = (byte) (s11 >> 9); + result[31] = (byte) (s11 >> 17); + return result; + } + + + /** + * $(ab+c) \bmod q$ + *

    + * Input: + *

      + *
    • $a[0]+256*a[1]+\dots+256^{31}*a[31] = a$ + *
    • $b[0]+256*b[1]+\dots+256^{31}*b[31] = b$ + *
    • $c[0]+256*c[1]+\dots+256^{31}*c[31] = c$ + *

    + * Output: + * $result[0]+256*result[1]+\dots+256^{31}*result[31] = (ab+c) \bmod q$ + * where $q = 2^{252} + 27742317777372353535851937790883648493$. + *

    + * See the comments in {@link #reduce(byte[])} for an explanation of the algorithm. + */ + public byte[] multiplyAndAdd(byte[] a, byte[] b, byte[] c) { + long a0 = 0x1FFFFF & load_3(a, 0); + long a1 = 0x1FFFFF & (load_4(a, 2) >> 5); + long a2 = 0x1FFFFF & (load_3(a, 5) >> 2); + long a3 = 0x1FFFFF & (load_4(a, 7) >> 7); + long a4 = 0x1FFFFF & (load_4(a, 10) >> 4); + long a5 = 0x1FFFFF & (load_3(a, 13) >> 1); + long a6 = 0x1FFFFF & (load_4(a, 15) >> 6); + long a7 = 0x1FFFFF & (load_3(a, 18) >> 3); + long a8 = 0x1FFFFF & load_3(a, 21); + long a9 = 0x1FFFFF & (load_4(a, 23) >> 5); + long a10 = 0x1FFFFF & (load_3(a, 26) >> 2); + long a11 = (load_4(a, 28) >> 7); + long b0 = 0x1FFFFF & load_3(b, 0); + long b1 = 0x1FFFFF & (load_4(b, 2) >> 5); + long b2 = 0x1FFFFF & (load_3(b, 5) >> 2); + long b3 = 0x1FFFFF & (load_4(b, 7) >> 7); + long b4 = 0x1FFFFF & (load_4(b, 10) >> 4); + long b5 = 0x1FFFFF & (load_3(b, 13) >> 1); + long b6 = 0x1FFFFF & (load_4(b, 15) >> 6); + long b7 = 0x1FFFFF & (load_3(b, 18) >> 3); + long b8 = 0x1FFFFF & load_3(b, 21); + long b9 = 0x1FFFFF & (load_4(b, 23) >> 5); + long b10 = 0x1FFFFF & (load_3(b, 26) >> 2); + long b11 = (load_4(b, 28) >> 7); + long c0 = 0x1FFFFF & load_3(c, 0); + long c1 = 0x1FFFFF & (load_4(c, 2) >> 5); + long c2 = 0x1FFFFF & (load_3(c, 5) >> 2); + long c3 = 0x1FFFFF & (load_4(c, 7) >> 7); + long c4 = 0x1FFFFF & (load_4(c, 10) >> 4); + long c5 = 0x1FFFFF & (load_3(c, 13) >> 1); + long c6 = 0x1FFFFF & (load_4(c, 15) >> 6); + long c7 = 0x1FFFFF & (load_3(c, 18) >> 3); + long c8 = 0x1FFFFF & load_3(c, 21); + long c9 = 0x1FFFFF & (load_4(c, 23) >> 5); + long c10 = 0x1FFFFF & (load_3(c, 26) >> 2); + long c11 = (load_4(c, 28) >> 7); + long s0; + long s1; + long s2; + long s3; + long s4; + long s5; + long s6; + long s7; + long s8; + long s9; + long s10; + long s11; + long s12; + long s13; + long s14; + long s15; + long s16; + long s17; + long s18; + long s19; + long s20; + long s21; + long s22; + long s23; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + long carry10; + long carry11; + long carry12; + long carry13; + long carry14; + long carry15; + long carry16; + long carry17; + long carry18; + long carry19; + long carry20; + long carry21; + long carry22; + + s0 = c0 + a0 * b0; + s1 = c1 + a0 * b1 + a1 * b0; + s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; + s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; + s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; + s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; + s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0; + s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 + a8 * b0; + s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0; + s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0; + s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; + s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1; + s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2; + s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + a11 * b3; + s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4; + s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; + s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; + s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; + s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; + s20 = a9 * b11 + a10 * b10 + a11 * b9; + s21 = a10 * b11 + a11 * b10; + s22 = a11 * b11; + // set below + //s23 = 0; + + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + carry18 = (s18 + (1 << 20)) >> 21; + s19 += carry18; + s18 -= carry18 << 21; + carry20 = (s20 + (1 << 20)) >> 21; + s21 += carry20; + s20 -= carry20 << 21; + //carry22 = (s22 + (1<<20)) >> 21; s23 += carry22; s22 -= carry22 << 21; + carry22 = (s22 + (1 << 20)) >> 21; + s23 = carry22; + s22 -= carry22 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + carry17 = (s17 + (1 << 20)) >> 21; + s18 += carry17; + s17 -= carry17 << 21; + carry19 = (s19 + (1 << 20)) >> 21; + s20 += carry19; + s19 -= carry19 << 21; + carry21 = (s21 + (1 << 20)) >> 21; + s22 += carry21; + s21 -= carry21 << 21; + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + // not used again + //s23 = 0; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + // not used again + //s22 = 0; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + // not used again + //s21 = 0; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + // not used again + //s20 = 0; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + // not used again + //s19 = 0; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + // not used again + //s18 = 0; + + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + // not used again + //s17 = 0; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + // not used again + //s16 = 0; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + // not used again + //s15 = 0; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + // not used again + //s14 = 0; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + // not used again + //s13 = 0; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + // set below + //s12 = 0; + + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + //carry11 = (s11 + (1<<20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 = carry11; + s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + // set below + //s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + //carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21; + carry11 = s11 >> 21; + s12 = carry11; + s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + // not used again + //s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + byte[] result = new byte[32]; + result[0] = (byte) s0; + result[1] = (byte) (s0 >> 8); + result[2] = (byte) ((s0 >> 16) | (s1 << 5)); + result[3] = (byte) (s1 >> 3); + result[4] = (byte) (s1 >> 11); + result[5] = (byte) ((s1 >> 19) | (s2 << 2)); + result[6] = (byte) (s2 >> 6); + result[7] = (byte) ((s2 >> 14) | (s3 << 7)); + result[8] = (byte) (s3 >> 1); + result[9] = (byte) (s3 >> 9); + result[10] = (byte) ((s3 >> 17) | (s4 << 4)); + result[11] = (byte) (s4 >> 4); + result[12] = (byte) (s4 >> 12); + result[13] = (byte) ((s4 >> 20) | (s5 << 1)); + result[14] = (byte) (s5 >> 7); + result[15] = (byte) ((s5 >> 15) | (s6 << 6)); + result[16] = (byte) (s6 >> 2); + result[17] = (byte) (s6 >> 10); + result[18] = (byte) ((s6 >> 18) | (s7 << 3)); + result[19] = (byte) (s7 >> 5); + result[20] = (byte) (s7 >> 13); + result[21] = (byte) s8; + result[22] = (byte) (s8 >> 8); + result[23] = (byte) ((s8 >> 16) | (s9 << 5)); + result[24] = (byte) (s9 >> 3); + result[25] = (byte) (s9 >> 11); + result[26] = (byte) ((s9 >> 19) | (s10 << 2)); + result[27] = (byte) (s10 >> 6); + result[28] = (byte) ((s10 >> 14) | (s11 << 7)); + result[29] = (byte) (s11 >> 1); + result[30] = (byte) (s11 >> 9); + result[31] = (byte) (s11 >> 17); + return result; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/package-info.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/package-info.java new file mode 100644 index 0000000..321eaa2 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/ed25519/package-info.java @@ -0,0 +1,4 @@ +/** + * Low-level, optimized implementation using Radix $2^{51}$ for Curve 25519. + */ +package org.xbib.net.security.eddsa.math.ed25519; diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/math/package-info.java b/net-security/src/main/java/org/xbib/net/security/eddsa/math/package-info.java new file mode 100644 index 0000000..03c6017 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/math/package-info.java @@ -0,0 +1,6 @@ +/** + * Data structures that defines curves and fields, and the mathematical operations on them. + * Low-level implementation in bigint for any curve using BigIntegers, + * and in ed25519 for Curve 25519 using Radix $2^{51}$. + */ +package org.xbib.net.security.eddsa.math; diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAGenParameterSpec.java b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAGenParameterSpec.java new file mode 100644 index 0000000..df48806 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAGenParameterSpec.java @@ -0,0 +1,19 @@ +package org.xbib.net.security.eddsa.spec; + +import java.security.spec.AlgorithmParameterSpec; + +/** + * Implementation of AlgorithmParameterSpec that holds the name of a named + * EdDSA curve specification. + */ +public class EdDSAGenParameterSpec implements AlgorithmParameterSpec { + private final String name; + + public EdDSAGenParameterSpec(String stdName) { + name = stdName; + } + + public String getName() { + return name; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSANamedCurveSpec.java b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSANamedCurveSpec.java new file mode 100644 index 0000000..caec379 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSANamedCurveSpec.java @@ -0,0 +1,23 @@ +package org.xbib.net.security.eddsa.spec; + +import org.xbib.net.security.eddsa.math.Curve; +import org.xbib.net.security.eddsa.math.GroupElement; +import org.xbib.net.security.eddsa.math.ScalarOps; + +/** + * EdDSA Curve specification that can also be referred to by name. + */ +public class EdDSANamedCurveSpec extends EdDSAParameterSpec { + + private final String name; + + public EdDSANamedCurveSpec(String name, Curve curve, + String hashAlgo, ScalarOps sc, GroupElement B) { + super(curve, hashAlgo, sc, B); + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSANamedCurveTable.java b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSANamedCurveTable.java new file mode 100644 index 0000000..c012a72 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSANamedCurveTable.java @@ -0,0 +1,60 @@ +package org.xbib.net.security.eddsa.spec; + +import org.xbib.net.security.eddsa.Utils; +import org.xbib.net.security.eddsa.math.Curve; +import org.xbib.net.security.eddsa.math.Field; +import org.xbib.net.security.eddsa.math.ed25519.Ed25519LittleEndianEncoding; +import org.xbib.net.security.eddsa.math.ed25519.Ed25519ScalarOps; + +import java.util.Hashtable; +import java.util.Locale; + +/** + * The named EdDSA curves. + */ +public class EdDSANamedCurveTable { + + private static final Field ed25519field = new Field( + 256, // b + Utils.hexToBytes("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), // q + new Ed25519LittleEndianEncoding()); + + private static final Curve ed25519curve = new Curve(ed25519field, + Utils.hexToBytes("a3785913ca4deb75abd841414d0a700098e879777940c78c73fe6f2bee6c0352"), // d + ed25519field.fromByteArray(Utils.hexToBytes("b0a00e4a271beec478e42fad0618432fa7d7fb3d99004d2b0bdfc14f8024832b"))); // I + + private static final EdDSANamedCurveSpec ed25519 = new EdDSANamedCurveSpec( + "Ed25519", + ed25519curve, + "SHA-512", // H + new Ed25519ScalarOps(), // l + ed25519curve.createPoint( // B + Utils.hexToBytes("5866666666666666666666666666666666666666666666666666666666666666"), + true)); // Precompute tables for B + + private static final Hashtable curves = new Hashtable(); + + static { + // RFC 8032 + defineCurve(ed25519); + } + + EdDSANamedCurveTable() { + } + + public static void defineCurve(EdDSANamedCurveSpec curve) { + curves.put(curve.getName().toLowerCase(Locale.ENGLISH), curve); + } + + static void defineCurveAlias(String name, String alias) { + EdDSANamedCurveSpec curve = curves.get(name.toLowerCase(Locale.ENGLISH)); + if (curve == null) { + throw new IllegalStateException(); + } + curves.put(alias.toLowerCase(Locale.ENGLISH), curve); + } + + public static EdDSANamedCurveSpec getByName(String name) { + return curves.get(name.toLowerCase(Locale.ENGLISH)); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAParameterSpec.java b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAParameterSpec.java new file mode 100644 index 0000000..9f12b38 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAParameterSpec.java @@ -0,0 +1,83 @@ +package org.xbib.net.security.eddsa.spec; + +import org.xbib.net.security.eddsa.math.Curve; +import org.xbib.net.security.eddsa.math.GroupElement; +import org.xbib.net.security.eddsa.math.ScalarOps; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.AlgorithmParameterSpec; + +/** + * Parameter specification for an EdDSA algorithm. + */ +public class EdDSAParameterSpec implements AlgorithmParameterSpec { + private final Curve curve; + private final String hashAlgo; + private final ScalarOps sc; + private final GroupElement B; + + /** + * @param curve the curve + * @param hashAlgo the JCA string for the hash algorithm + * @param sc the parameter L represented as ScalarOps + * @param B the parameter B + * @throws IllegalArgumentException if hash algorithm is unsupported or length is wrong + */ + public EdDSAParameterSpec(Curve curve, String hashAlgo, + ScalarOps sc, GroupElement B) { + try { + MessageDigest hash = MessageDigest.getInstance(hashAlgo); + // EdDSA hash function must produce 2b-bit output + if (curve.getField().getb() / 4 != hash.getDigestLength()) + throw new IllegalArgumentException("Hash output is not 2b-bit"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unsupported hash algorithm"); + } + + this.curve = curve; + this.hashAlgo = hashAlgo; + this.sc = sc; + this.B = B; + } + + public Curve getCurve() { + return curve; + } + + public String getHashAlgorithm() { + return hashAlgo; + } + + public ScalarOps getScalarOps() { + return sc; + } + + /** + * @return the base (generator) + */ + public GroupElement getB() { + return B; + } + + @Override + public int hashCode() { + return hashAlgo.hashCode() ^ + curve.hashCode() ^ + B.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof EdDSAParameterSpec)) { + return false; + } + EdDSAParameterSpec s = (EdDSAParameterSpec) o; + return hashAlgo.equals(s.getHashAlgorithm()) && + curve.equals(s.getCurve()) && + B.equals(s.getB()); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAPrivateKeySpec.java b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAPrivateKeySpec.java new file mode 100644 index 0000000..54c4cec --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAPrivateKeySpec.java @@ -0,0 +1,120 @@ +package org.xbib.net.security.eddsa.spec; + +import org.xbib.net.security.eddsa.math.GroupElement; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.KeySpec; +import java.util.Arrays; + +/** + * + */ +public class EdDSAPrivateKeySpec implements KeySpec { + private final byte[] seed; + private final byte[] h; + private final byte[] a; + private final GroupElement A; + private final EdDSAParameterSpec spec; + + /** + * @param seed the private key + * @param spec the parameter specification for this key + * @throws IllegalArgumentException if seed length is wrong or hash algorithm is unsupported + */ + public EdDSAPrivateKeySpec(byte[] seed, EdDSAParameterSpec spec) { + if (seed.length != spec.getCurve().getField().getb() / 8) + throw new IllegalArgumentException("seed length is wrong"); + + this.spec = spec; + this.seed = seed; + + try { + MessageDigest hash = MessageDigest.getInstance(spec.getHashAlgorithm()); + int b = spec.getCurve().getField().getb(); + + // H(k) + h = hash.digest(seed); + + /*a = BigInteger.valueOf(2).pow(b-2); + for (int i=3;i<(b-2);i++) { + a = a.add(BigInteger.valueOf(2).pow(i).multiply(BigInteger.valueOf(Utils.bit(h,i)))); + }*/ + // Saves ~0.4ms per key when running signing tests. + // TODO: are these bitflips the same for any hash function? + h[0] &= 248; + h[(b / 8) - 1] &= 63; + h[(b / 8) - 1] |= 64; + a = Arrays.copyOfRange(h, 0, b / 8); + + A = spec.getB().scalarMultiply(a); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unsupported hash algorithm"); + } + } + + /** + * Initialize directly from the hash. + * getSeed() will return null if this constructor is used. + * + * @param spec the parameter specification for this key + * @param h the private key + * @throws IllegalArgumentException if hash length is wrong + */ + public EdDSAPrivateKeySpec(EdDSAParameterSpec spec, byte[] h) { + if (h.length != spec.getCurve().getField().getb() / 4) + throw new IllegalArgumentException("hash length is wrong"); + + this.seed = null; + this.h = h; + this.spec = spec; + int b = spec.getCurve().getField().getb(); + + h[0] &= 248; + h[(b / 8) - 1] &= 63; + h[(b / 8) - 1] |= 64; + a = Arrays.copyOfRange(h, 0, b / 8); + + A = spec.getB().scalarMultiply(a); + } + + public EdDSAPrivateKeySpec(byte[] seed, byte[] h, byte[] a, GroupElement A, EdDSAParameterSpec spec) { + this.seed = seed; + this.h = h; + this.a = a; + this.A = A; + this.spec = spec; + } + + /** + * @return will be null if constructed directly from the private key + */ + public byte[] getSeed() { + return seed; + } + + /** + * @return the hash + */ + public byte[] getH() { + return h; + } + + /** + * @return the private key + */ + public byte[] geta() { + return a; + } + + /** + * @return the public key + */ + public GroupElement getA() { + return A; + } + + public EdDSAParameterSpec getParams() { + return spec; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAPublicKeySpec.java b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAPublicKeySpec.java new file mode 100644 index 0000000..d9fef30 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/EdDSAPublicKeySpec.java @@ -0,0 +1,46 @@ +package org.xbib.net.security.eddsa.spec; + +import org.xbib.net.security.eddsa.math.GroupElement; + +import java.security.spec.KeySpec; + +public class EdDSAPublicKeySpec implements KeySpec { + private final GroupElement A; + private final GroupElement Aneg; + private final EdDSAParameterSpec spec; + + /** + * @param pk the public key + * @param spec the parameter specification for this key + * @throws IllegalArgumentException if key length is wrong + */ + public EdDSAPublicKeySpec(byte[] pk, EdDSAParameterSpec spec) { + if (pk.length != spec.getCurve().getField().getb() / 8) + throw new IllegalArgumentException("public-key length is wrong"); + + this.A = new GroupElement(spec.getCurve(), pk); + // Precompute -A for use in verification. + this.Aneg = A.negate(); + Aneg.precompute(false); + this.spec = spec; + } + + public EdDSAPublicKeySpec(GroupElement A, EdDSAParameterSpec spec) { + this.A = A; + this.Aneg = A.negate(); + Aneg.precompute(false); + this.spec = spec; + } + + public GroupElement getA() { + return A; + } + + public GroupElement getNegativeA() { + return Aneg; + } + + public EdDSAParameterSpec getParams() { + return spec; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/eddsa/spec/package-info.java b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/package-info.java new file mode 100644 index 0000000..b43b45a --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/eddsa/spec/package-info.java @@ -0,0 +1,5 @@ +/** + * Specifications for curves and keys, and a table for named curves. + * Contains the following curves: Ed25519 + */ +package org.xbib.net.security.eddsa.spec; diff --git a/net-security/src/main/java/org/xbib/net/security/jaas/DummyLoginModule.java b/net-security/src/main/java/org/xbib/net/security/jaas/DummyLoginModule.java new file mode 100644 index 0000000..3dc14bd --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/jaas/DummyLoginModule.java @@ -0,0 +1,64 @@ +package org.xbib.net.security.jaas; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; +import java.io.IOException; +import java.util.Map; + +class DummyLoginModule implements LoginModule { + + private Subject subject; + private CallbackHandler callbackHandler; + + public DummyLoginModule() { + super(); + } + + public Subject getSubject() { + return subject; + } + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + } + + @Override + public boolean login() throws LoginException { + Callback[] callbacks = new Callback[2]; + callbacks[0] = new NameCallback("Username: "); + callbacks[1] = new PasswordCallback("Password: ", false); + try { + callbackHandler.handle(callbacks); + } catch (IOException ioe) { + throw new LoginException(ioe.getMessage()); + } catch (UnsupportedCallbackException uce) { + throw new LoginException(uce.getMessage() + " not available to obtain information from user"); + } + String user = ((NameCallback) callbacks[0]).getName(); + char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword(); + return user.equals(new String(tmpPassword)); + } + + @Override + public boolean commit() throws LoginException { + return true; + } + + @Override + public boolean abort() throws LoginException { + return true; + } + + @Override + public boolean logout() throws LoginException { + return true; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/jaas/PlainLoginModule.java b/net-security/src/main/java/org/xbib/net/security/jaas/PlainLoginModule.java new file mode 100644 index 0000000..b90a718 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/jaas/PlainLoginModule.java @@ -0,0 +1,109 @@ +package org.xbib.net.security.jaas; + +import java.io.IOException; +import java.security.Principal; +import java.util.Map; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +public class PlainLoginModule implements LoginModule { + + private CallbackHandler handler; + + private Subject subject; + + private String login; + + public PlainLoginModule() { + } + + /** + * This implementation always return false. + * + * @see javax.security.auth.spi.LoginModule#abort() + */ + @Override + public boolean abort() throws LoginException { + return false; + } + + /** + * This is where, should the entire authentication process succeeds, + * principal would be set. + * + * @see javax.security.auth.spi.LoginModule#commit() + */ + @Override + public boolean commit() throws LoginException { + + try { + PlainUserPrincipal user = new PlainUserPrincipal(login); + PlainRolePrincipal role = new PlainRolePrincipal("admin"); + subject.getPrincipals().add(user); + subject.getPrincipals().add(role); + return true; + } catch (Exception e) { + throw new LoginException(e.getMessage()); + } + } + + /** + * This implementation ignores both state and options. + * @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject, + * javax.security.auth.callback.CallbackHandler, java.util.Map, + * java.util.Map) + */ + @Override + public void initialize(Subject aSubject, CallbackHandler aCallbackHandler, Map aSharedState, Map aOptions) { + handler = aCallbackHandler; + subject = aSubject; + } + + /** + * This method checks whether the name and the password are the same. + * @see javax.security.auth.spi.LoginModule#login() + */ + @Override + public boolean login() throws LoginException { + Callback[] callbacks = new Callback[2]; + callbacks[0] = new NameCallback("login"); + callbacks[1] = new PasswordCallback("password", true); + try { + handler.handle(callbacks); + String name = ((NameCallback) callbacks[0]).getName(); + String password = String.valueOf(((PasswordCallback) callbacks[1]).getPassword()); + if (!name.equals(password)) { + throw new LoginException("Authentication failed"); + } + login = name; + return true; + } catch (IOException | UnsupportedCallbackException e) { + throw new LoginException(e.getMessage()); + } + } + + /** + * Clears subject from principal and credentials. + * + * @see javax.security.auth.spi.LoginModule#logout() + */ + @Override + public boolean logout() throws LoginException { + try { + Principal user = new PlainUserPrincipal(login); + Principal role = new PlainRolePrincipal("admin"); + subject.getPrincipals().remove(user); + subject.getPrincipals().remove(role); + return true; + } catch (Exception e) { + throw new LoginException(e.getMessage()); + } + } +} + diff --git a/net-security/src/main/java/org/xbib/net/security/jaas/PlainRolePrincipal.java b/net-security/src/main/java/org/xbib/net/security/jaas/PlainRolePrincipal.java new file mode 100644 index 0000000..5909d79 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/jaas/PlainRolePrincipal.java @@ -0,0 +1,34 @@ +package org.xbib.net.security.jaas; + +import java.security.Principal; + +public class PlainRolePrincipal implements Principal { + + String roleName; + + public PlainRolePrincipal(String name) { + roleName = name; + } + public String getName() { + return roleName; + } + + public String toString() { + return ("RolePrincipal: " + roleName); + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof PlainRolePrincipal) { + PlainRolePrincipal other = (PlainRolePrincipal) obj; + return roleName.equals(other.roleName); + } + return false; + } + + public int hashCode() { + return roleName.hashCode(); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/jaas/PlainUserPrincipal.java b/net-security/src/main/java/org/xbib/net/security/jaas/PlainUserPrincipal.java new file mode 100644 index 0000000..3af4998 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/jaas/PlainUserPrincipal.java @@ -0,0 +1,34 @@ +package org.xbib.net.security.jaas; + +import java.security.Principal; + +public class PlainUserPrincipal implements Principal { + + String UserName; + + public PlainUserPrincipal(String name) { + UserName = name; + } + public String getName() { + return UserName; + } + + public String toString() { + return ("UserPrincipal: " + UserName); + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof PlainUserPrincipal) { + PlainUserPrincipal other = (PlainUserPrincipal) obj; + return UserName.equals(other.UserName); + } + return false; + } + + public int hashCode() { + return UserName.hashCode(); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/Algorithm.java b/net-security/src/main/java/org/xbib/net/security/signatures/Algorithm.java new file mode 100644 index 0000000..794dae2 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/Algorithm.java @@ -0,0 +1,123 @@ +package org.xbib.net.security.signatures; + +import javax.crypto.Mac; +import java.util.HashMap; +import java.util.Map; + +/** + * The cryptographic algorithms for the HTTP signature. + */ +public enum Algorithm { + + // hmac + HMAC_SHA1("HmacSHA1", "hmac-sha1", Mac.class), + HMAC_SHA224("HmacSHA224", "hmac-sha224", Mac.class), + HMAC_SHA256("HmacSHA256", "hmac-sha256", Mac.class), + HMAC_SHA384("HmacSHA384", "hmac-sha384", Mac.class), + HMAC_SHA512("HmacSHA512", "hmac-sha512", Mac.class), + + // RSA PKCS#1 v1.5 signature + RSA_SHA1("SHA1withRSA", "rsa-sha1", java.security.Signature.class), + RSA_SHA256("SHA256withRSA", "rsa-sha256", java.security.Signature.class), + RSA_SHA384("SHA384withRSA", "rsa-sha384", java.security.Signature.class), + RSA_SHA512("SHA512withRSA", "rsa-sha512", java.security.Signature.class), + + RSA_SHA3_256("SHA3-256withRSA", "rsa-sha3-256", java.security.Signature.class), + RSA_SHA3_384("SHA3-384withRSA", "rsa-sha3-384", java.security.Signature.class), + RSA_SHA3_512("SHA3-512withRSA", "rsa-sha3-512", java.security.Signature.class), + + // RSA PSS signature + // This algorithm requires parameter. For example: + // new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1) + RSA_PSS("RSASSA-PSS", "rsassa-pss", java.security.Signature.class), + + // dsa + DSA_SHA1("SHA1withDSA", "dsa-sha1", java.security.Signature.class), + DSA_SHA224("SHA224withDSA", "dsa-sha224", java.security.Signature.class), + DSA_SHA256("SHA256withDSA", "dsa-sha256", java.security.Signature.class), + DSA_SHA384("SHA384withDSA", "dsa-sha384", java.security.Signature.class), + DSA_SHA512("SHA512withDSA", "dsa-sha512", java.security.Signature.class), + + // dsa with SHA3 + DSA_SHA3_256("SHA3-256withDSA", "dsa-sha3-256", java.security.Signature.class), + DSA_SHA3_384("SHA3-384withDSA", "dsa-sha3-384", java.security.Signature.class), + DSA_SHA3_512("SHA3-512withDSA", "dsa-sha3-512", java.security.Signature.class), + + // ecdsa + // The format of the Signature bytes for these algorithms is an ASN.1 encoded + // sequence as specified in RFC 3279 section 2.2.2. + ECDSA_SHA1("SHA1withECDSA", "ecdsa-sha1", java.security.Signature.class), + ECDSA_SHA256("SHA256withECDSA", "ecdsa-sha256", java.security.Signature.class), + ECDSA_SHA384("SHA384withECDSA", "ecdsa-sha384", java.security.Signature.class), + ECDSA_SHA512("SHA512withECDSA", "ecdsa-sha512", java.security.Signature.class), + + // ecdsa with SHA3 + ECDSA_SHA3_256("SHA3-256withECDSA", "ecdsa-sha3-256", java.security.Signature.class), + ECDSA_SHA3_384("SHA3-384withECDSA", "ecdsa-sha3-384", java.security.Signature.class), + ECDSA_SHA3_512("SHA3-512withECDSA", "ecdsa-sha3-512", java.security.Signature.class), + + // ecdsa in P1363 Format. + // The ECDSA signature algorithms as defined in ANSI X9.62 with an output as + // defined in IEEE P1363 format. + // The signature is the raw concatenation of r and s. + ECDSA_SHA256_P1363("SHA256withECDSAinP1363Format", "ecdsa-sha256-p1363", java.security.Signature.class), + ECDSA_SHA384_P1363("SHA384withECDSAinP1363Format", "ecdsa-sha384-p1363", java.security.Signature.class), + ECDSA_SHA512_P1363("SHA512withECDSAinP1363Format", "ecdsa-sha512-p1363", java.security.Signature.class), + ; + + private static final Map aliases = new HashMap<>(); + + static { + for (final Algorithm algorithm : Algorithm.values()) { + aliases.put(normalize(algorithm.getJvmName()), algorithm); + aliases.put(normalize(algorithm.getPortableName()), algorithm); + } + } + + private final String portableName; + private final String jvmName; + private final Class type; + + Algorithm(final String jvmName, final String portableName, final Class type) { + this.portableName = portableName; + this.jvmName = jvmName; + this.type = type; + } + + public static String toPortableName(final String name) { + return get(name).getPortableName(); + } + + public static String toJvmName(final String name) { + return get(name).getJvmName(); + } + + public static Algorithm get(final String name) { + final Algorithm algorithm = aliases.get(normalize(name)); + + if (algorithm != null) return algorithm; + + throw new UnsupportedAlgorithmException(name); + } + + private static String normalize(final String algorithm) { + return algorithm.replaceAll("[^A-Za-z0-9]+", "").toLowerCase(); + } + + public String getPortableName() { + return portableName; + } + + public String getJvmName() { + return jvmName; + } + + public Class getType() { + return type; + } + + @Override + public String toString() { + return getPortableName(); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/AuthenticationException.java b/net-security/src/main/java/org/xbib/net/security/signatures/AuthenticationException.java new file mode 100644 index 0000000..5028dca --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/AuthenticationException.java @@ -0,0 +1,16 @@ +package org.xbib.net.security.signatures; + +@SuppressWarnings("serial") +public class AuthenticationException extends RuntimeException { + + public AuthenticationException() { + } + + public AuthenticationException(final String message) { + super(message); + } + + public AuthenticationException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/Base64.java b/net-security/src/main/java/org/xbib/net/security/signatures/Base64.java new file mode 100644 index 0000000..675c6ab --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/Base64.java @@ -0,0 +1,316 @@ +package org.xbib.net.security.signatures; + +/** + * Provides Base64 encoding and decoding as defined by RFC 2045. + * + *

    This class implements section 6.8. Base64 Content-Transfer-Encoding + * from RFC 2045 Multipurpose Internet Mail Extensions (MIME) Part One: + * Format of Internet Message Bodies by Freed and Borenstein.

    + * + * @see RFC 2045 + */ +public class Base64 { + + /** + * Chunk size per RFC 2045 section 6.8. + * + *

    The {@value} character limit does not count the trailing CRLF, but counts + * all other characters, including any equal signs.

    + * + * @see RFC 2045 section 6.8 + */ + static final int CHUNK_SIZE = 76; + + /** + * Chunk separator per RFC 2045 section 2.1. + * + * @see RFC 2045 section 2.1 + */ + static final byte[] CHUNK_SEPARATOR = "\r\n".getBytes(); + + /** + * The base length. + */ + static final int BASELENGTH = 255; + + /** + * Lookup length. + */ + static final int LOOKUPLENGTH = 64; + + /** + * Used to calculate the number of bits in a byte. + */ + static final int EIGHTBIT = 8; + + /** + * Used when encoding something which has fewer than 24 bits. + */ + static final int SIXTEENBIT = 16; + + /** + * Used to determine how many bits data contains. + */ + static final int TWENTYFOURBITGROUP = 24; + + /** + * Used to get the number of Quadruples. + */ + static final int FOURBYTE = 4; + + /** + * Used to test the sign of a byte. + */ + static final int SIGN = -128; + + /** + * Byte used to pad output. + */ + static final byte PAD = (byte) '='; + + /** + * Contains the Base64 values 0 through 63 accessed by using character encodings as + * indices. + *

    + * For example, base64Alphabet['+'] returns 62. + *

    + *

    + * The value of undefined encodings is -1. + *

    + */ + private static final byte[] base64Alphabet = new byte[BASELENGTH]; + + /** + *

    + * Contains the Base64 encodings A through Z, followed by a through + * z, followed by 0 through 9, followed by +, and + * /. + *

    + *

    + * This array is accessed by using character values as indices. + *

    + *

    + * For example, lookUpBase64Alphabet[62] returns '+'. + *

    + */ + private static final byte[] lookUpBase64Alphabet = new byte[LOOKUPLENGTH]; + + static { + for (int i = 0; i < BASELENGTH; i++) { + base64Alphabet[i] = (byte) -1; + } + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + for (int i = 0; i <= 25; i++) { + lookUpBase64Alphabet[i] = (byte) ('A' + i); + } + for (int i = 26, j = 0; i <= 51; i++, j++) { + lookUpBase64Alphabet[i] = (byte) ('a' + j); + } + for (int i = 52, j = 0; i <= 61; i++, j++) { + lookUpBase64Alphabet[i] = (byte) ('0' + j); + } + lookUpBase64Alphabet[62] = (byte) '+'; + lookUpBase64Alphabet[63] = (byte) '/'; + } + + private Base64() { + } + + /** + * Encodes binary data using the base64 algorithm but + * does not chunk the output. + * + * @param binaryData binary data to encode + * @return Base64 characters + */ + public static byte[] encodeBase64(final byte[] binaryData) { + return encodeBase64(binaryData, false); + } + + /** + * Encodes binary data using the base64 algorithm, optionally + * chunking the output into 76 character blocks. + * + * @param binaryData Array containing binary data to encode. + * @param isChunked if true this encoder will chunk + * the base64 output into 76 character blocks + * @return Base64-encoded data. + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked) { + final int lengthDataBits = binaryData.length * EIGHTBIT; + final int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + final int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + byte[] encodedData; + int encodedDataLength; + int nbrChunks = 0; + if (fewerThan24bits != 0) { + encodedDataLength = (numberTriplets + 1) * 4; + } else { + encodedDataLength = numberTriplets * 4; + } + if (isChunked) { + nbrChunks = (CHUNK_SEPARATOR.length == 0 ? 0 : (int) Math.ceil((float) encodedDataLength / CHUNK_SIZE)); + encodedDataLength += nbrChunks * CHUNK_SEPARATOR.length; + } + encodedData = new byte[encodedDataLength]; + byte k; + byte l; + byte b1; + byte b2; + byte b3; + int encodedIndex = 0; + int dataIndex; + int i; + int nextSeparatorIndex = CHUNK_SIZE; + int chunksSoFar = 0; + for (i = 0; i < numberTriplets; i++) { + dataIndex = i * 3; + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + b3 = binaryData[dataIndex + 2]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + final byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + final byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + final byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + encodedData[encodedIndex] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex + 1] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex + 2] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex + 3] = lookUpBase64Alphabet[b3 & 0x3f]; + encodedIndex += 4; + if (isChunked) { + if (encodedIndex == nextSeparatorIndex) { + System.arraycopy(CHUNK_SEPARATOR, 0, encodedData, encodedIndex, CHUNK_SEPARATOR.length); + chunksSoFar++; + nextSeparatorIndex = (CHUNK_SIZE * (chunksSoFar + 1)) + (chunksSoFar * CHUNK_SEPARATOR.length); + encodedIndex += CHUNK_SEPARATOR.length; + } + } + } + dataIndex = i * 3; + if (fewerThan24bits == EIGHTBIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + final byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex + 1] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex + 2] = PAD; + encodedData[encodedIndex + 3] = PAD; + } else if (fewerThan24bits == SIXTEENBIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + final byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + final byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + encodedData[encodedIndex] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex + 1] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex + 2] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex + 3] = PAD; + } + if (isChunked) { + if (chunksSoFar < nbrChunks) { + System.arraycopy(CHUNK_SEPARATOR, 0, encodedData, encodedDataLength - CHUNK_SEPARATOR.length, CHUNK_SEPARATOR.length); + } + } + return encodedData; + } + + /** + * Decodes Base64 data into octects + * + * @param base64Data Byte array containing Base64 data + * @return Array containing decoded data. + */ + public static byte[] decodeBase64(byte[] base64Data) { + base64Data = discardNonBase64(base64Data); + if (base64Data.length == 0) { + return new byte[0]; + } + final int numberQuadruple = base64Data.length / FOURBYTE; + byte[] decodedData; + byte b1; + byte b2; + byte b3; + byte b4; + byte marker0; + byte marker1; + int encodedIndex = 0; + int dataIndex; + { + int lastData = base64Data.length; + while (base64Data[lastData - 1] == PAD) { + if (--lastData == 0) { + return new byte[0]; + } + } + decodedData = new byte[lastData - numberQuadruple]; + } + for (int i = 0; i < numberQuadruple; i++) { + dataIndex = i * 4; + marker0 = base64Data[dataIndex + 2]; + marker1 = base64Data[dataIndex + 3]; + b1 = base64Alphabet[base64Data[dataIndex]]; + b2 = base64Alphabet[base64Data[dataIndex + 1]]; + if (marker0 != PAD && marker1 != PAD) { + b3 = base64Alphabet[marker0]; + b4 = base64Alphabet[marker1]; + decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex + 1] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex + 2] = (byte) (b3 << 6 | b4); + } else if (marker0 == PAD) { + decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + } else { + b3 = base64Alphabet[marker0]; + decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex + 1] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + } + encodedIndex += 3; + } + return decodedData; + } + + /** + * Discards any characters outside of the base64 alphabet, per + * the requirements on page 25 of RFC 2045 - "Any characters + * outside of the base64 alphabet are to be ignored in base64 + * encoded data." + * + * @param data The base-64 encoded data to groom + * @return The data, less non-base64 characters (see RFC 2045). + */ + private static byte[] discardNonBase64(final byte[] data) { + final byte[] groomedData = new byte[data.length]; + int bytesCopied = 0; + for (byte datum : data) { + if (isBase64(datum)) { + groomedData[bytesCopied++] = datum; + } + } + final byte[] packedData = new byte[bytesCopied]; + System.arraycopy(groomedData, 0, packedData, 0, bytesCopied); + return packedData; + } + + /** + * Returns whether or not the octect is in the base 64 alphabet. + * + * @param octect The value to test + * @return true if the value is defined in the the base 64 alphabet, false otherwise. + */ + private static boolean isBase64(final byte octect) { + if (octect == PAD) { + return true; + } else return octect >= 0 && base64Alphabet[octect] != -1; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/EC.java b/net-security/src/main/java/org/xbib/net/security/signatures/EC.java new file mode 100644 index 0000000..c53cfc2 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/EC.java @@ -0,0 +1,58 @@ +package org.xbib.net.security.signatures; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * Utility for Elliptic Curve keys. + */ +public class EC { + + private static final String EC = "EC"; + + private static final String SUNEC = "SunEC"; // Sun's ECC provider + + private EC() { + } + + /** + * Returns a private key constructed from the given DER bytes in PKCS#8 format. + * + * @param pkcs8 DER bytes in PKCS#8 format + * @return Private key + * @throws InvalidKeySpecException if the DER bytes cannot be converted to a private key + */ + public static PrivateKey privateKeyFromPKCS8(final byte[] pkcs8) throws InvalidKeySpecException { + try { + final EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(pkcs8); + final KeyFactory keyFactory = KeyFactory.getInstance(EC, SUNEC); + return keyFactory.generatePrivate(privateKeySpec); + } catch (final NoSuchAlgorithmException | NoSuchProviderException e) { + throw new IllegalStateException(e); + } + } + + /** + * Returns a public key constructed from the given DER bytes. + * + * @param derBytes DER bytes + * @return Public key + * @throws InvalidKeySpecException if the DER bytes cannot be converted to a public key + */ + public static PublicKey publicKeyFrom(final byte[] derBytes) throws InvalidKeySpecException { + try { + final KeyFactory keyFactory = KeyFactory.getInstance(EC, SUNEC); + final EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(derBytes); + return keyFactory.generatePublic(publicKeySpec); + } catch (final NoSuchAlgorithmException | NoSuchProviderException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/InvalidCreatedFieldException.java b/net-security/src/main/java/org/xbib/net/security/signatures/InvalidCreatedFieldException.java new file mode 100644 index 0000000..d510d78 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/InvalidCreatedFieldException.java @@ -0,0 +1,8 @@ +package org.xbib.net.security.signatures; + +@SuppressWarnings("serial") +public class InvalidCreatedFieldException extends AuthenticationException { + public InvalidCreatedFieldException(final String message) { + super(message); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/InvalidExpiresFieldException.java b/net-security/src/main/java/org/xbib/net/security/signatures/InvalidExpiresFieldException.java new file mode 100644 index 0000000..f72399e --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/InvalidExpiresFieldException.java @@ -0,0 +1,8 @@ +package org.xbib.net.security.signatures; + +@SuppressWarnings("serial") +public class InvalidExpiresFieldException extends AuthenticationException { + public InvalidExpiresFieldException(final String message) { + super(message); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/Joiner.java b/net-security/src/main/java/org/xbib/net/security/signatures/Joiner.java new file mode 100644 index 0000000..2b948c4 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/Joiner.java @@ -0,0 +1,31 @@ +package org.xbib.net.security.signatures; + +import java.util.Collection; + +public class Joiner { + + private Joiner() { + } + + public static String join(final String delimiter, final Collection collection) { + if (collection.size() == 0) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + for (final Object obj : collection) { + sb.append(obj).append(delimiter); + } + return sb.substring(0, sb.length() - delimiter.length()); + } + + public static String join(final String delimiter, final Object... collection) { + if (collection.length == 0) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + for (final Object obj : collection) { + sb.append(obj).append(delimiter); + } + return sb.substring(0, sb.length() - delimiter.length()); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/MissingAlgorithmException.java b/net-security/src/main/java/org/xbib/net/security/signatures/MissingAlgorithmException.java new file mode 100644 index 0000000..098dbf2 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/MissingAlgorithmException.java @@ -0,0 +1,8 @@ +package org.xbib.net.security.signatures; + +@SuppressWarnings("serial") +public class MissingAlgorithmException extends AuthenticationException { + + public MissingAlgorithmException() { + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/MissingKeyIdException.java b/net-security/src/main/java/org/xbib/net/security/signatures/MissingKeyIdException.java new file mode 100644 index 0000000..d37371c --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/MissingKeyIdException.java @@ -0,0 +1,8 @@ +package org.xbib.net.security.signatures; + +@SuppressWarnings("serial") +public class MissingKeyIdException extends AuthenticationException { + + public MissingKeyIdException() { + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/MissingRequiredHeaderException.java b/net-security/src/main/java/org/xbib/net/security/signatures/MissingRequiredHeaderException.java new file mode 100644 index 0000000..f77e2f0 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/MissingRequiredHeaderException.java @@ -0,0 +1,10 @@ +package org.xbib.net.security.signatures; + +@SuppressWarnings("serial") +public class MissingRequiredHeaderException extends AuthenticationException { + + public MissingRequiredHeaderException(final String key) { + super(key); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/MissingSignatureException.java b/net-security/src/main/java/org/xbib/net/security/signatures/MissingSignatureException.java new file mode 100644 index 0000000..5cdefd3 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/MissingSignatureException.java @@ -0,0 +1,8 @@ +package org.xbib.net.security.signatures; + +@SuppressWarnings("serial") +public class MissingSignatureException extends AuthenticationException { + + public MissingSignatureException() { + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/PEM.java b/net-security/src/main/java/org/xbib/net/security/signatures/PEM.java new file mode 100644 index 0000000..7f33aa6 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/PEM.java @@ -0,0 +1,189 @@ +package org.xbib.net.security.signatures; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.List; + +/** + * A PEM utility that can be used to read keys from PEM. With this PEM utility, + * private keys in either PKCS#1 or PKCS#8 PEM encoded format can be read + * without the need to depend on the Bouncy Castle library. + *

    + * Some background information: + *

      + *
    • Interestingly, the creation of a CloudFront Key Pair via the AWS console + * would result in a private key in PKCS#1 PEM format.
    • + *
    • Unfortunately, the JDK doesn't provide a means to load PEM key encoded in + * PKCS#1 without adding the Bouncy Castle to the classpath. The JDK can only + * load PEM key encoded in PKCS#8 encoding.
    • + *
    • One the other hand, one can use openssl to convert a PEM file from PKCS#1 + * to PKCS#8. Example: + * + *
      + * openssl pkcs8 -topk8 -in pk-APKAJM22QV32R3I2XVIQ.pem -inform pem -out pk-APKAJM22QV32R3I2XVIQ_pk8.pem  -outform pem -nocrypt
      + * 
      + * + *
    • + *
    + */ +public enum PEM { + ; + private static final String BEGIN_MARKER = "-----BEGIN "; + + /** + * Returns the first private key that is found from the input stream of a + * PEM file. + * + * @param is Inputstream to read a private key from + * @return the first PrivateKey found in the stream + * @throws InvalidKeySpecException if failed to convert the DER bytes into a private key. + * @throws IllegalArgumentException if no private key is found. + * @throws IOException if an IO exception occurs while reading the stream + */ + public static PrivateKey readPrivateKey(final InputStream is) throws InvalidKeySpecException, IOException { + final List objects = readPEMObjects(is); + for (final PEMObject object : objects) { + switch (object.getPEMObjectType()) { + case PRIVATE_KEY_PKCS1: + return RSA.privateKeyFromPKCS1(object.getDerBytes()); + case PRIVATE_EC_KEY_PKCS8: + return EC.privateKeyFromPKCS8(object.getDerBytes()); + case PRIVATE_KEY_PKCS8: + try { + return RSA.privateKeyFromPKCS8(object.getDerBytes()); + } catch (final InvalidKeySpecException e) { + return EC.privateKeyFromPKCS8(object.getDerBytes()); + } + default: + break; + } + } + throw new IllegalArgumentException("Found no private key"); + } + + /** + * Returns the first public key that is found from the input stream of a PEM + * file. + * + * @param is The Input stream to read + * @return the first PublicKey found in the stream + * @throws InvalidKeySpecException if failed to convert the DER bytes into a public key. + * @throws IllegalArgumentException if no public key is found. + * @throws IOException if an IO exception occurs while reading the stream + */ + public static PublicKey readPublicKey(final InputStream is) throws InvalidKeySpecException, IOException { + for (final PEMObject object : readPEMObjects(is)) { + if (object.getPEMObjectType() == PEMObjectType.PUBLIC_KEY_X509) { + try { + return RSA.publicKeyFrom(object.getDerBytes()); + } catch (final InvalidKeySpecException e) { + return EC.publicKeyFrom(object.getDerBytes()); + } + } + } + throw new IllegalArgumentException("Found no public key"); + } + + /** + * A lower level API used to returns all PEM objects that can be read off + * from the input stream of a PEM file. + *

    + * This method can be useful if more than one PEM object of different types + * are embedded in the same PEM file. + */ + private static List readPEMObjects(final InputStream is) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { + final List pemContents = new ArrayList<>(); + boolean readingContent = false; + String beginMarker = null; + String endMarker = null; + StringBuilder sb = null; + String line; + while ((line = reader.readLine()) != null) { + if (readingContent) { + if (line.contains(endMarker)) { + pemContents.add(new PEMObject(beginMarker, Base64.decodeBase64(sb.toString().getBytes(StandardCharsets.US_ASCII)))); + readingContent = false; + } else { + sb.append(line.trim()); + } + } else { + if (line.contains(BEGIN_MARKER)) { + readingContent = true; + beginMarker = line.trim(); + endMarker = beginMarker.replace("BEGIN", "END"); + sb = new StringBuilder(); + } + } + } + return pemContents; + } + } + + /** + * The type of a specific PEM object in a PEM file. + *

    + * A PEM file can contain one or multiple PEM objects, each with a beginning + * and ending marker. + */ + enum PEMObjectType { + PRIVATE_KEY_PKCS1("-----BEGIN RSA PRIVATE KEY-----"), + PRIVATE_EC_KEY_PKCS8("-----BEGIN EC PRIVATE KEY-----"), // RFC-5915 + PRIVATE_KEY_PKCS8("-----BEGIN PRIVATE KEY-----"), + PUBLIC_KEY_X509("-----BEGIN PUBLIC KEY-----"), + CERTIFICATE_X509("-----BEGIN CERTIFICATE-----"); + private final String beginMarker; + + PEMObjectType(final String beginMarker) { + this.beginMarker = beginMarker; + } + + public static PEMObjectType fromBeginMarker(final String beginMarker) { + for (final PEMObjectType e : PEMObjectType.values()) { + if (e.getBeginMarker().equals(beginMarker)) { + return e; + } + } + return null; + } + + public String getBeginMarker() { + return beginMarker; + } + } + + /** + * A PEM object in a PEM file. + *

    + * A PEM file can contain one or multiple PEM objects, each with a beginning + * and ending marker. + */ + static class PEMObject { + private final String beginMarker; + private final byte[] derBytes; + + public PEMObject(final String beginMarker, final byte[] derBytes) { + this.beginMarker = beginMarker; + this.derBytes = derBytes.clone(); + } + + public String getBeginMarker() { + return beginMarker; + } + + public byte[] getDerBytes() { + return derBytes.clone(); + } + + public PEMObjectType getPEMObjectType() { + return PEMObjectType.fromBeginMarker(beginMarker); + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/RSA.java b/net-security/src/main/java/org/xbib/net/security/signatures/RSA.java new file mode 100644 index 0000000..3acac6d --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/RSA.java @@ -0,0 +1,404 @@ +package org.xbib.net.security.signatures; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.X509EncodedKeySpec; + +public enum RSA { + ; + + private static final String RSA = "RSA"; + + /** + * Returns a private key constructed from the given DER bytes in PKCS#8 format. + * + * @param pkcs8 DER bytes in PKCS#8 format + * @return Private Key + * @throws InvalidKeySpecException if DER bytes cannot be converted to a private key + */ + public static PrivateKey privateKeyFromPKCS8(final byte[] pkcs8) throws InvalidKeySpecException { + try { + final EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(pkcs8); + final KeyFactory keyFactory = KeyFactory.getInstance(RSA); + return keyFactory.generatePrivate(privateKeySpec); + } catch (final NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + /** + * Returns a private key constructed from the given DER bytes in PKCS#1 format. + * + * @param pkcs1 DER bytes in PKCS#1 format + * @return Private Key + * @throws InvalidKeySpecException if DER bytes cannot be converted to a private key + */ + public static PrivateKey privateKeyFromPKCS1(final byte[] pkcs1) throws InvalidKeySpecException { + try { + final RSAPrivateCrtKeySpec privateKeySpec = newRSAPrivateCrtKeySpec(pkcs1); + final KeyFactory keyFactory = KeyFactory.getInstance(RSA); + return keyFactory.generatePrivate(privateKeySpec); + } catch (final IOException e) { + throw new IllegalArgumentException(e); + } catch (final NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + /** + * Returns a public key constructed from the given DER bytes. + * + * @param derBytes DER bytes to use to construct a public key + * @return Public Key + * @throws InvalidKeySpecException if the DER bytes cannot be converted to a public key + */ + public static PublicKey publicKeyFrom(final byte[] derBytes) throws InvalidKeySpecException { + try { + final KeyFactory keyFactory = KeyFactory.getInstance(RSA); + final EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(derBytes); + return keyFactory.generatePublic(publicKeySpec); + } catch (final NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + // Extracted from: + // http://oauth.googlecode.com/svn/code/branches/jmeter/jmeter/src/main/java/org/apache/jmeter/protocol/oauth/sampler/PrivateKeyReader.java + // See p.41 of http://www.emc.com/emc-plus/rsa-labs/pkcs/files/h11300-wp-pkcs-1v2-2-rsa-cryptography-standard.pdf + + /**************************************************************************** + * Amazon Modifications: Copyright 2014 Amazon.com, Inc. or its affiliates. + * All Rights Reserved. + ***************************************************************************** + * Copyright (c) 1998-2010 AOL Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + **************************************************************************** + * Convert PKCS#1 encoded private key into RSAPrivateCrtKeySpec. + * + *

    The ASN.1 syntax for the private key with CRT is + * + *

    +     * -- 
    +     * -- Representation of RSA private key with information for the CRT algorithm.
    +     * --
    +     * RSAPrivateKey ::= SEQUENCE {
    +     *   version           Version, 
    +     *   modulus           INTEGER,  -- n
    +     *   publicExponent    INTEGER,  -- e
    +     *   privateExponent   INTEGER,  -- d
    +     *   prime1            INTEGER,  -- p
    +     *   prime2            INTEGER,  -- q
    +     *   exponent1         INTEGER,  -- d mod (p-1)
    +     *   exponent2         INTEGER,  -- d mod (q-1) 
    +     *   coefficient       INTEGER,  -- (inverse of q) mod p
    +     *   otherPrimeInfos   OtherPrimeInfos OPTIONAL 
    +     * }
    +     * 
    + * + * @param keyInPkcs1 PKCS#1 encoded key + * @throws IOException + */ + private static RSAPrivateCrtKeySpec newRSAPrivateCrtKeySpec(final byte[] keyInPkcs1) throws IOException { + final DerParser parser = new DerParser(keyInPkcs1); + + final Asn1Object sequence = parser.read(); + if (sequence.getType() != DerParser.SEQUENCE) { + throw new IllegalArgumentException("Invalid DER: not a sequence"); + } + + // Parse inside the sequence + final DerParser p = sequence.getParser(); + + p.read(); // Skip version + final BigInteger modulus = p.read().getInteger(); + final BigInteger publicExp = p.read().getInteger(); + final BigInteger privateExp = p.read().getInteger(); + final BigInteger prime1 = p.read().getInteger(); + final BigInteger prime2 = p.read().getInteger(); + final BigInteger exp1 = p.read().getInteger(); + final BigInteger exp2 = p.read().getInteger(); + final BigInteger crtCoef = p.read().getInteger(); + + return new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crtCoef); + } + + /** + * An ASN.1 TLV. The object is not parsed. It can + * only handle integers and strings. + */ + static class Asn1Object { + protected final int type; + protected final int length; + protected final byte[] value; + protected final int tag; + + /** + * Construct a ASN.1 TLV. The TLV could be either a + * constructed or primitive entity. + *

    + *

    The first byte in DER encoding is made of following fields, + *

    +         * -------------------------------------------------
    +         * |Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1|
    +         * -------------------------------------------------
    +         * |  Class    | CF  |     +      Type             |
    +         * -------------------------------------------------
    +         * 
    + *
      + *
    • Class: Universal, Application, Context or Private + *
    • CF: Constructed flag. If 1, the field is constructed. + *
    • Type: This is actually called tag in ASN.1. It + * indicates data type (Integer, String) or a construct + * (sequence, choice, set). + *
    + * + * @param tag Tag or Identifier + * @param length Length of the field + * @param value Encoded octet string for the field. + */ + public Asn1Object(final int tag, final int length, final byte[] value) { + this.tag = tag; + this.type = tag & 0x1F; + this.length = length; + this.value = value; + } + + public int getType() { + return type; + } + + public int getLength() { + return length; + } + + public byte[] getValue() { + return value; + } + + public boolean isConstructed() { + return (tag & DerParser.CONSTRUCTED) == DerParser.CONSTRUCTED; + } + + /** + * For constructed field, return a parser for its content. + * + * @return A parser for the construct. + */ + public DerParser getParser() throws IOException { + if (!isConstructed()) { + throw new IOException("Invalid DER: can't parse primitive entity"); + } + + return new DerParser(value); + } + + /** + * Get the value as integer + */ + public BigInteger getInteger() throws IOException { + if (type != DerParser.INTEGER) { + throw new IOException("Invalid DER: object is not integer"); + } + + return new BigInteger(value); + } + + /** + * Get value as string. Most strings are treated + * as Latin-1. + */ + public String getString() throws IOException { + switch (type) { + + // Not all are Latin-1 but it's the closest thing + case DerParser.NUMERIC_STRING: + case DerParser.PRINTABLE_STRING: + case DerParser.VIDEOTEX_STRING: + case DerParser.IA5_STRING: + case DerParser.GRAPHIC_STRING: + case DerParser.ISO646_STRING: + case DerParser.GENERAL_STRING: + return new String(value, StandardCharsets.ISO_8859_1); + + case DerParser.BMP_STRING: + return new String(value, StandardCharsets.UTF_16BE); + + case DerParser.UTF8_STRING: + return new String(value, StandardCharsets.UTF_8); + + case DerParser.UNIVERSAL_STRING: + throw new IOException("Invalid DER: can't handle UCS-4 string"); + + default: + throw new IOException("Invalid DER: object is not a string"); + } + } + } + + /** + * A bare-minimum ASN.1 DER decoder, just having enough functions to decode + * PKCS#1 private keys. Especially, it doesn't handle explicitly tagged types + * with an outer tag. + *

    + *

    + * This parser can only handle one layer. To parse nested constructs, get a new + * parser for each layer using Asn1Object.getParser(). + *

    + *

    + * There are many DER decoders in JRE but using them will tie this program to a + * specific JCE/JVM. + */ + static class DerParser { + // Classes + public final static int UNIVERSAL = 0x00; + public final static int APPLICATION = 0x40; + public final static int CONTEXT = 0x80; + public final static int PRIVATE = 0xC0; + + // Constructed Flag + public final static int CONSTRUCTED = 0x20; + + // Tag and data types + public final static int ANY = 0x00; + public final static int BOOLEAN = 0x01; + public final static int INTEGER = 0x02; + public final static int BIT_STRING = 0x03; + public final static int OCTET_STRING = 0x04; + public final static int NULL = 0x05; + public final static int OBJECT_IDENTIFIER = 0x06; + public final static int REAL = 0x09; + public final static int ENUMERATED = 0x0a; + public final static int RELATIVE_OID = 0x0d; + + public final static int SEQUENCE = 0x10; + public final static int SET = 0x11; + + public final static int NUMERIC_STRING = 0x12; + public final static int PRINTABLE_STRING = 0x13; + public final static int T61_STRING = 0x14; + public final static int VIDEOTEX_STRING = 0x15; + public final static int IA5_STRING = 0x16; + public final static int GRAPHIC_STRING = 0x19; + public final static int ISO646_STRING = 0x1A; + public final static int GENERAL_STRING = 0x1B; + + public final static int UTF8_STRING = 0x0C; + public final static int UNIVERSAL_STRING = 0x1C; + public final static int BMP_STRING = 0x1E; + + public final static int UTC_TIME = 0x17; + public final static int GENERALIZED_TIME = 0x18; + + protected final InputStream in; + + /** + * Create a new DER decoder from an input stream. + * + * @param in The DER encoded stream + */ + public DerParser(final InputStream in) throws IOException { + this.in = in; + } + + /** + * Create a new DER decoder from a byte array. + * + * @param bytes the encoded bytes + */ + public DerParser(final byte[] bytes) throws IOException { + this(new ByteArrayInputStream(bytes)); + } + + /** + * Read next object. If it's constructed, the value holds encoded content + * and it should be parsed by a new parser from + * Asn1Object.getParser. + */ + public Asn1Object read() throws IOException { + final int tag = in.read(); + + if (tag == -1) { + throw new IOException("Invalid DER: stream too short, missing tag"); + } + + final int length = getLength(); + + final byte[] value = new byte[length]; + final int n = in.read(value); + + if (n < length) { + throw new IOException("Invalid DER: stream too short, missing value"); + } + + return new Asn1Object(tag, length, value); + } + + /** + * Decode the length of the field. Can only support length encoding up to 4 + * octets. + *

    + *

    + * In BER/DER encoding, length can be encoded in 2 forms, + *

      + *
    • Short form. One octet. Bit 8 has value "0" and bits 7-1 give the + * length. + *
    • Long form. Two to 127 octets (only 4 is supported here). Bit 8 of + * first octet has value "1" and bits 7-1 give the number of additional + * length octets. Second and following octets give the length, base 256, + * most significant digit first. + *
    + * + * @return The length as integer + */ + private int getLength() throws IOException { + + final int i = in.read(); + + if (i == -1) { + throw new IOException("Invalid DER: length missing"); + } + + // A single byte short length + if ((i & ~0x7F) == 0) return i; + + final int num = i & 0x7F; + + // We can't handle length longer than 4 bytes + if (i >= 0xFF || num > 4) { + throw new IOException("Invalid DER: length field too big (" + i + ")"); + } + + final byte[] bytes = new byte[num]; + final int n = in.read(bytes); + + if (n < num) { + throw new IOException("Invalid DER: length too short"); + } + + return new BigInteger(1, bytes).intValue(); + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/Signature.java b/net-security/src/main/java/org/xbib/net/security/signatures/Signature.java new file mode 100644 index 0000000..5419807 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/Signature.java @@ -0,0 +1,528 @@ +package org.xbib.net.security.signatures; + +import java.security.spec.AlgorithmParameterSpec; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Signature { + + /** + * Regular expression pattern for fields present in the Authorization field. + * Fields value may be double-quoted strings, e.g. algorithm="hs2019" + * Some fields may be numerical values without double-quotes, e.g. created=123456 + */ + private static final Pattern RFC_2617_PARAM = Pattern + .compile("(?\\w+)=((\"(?[^\"]*)\")|(?\\d+\\.?\\d*))"); + /** + * The maximum time skew between the client and the server. + * This is used to validate the (created) and (expires) fields in the HTTP signature. + */ + public static long maxTimeSkewInMilliseconds = 30 * 1000L; + /** + * REQUIRED. The `keyId` field is an opaque string that the server can + * use to look up the component they need to validate the signature. It + * could be an SSH key fingerprint, a URL to machine-readable key data, + * an LDAP DN, etc. Management of keys and assignment of `keyId` is out + * of scope for this document. + */ + private final String keyId; + /** + * RECOMMENDED. The `signingAlgorithm` parameter is used to specify the digital + * signature algorithm to use when generating the signature. Valid + * values for this parameter can be found in the Signature Algorithms + * registry located at http://www.iana.org/assignments/signature- + * algorithms and MUST NOT be marked "deprecated". + *

    + * Verifiers MUST determine the signature's Algorithm from the keyId parameter + * rather than from algorithm. If algorithm is provided and differs from or + * is incompatible with the algorithm or key material identified by keyId + * (for example, algorithm has a value of rsa-sha256 but keyId identifies + * an EdDSA key), then implementations MUST produce an error. + *

    + * https://datatracker.ietf.org/doc/draft-ietf-httpbis-message-signatures/ + */ + private final SigningAlgorithm signingAlgorithm; + /** + * REQUIRED. The `algorithm` parameter is used to specify the digital + * signature algorithm to use when generating the signature. Valid + * values for this parameter can be found in the Signature Algorithms + * registry located at http://www.iana.org/assignments/signature- + * algorithms and MUST NOT be marked "deprecated". + */ + private final Algorithm algorithm; + /** + * REQUIRED. The `signature` parameter is a base 64 encoded digital + * signature, as described in RFC 4648 [RFC4648], Section 4 [4]. The + * client uses the `algorithm` and `headers` signature parameters to + * form a canonicalized `signing string`. This `signing string` is then + * signed with the key associated with `keyId` and the algorithm + * corresponding to `algorithm`. The `signature` parameter is then set + * to the base 64 encoding of the signature. + *

    + * Signing: this field is calculated from the input data. + * Verification: this field is parsed from the signature field in the + * Authorization header. + */ + private final String signature; + /** + * OPTIONAL. The `headers` parameter is used to specify the list of + * HTTP headers included when generating the signature for the message. + * If specified, it should be a lowercased, quoted list of HTTP header + * fields, separated by a single space character. If not specified, + * implementations MUST operate as if the field were specified with a + * single value, the `Date` header, in the list of HTTP headers. Note + * that the list order is important, and MUST be specified in the order + * the HTTP header field-value pairs are concatenated together during + * signing. + */ + private final List headers; + /** + * OPTIONAL. The `parameterSpec` is used to specify the cryptographic + * parameters. Some cryptographic algorithm such as RSASSA-PSS + * require parameters. + */ + private final AlgorithmParameterSpec parameterSpec; + /** + * OPTIONAL. The configurable signature's maximum validation duration, in milliseconds. + * This field is applicable when the signed headers include '(expires)'. + * In that case, the value of the '(expires)' field is calculated by adding + * maxSignatureValidityDuration to the timestamp of the signature creation time. + *

    + * This field is used to derive the signature expiration time when a cryptographic signature + * is generated. + */ + private final Long maxSignatureValidityDuration; + /** + * The signature's Creation Time, in milliseconds since the epoch. + *

    + * This field is set at the time the cryptographic signature is generated, or when + * the 'Authorization' header is parsed. + */ + private final Long signatureCreatedTime; + /** + * The signature's Expiration Time, in milliseconds since the epoch. + *

    + * This field is set at the time the cryptographic signature is generated, or when + * the 'Authorization' header is parsed. + */ + private final Long signatureExpiresTime; + + /** + * Construct a signature configuration instance with the specified keyId, algorithm and HTTP headers. + * + * @param keyId An opaque string that the server can use to look up the component they need to validate the signature. + * @param signingAlgorithm An identifier for the HTTP Signature algorithm. + * This should be "hs2019" except for legacy applications that use an older version of the draft HTTP signature specification. + * @param algorithm The detailed algorithm used to sign the message. + * @param parameterSpec optional cryptographic parameters for the signature. + * @param headers The list of HTTP headers that will be used in the signature. + */ + public Signature(final String keyId, final String signingAlgorithm, final String algorithm, final AlgorithmParameterSpec parameterSpec, final List headers) { + this(keyId, getSigningAlgorithm(signingAlgorithm), getAlgorithm(algorithm), parameterSpec, null, headers); + } + + public Signature(final String keyId, final String signingAlgorithm, final String algorithm, + final AlgorithmParameterSpec parameterSpec, final String signature, final List headers) { + this(keyId, getSigningAlgorithm(signingAlgorithm), getAlgorithm(algorithm), parameterSpec, signature, headers); + } + + public Signature(final String keyId, final SigningAlgorithm signingAlgorithm, final Algorithm algorithm, + final AlgorithmParameterSpec parameterSpec, final String signature, final List headers) { + this(keyId, signingAlgorithm, algorithm, parameterSpec, signature, headers, null); + } + + public Signature(final String keyId, final SigningAlgorithm signingAlgorithm, final Algorithm algorithm, + final AlgorithmParameterSpec parameterSpec, final String signature, + final List headers, final Long maxSignatureValidityDuration) { + this(keyId, signingAlgorithm, algorithm, parameterSpec, signature, headers, maxSignatureValidityDuration, null, null); + } + + public Signature(final String keyId, final SigningAlgorithm signingAlgorithm, final Algorithm algorithm, + final AlgorithmParameterSpec parameterSpec, final String signature, + final List headers, final Long maxSignatureValidityDuration, + final Long signatureCreatedTime, final Long signatureExpiresTime) { + if (keyId == null || keyId.trim().isEmpty()) { + throw new IllegalArgumentException("keyId is required."); + } + if (algorithm == null) { + throw new IllegalArgumentException("algorithm is required."); + } + if (signingAlgorithm != null && + signingAlgorithm.getSupportedAlgorithms() != null && + !signingAlgorithm.getSupportedAlgorithms().contains(algorithm)) { + throw new IllegalArgumentException("Signing algorithm " + signingAlgorithm.getAlgorithmName() + + " is not compatible with " + algorithm.getPortableName()); + } + + this.keyId = keyId; + this.signingAlgorithm = signingAlgorithm; + this.algorithm = algorithm; + if (maxSignatureValidityDuration != null && maxSignatureValidityDuration <= 0) { + throw new IllegalArgumentException("Signature max validity must be a positive number"); + } + this.maxSignatureValidityDuration = maxSignatureValidityDuration; + this.signatureCreatedTime = signatureCreatedTime; + this.signatureExpiresTime = signatureExpiresTime; + this.signature = signature; + this.parameterSpec = parameterSpec; + if (headers == null || headers.size() == 0) { + this.headers = List.of("date"); + } else { + this.headers = Collections.unmodifiableList(lowercase(headers)); + } + } + + private static Algorithm getAlgorithm(final String algorithm) { + if (algorithm == null) throw new IllegalArgumentException("Algorithm cannot be null"); + return Algorithm.get(algorithm); + } + + private static SigningAlgorithm getSigningAlgorithm(final String scheme) { + if (scheme == null) { + throw new IllegalArgumentException("Signing scheme cannot be null"); + } + return SigningAlgorithm.get(scheme); + } + + /** + * Constructs a Signature object by parsing the 'Authorization' header. + *

    + * As stated in the HTTP signature specification, the value of the algorithm parameter in + * the 'Authorization' header should be set to generic identifier. The detailed algorithm + * should be derived from the keyId. Hence it is not possible to determine the detailed + * algorithm by inspecting the signature data. + * + * @param authorization The value of the HTTP 'Authorization' header containing the signature data. + * @param algorithm The detailed cryptographic algorithm for the HTTP signature. + * @return The Signature object. + */ + public static Signature fromString(String authorization, final Algorithm algorithm) { + /* + * A HTTP signature field value in the authorization header. + */ + class FieldValue { + /** + * The field value. It may be a string or number. + */ + private final Object value; + /** + * A flag indicating whether the field is a string or number. + */ + private final boolean isNumber; + + FieldValue(final String value, final boolean isNumber) throws ParseException { + this.isNumber = isNumber; + if (isNumber) { + this.value = NumberFormat.getInstance().parse(value); + } else { + this.value = value; + } + } + + /** Returns true if the field is a string */ + boolean isString() { + return !isNumber; + } + + /** Returns true if the field is a number */ + boolean isNumber() { + return isNumber; + } + + /** Returns true if the field is an integer value */ + boolean isInteger() { + return value instanceof Long; + } + + /** Returns the field as a string, or null if the field is not a string. */ + String getValueAsString() { + if (!isString()) return null; + return (String) value; + } + + /** Returns the field as a long value, or null if the field is not a integer number. */ + Long getValueAsLong() { + if (!isInteger()) return null; + return ((Number) value).longValue(); + } + + /** Returns the field as a double value, or null if the field is not a number. */ + Double getValueAsDouble() { + if (!isNumber()) return null; + return ((Number) value).doubleValue(); + } + } + + try { + authorization = normalize(authorization); + + final Map map = new HashMap<>(); + + final Matcher matcher = RFC_2617_PARAM.matcher(authorization); + while (matcher.find()) { + final String key = matcher.group("key").toLowerCase(); + boolean isNumber = false; + String value = matcher.group("stringValue"); + if (value == null) { + value = matcher.group("numberValue"); + isNumber = true; + } + map.put(key, new FieldValue(value, isNumber)); + } + + final List headers = new ArrayList(); + FieldValue fieldValue = map.get("headers"); + if (fieldValue != null) { + if (!fieldValue.isString()) { + throw new IllegalArgumentException("headers field must be a double-quoted string"); + } + Collections.addAll(headers, fieldValue.getValueAsString().toLowerCase().split(" +")); + } + String keyid = null; + fieldValue = map.get("keyid"); + if (fieldValue != null && fieldValue.isString()) { + keyid = fieldValue.getValueAsString(); + } + if (keyid == null) { + throw new MissingKeyIdException(); + } + String algorithmField = null; + fieldValue = map.get("algorithm"); + if (fieldValue != null && fieldValue.isString()) { + algorithmField = fieldValue.getValueAsString(); + } + if (algorithmField == null) { + throw new MissingAlgorithmException(); + } + String signature = null; + fieldValue = map.get("signature"); + if (fieldValue != null && fieldValue.isString()) { + signature = fieldValue.getValueAsString(); + } + if (signature == null) { + throw new MissingSignatureException(); + } + Long created = null; + fieldValue = map.get("created"); + if (fieldValue != null) { + if (!fieldValue.isInteger()) { + throw new InvalidCreatedFieldException("Field must be an integer value"); + } + created = fieldValue.getValueAsLong() * 1000L; + } + Long expires = null; // The signature expiration time, in milliseconds since the epoch. + fieldValue = map.get("expires"); + if (fieldValue != null) { + if (!fieldValue.isNumber()) { + throw new InvalidExpiresFieldException("Field must be a number"); + } + expires = (long) (fieldValue.getValueAsDouble() * 1000L); + } + SigningAlgorithm parsedSigningAlgorithm = null; + try { + parsedSigningAlgorithm = SigningAlgorithm.get(algorithmField); + } catch (final UnsupportedAlgorithmException ex) { + // This may happen for older implementations that pass the serialize the detailed + // algorithm instead of using 'hs2019'. In that case, the value of 'algorithm' + // should be one of the supported values in the Algorithm enum. If not, an + // exception is raised. + } + Algorithm parsedAlgorithm = null; + try { + parsedAlgorithm = Algorithm.get(algorithmField); + if (algorithm != null && parsedAlgorithm.getPortableName() != algorithm.getPortableName()) { + throw new IllegalArgumentException("The algorithm does not match the value of the 'Authorization' header."); + } + } catch (final UnsupportedAlgorithmException ex) { + // This is expected for new conformant implementations that set the algorithm + // field in the 'Authorization' header to 'hs2019'. The algorithm must be + // derived from the keyId. The client is responsible for maintaining the + // mapping between the keyId and the detailed cryptographic algorithm. + if (algorithm == null) { + throw new IllegalArgumentException("The algorithm is required."); + } + parsedAlgorithm = algorithm; + } + final Signature s = new Signature(keyid, parsedSigningAlgorithm, parsedAlgorithm, null, signature, headers, null, created, expires); + s.verifySignatureValidityDates(); + return s; + + } catch (final AuthenticationException e) { + throw e; + } catch (final Throwable e) { + throw new UnparsableSignatureException(authorization, e); + } + } + + public static Signature fromString(String authorization) { + return fromString(authorization, null); + } + + private static String normalize(String authorization) { + final String start = "signature "; + final String prefix = authorization.substring(0, start.length()).toLowerCase(); + if (prefix.equals(start)) { + authorization = authorization.substring(start.length()); + } + return authorization.trim(); + } + + /** + * Returns the signature creation time. + * + * @return the signature creation time. + */ + public Date getSignatureCreation() { + if (signatureCreatedTime == null) return null; + return new Date(signatureCreatedTime); + } + + /** + * Returns the signature creation time in milliseconds since the epoch. + * + * @return the signature creation time in milliseconds since the epoch. + */ + public Long getSignatureCreationTimeMilliseconds() { + return signatureCreatedTime; + } + + /** + * Returns the signature max validity duration, in milliseconds. + * + * @return the signature max validity duration, in milliseconds. + */ + public Long getSignatureMaxValidityMilliseconds() { + return maxSignatureValidityDuration; + } + + /** + * Returns the signature expiration time. + * + * @return the signature expiration time. + */ + public Date getSignatureExpiration() { + if (signatureExpiresTime == null) return null; + return new Date(signatureExpiresTime); + } + + /** + * Returns the signature expiration time in milliseconds since the epoch. + * + * @return the signature expiration time in milliseconds since the epoch. + */ + public Long getSignatureExpirationTimeMilliseconds() { + return signatureExpiresTime; + } + + private List lowercase(final List headers) { + final List list = new ArrayList(headers.size()); + for (final String header : headers) { + list.add(header.toLowerCase()); + } + return list; + } + + public String getKeyId() { + return keyId; + } + + /** + * Returns the detailed implementation algorithm for HTTP signatures. + * + * @return the cryptographic algorithm. + */ + public Algorithm getAlgorithm() { + return algorithm; + } + + /** + * Returns the identifier for the HTTP Signature Algorithm, as registered + * in the HTTP Signature Algorithms Registry. + * + * @return the identifier for the HTTP Signature Algorithm. + */ + public SigningAlgorithm getSigningAlgorithm() { + return signingAlgorithm; + } + + /** + * Returns the base-64 encoded value of the signature. + * + * @return the base-64 encoded value of the signature. + */ + public String getSignature() { + return signature; + } + + /** + * Returns the specification of cryptographic parameters. + * + * @return specification of cryptographic parameters. + */ + public AlgorithmParameterSpec getParameterSpec() { + return parameterSpec; + } + + public List getHeaders() { + return headers; + } + + /** + * Verify the signature is valid with regards to the (created) and (expires) fields. + *

    + * When the '(created)' field is present in the HTTP signature, the '(created)' field + * represents the date when the signature has been created. + * When the '(expires)' field is present in the HTTP signature, the '(expires)' field + * represents the date when the signature expires. + */ + public void verifySignatureValidityDates() { + if (signatureCreatedTime != null && signatureCreatedTime > System.currentTimeMillis() + maxTimeSkewInMilliseconds) { + throw new InvalidCreatedFieldException("Signature is not valid yet"); + } + if (signatureExpiresTime != null && signatureExpiresTime < System.currentTimeMillis()) { + throw new InvalidExpiresFieldException("Signature has expired"); + } + } + + @Override + public String toString() { + return toString("Signature"); + } + + /** + * Returns the formatted signature parameters without any "Signature " prefix + */ + public String toParamString() { + return toString(null); + } + + public String toString(final String prefix) { + if (SigningAlgorithm.HS2019.equals(signingAlgorithm)) { + // When the signing algorithm is set to 'hs2019', the value of the algorithm + // field must be set to 'hs2019'. The specific crypto algorithm is not + // serialized in the 'Authorization' header, the server must derive the value + // from the keyId. + return (prefix != null ? prefix + " " : "") + + "keyId=\"" + keyId + '\"' + + (signatureCreatedTime != null ? String.format(",created=%d", signatureCreatedTime / 1000L) : "") + + (signatureExpiresTime != null ? String.format(",expires=%.3f", signatureExpiresTime / 1000.0) : "") + + ",algorithm=\"" + signingAlgorithm + '\"' + + ",headers=\"" + Joiner.join(" ", headers) + '\"' + + ",signature=\"" + signature + '\"'; + } else { + return (prefix != null ? prefix + " " : "") + + "keyId=\"" + keyId + '\"' + + ",algorithm=\"" + algorithm + '\"' + + ",headers=\"" + Joiner.join(" ", headers) + '\"' + + ",signature=\"" + signature + '\"'; + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/Signatures.java b/net-security/src/main/java/org/xbib/net/security/signatures/Signatures.java new file mode 100644 index 0000000..072f0ed --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/Signatures.java @@ -0,0 +1,99 @@ +package org.xbib.net.security.signatures; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public enum Signatures { + ; + + /** + * Create a canonicalized string representation of the HTTP request. It is used + * as the input to calculate the signature of the HTTP request. + * + * @param required The list of headers that should be included in the HTTP signature. + * @param method The HTTP method. + * @param uri The HTTP request URI. + * @param headers A map of header names to header values. + */ + public static String createSigningString(final List required, final String method, final String uri, final Map headers) { + return createSigningString(required, method, uri, headers, null, null); + } + + /** + * Create a canonicalized string representation of the HTTP request. It is used + * as the input to calculate the signature of the HTTP request. + *

    + * The provided method, path and query values are used to generate the optional + * (request-target) field. + * + * @param required The list of headers that should be included in the HTTP signature. + * @param method The HTTP method. + * @param uri The path and query of the request target of the message. + * The value must already be encoded exactly as it will be sent in the + * request line of the HTTP message. No URL encoding is performed by this method. + * @param headers A map of header names to header values. + * @param signatureCreationTime The signature creation time in milliseconds since the epoch. + * @param signatureExpiryTime The signature expiration time in milliseconds since the epoch. + */ + public static String createSigningString(final List required, String method, final String uri, Map headers, + final Long signatureCreationTime, final Long signatureExpiryTime) { + headers = lowercase(headers); + + final List list = new ArrayList(required.size()); + + for (final String key : required) { + if ("(request-target)".equals(key)) { + method = lowercase(method); + list.add(Joiner.join(" ", "(request-target):", method, uri)); + } else if ("(created)".equals(key)) { + // The "created" parameter contains the signature's Creation Time. + // This parameter is useful when signers are not capable of controlling + // the "Date" HTTP Header such as when operating in certain web + // browser environments. + // Its canonicalized value is an Integer String containing the + // signature's Creation Time expressed as the number of seconds since + // the Epoch + if (signatureCreationTime == null) { + throw new InvalidCreatedFieldException("(created) field requested but signature creation time is not set"); + } + list.add(key + ": " + TimeUnit.MILLISECONDS.toSeconds(signatureCreationTime)); + } else if ("(expires)".equals(key)) { + // The "expires" parameter contains the signature's Expiration Time. + // If the signature does not have an Expiration Time, this parameter "MUST" + // be omitted. If not specified, the signature's Expiration Time is + // undefined. + // Its canonicalized value is a Decimal String containing the + // signature's Expiration Time expressed as the number of seconds since + // the Epoch. + if (signatureExpiryTime == null) { + throw new InvalidExpiresFieldException("(expires) field requested but signature expiration time is not set"); + } + final double expires = signatureExpiryTime / 1000.0; + list.add(key + ": " + String.format("%.3f", expires)); + } else { + final String value = headers.get(key); + if (value == null) throw new MissingRequiredHeaderException(key); + + list.add(key + ": " + value); + } + } + + return Joiner.join("\n", list); + } + + private static Map lowercase(final Map headers) { + final Map map = new HashMap(); + for (final Map.Entry entry : headers.entrySet()) { + map.put(entry.getKey().toLowerCase(), entry.getValue()); + } + + return map; + } + + private static String lowercase(final String spec) { + return spec.toLowerCase(); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/Signer.java b/net-security/src/main/java/org/xbib/net/security/signatures/Signer.java new file mode 100644 index 0000000..93a9573 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/Signer.java @@ -0,0 +1,171 @@ +package org.xbib.net.security.signatures; + +import javax.crypto.Mac; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Provider; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +/** + * It is an intentional part of the design that the same Signer instance + * can be reused on several HTTP Messages in a multi-threaded fashion + * + *

    + * The supplied Signature instance will be used as the basis for all + * future signatures created from this Signer. + * + *

    + * Each call to 'sign' will emit a Signature with the same 'keyId', + * 'algorithm', 'headers' but a newly calculated 'signature' + */ +public class Signer { + + private final Sign sign; + + private final Signature signature; + + private final Algorithm algorithm; + + private final Provider provider; + + public Signer(final Key key, final Signature signature) { + this(key, signature, null); + } + + public Signer(final Key key, final Signature signature, final Provider provider) { + requireNonNull(key, "Key cannot be null"); + this.signature = requireNonNull(signature, "Signature cannot be null"); + this.algorithm = signature.getAlgorithm(); + this.provider = provider; + if (java.security.Signature.class.equals(algorithm.getType())) { + this.sign = new Asymmetric((PrivateKey) key); + } else if (Mac.class.equals(algorithm.getType())) { + this.sign = new Symmetric(key); + } else { + throw new UnsupportedAlgorithmException(String.format("Unknown Algorithm type %s %s", algorithm.getPortableName(), algorithm.getType().getName())); + } + try { + sign.sign("validation".getBytes()); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new IllegalStateException("Can't initialise the Signer using the provided algorithm and key", e); + } + } + + /** + * Create and return a HTTP signature object. + * + * @param method The HTTP method. + * @param uri The path and query of the request target of the message. + * The value must already be encoded exactly as it will be sent in the + * request line of the HTTP message. No URL encoding is performed by this method. + * @param headers The HTTP headers. + * @return a Signature object containing the signed message. + */ + public Signature sign(final String method, final String uri, final Map headers) throws IOException { + final Long created = System.currentTimeMillis(); + Long expires = signature.getSignatureMaxValidityMilliseconds(); + if (expires != null) { + expires += created; + } + final String signingString = createSigningString(method, uri, headers, created, expires); + final byte[] binarySignature = sign.sign(signingString.getBytes(StandardCharsets.UTF_8)); + final byte[] encoded = Base64.encodeBase64(binarySignature); + final String signedAndEncodedString = new String(encoded, StandardCharsets.UTF_8); + return new Signature(signature.getKeyId(), signature.getSigningAlgorithm(), + signature.getAlgorithm(), signature.getParameterSpec(), + signedAndEncodedString, signature.getHeaders(), null, created, expires); + } + + /** + * Create and return the string which is used as input for the cryptographic signature. + * + * @param method The HTTP method. + * @param uri The path and query of the request target of the message. + * The value must already be encoded exactly as it will be sent in the + * request line of the HTTP message. No URL encoding is performed by this method. + * @param headers The HTTP headers. + * @param created The time when the signature is created. + * @param expires The time when the signature expires. + * @return The signing string. + * @throws IOException when an exception occurs while creating the signing string. + */ + public String createSigningString(final String method, final String uri, final Map headers, + final Long created, final Long expires) throws IOException { + return Signatures.createSigningString(signature.getHeaders(), method, uri, headers, created, expires); + } + + /** + * Create and return the string which is used as input for the cryptographic signature. + * + * @param method The HTTP method. + * @param uri The URI path and query parameters. + * @param headers The HTTP headers. + * @return The signing string. + * @throws IOException when an exception occurs while creating the signing string. + */ + public String createSigningString(final String method, final String uri, final Map headers) throws IOException { + return Signatures.createSigningString(signature.getHeaders(), method, uri, headers, + signature.getSignatureCreationTimeMilliseconds(), signature.getSignatureExpirationTimeMilliseconds()); + } + + private interface Sign { + byte[] sign(byte[] signingStringBytes); + } + + private class Asymmetric implements Sign { + + private final PrivateKey key; + + private Asymmetric(final PrivateKey key) { + this.key = key; + } + + @Override + public byte[] sign(final byte[] signingStringBytes) { + try { + final java.security.Signature instance = provider == null ? + java.security.Signature.getInstance(algorithm.getJvmName()) : + java.security.Signature.getInstance(algorithm.getJvmName(), provider); + if (signature.getParameterSpec() != null) { + instance.setParameter(signature.getParameterSpec()); + } + instance.initSign(key); + instance.update(signingStringBytes); + return instance.sign(); + } catch (final NoSuchAlgorithmException e) { + throw new UnsupportedAlgorithmException(algorithm.getJvmName()); + } catch (final Exception e) { + throw new IllegalStateException(e); + } + } + } + + private class Symmetric implements Sign { + + private final Key key; + + private Symmetric(final Key key) { + this.key = key; + } + + @Override + public byte[] sign(final byte[] signingStringBytes) { + try { + final Mac mac = provider == null ? Mac.getInstance(algorithm.getJvmName()) : Mac.getInstance(algorithm.getJvmName(), provider); + mac.init(key); + return mac.doFinal(signingStringBytes); + } catch (final NoSuchAlgorithmException e) { + throw new UnsupportedAlgorithmException(algorithm.getJvmName()); + } catch (final Exception e) { + throw new IllegalStateException(e); + } + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/SigningAlgorithm.java b/net-security/src/main/java/org/xbib/net/security/signatures/SigningAlgorithm.java new file mode 100644 index 0000000..1e8e001 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/SigningAlgorithm.java @@ -0,0 +1,98 @@ +package org.xbib.net.security.signatures; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The algorithm parameter contains the name of the signature's Algorithm, + * as registered in the HTTP Signature Algorithms Registry defined by this document. + *

    + * The signature verification is based on the signature's algorithm from the keyId + * parameter rather than from this algorithm. + * If algorithm is provided and differs from or is incompatible with the algorithm + * or key material identified by keyId (for example, algorithm has a value of + * rsa-sha256 but keyId identifies an EdDSA key), then a verification exception is + * raised. + *

    + * The default value for this parameter should be "hs2019". + * + * @see https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-00.html + */ +public enum SigningAlgorithm { + + /** + * The actual cryptographic algorithm is derived from metadata associated + * with keyId. + *

    + * Recommend support for: + * RSASSA-PSS [RFC8017] using SHA-512 [RFC6234] + * HMAC [RFC2104] using SHA-512 [RFC6234] + * ECDSA using curve P-256 [DSS] and SHA-512 [RFC6234] + * Ed25519ph, Ed25519ctx, and Ed25519 [RFC8032] + */ + HS2019("hs2019", null), + + // Deprecated, SHA-1 is not secure. + RSA_SHA1("rsa-sha1", new HashSet<>(List.of(Algorithm.RSA_SHA1))), + RSA_SHA256("rsa-sha256", new HashSet<>(List.of(Algorithm.RSA_SHA256))), + ECDSA_SHA256("ecdsa-sha256", new HashSet<>(List.of(Algorithm.ECDSA_SHA256))), + HMAC_SHA256("hmac-sha256", new HashSet<>(List.of(Algorithm.HMAC_SHA256))), + ; + + private static final Map aliases = new HashMap<>(); + + static { + for (final SigningAlgorithm algorithmName : SigningAlgorithm.values()) { + aliases.put(algorithmName.getAlgorithmName(), algorithmName); + } + } + + /** + * An identifier for the HTTP Signature Algorithm. + * The name MUST be an ASCII string consisting only of lower-case characters ("a" - "z"), + * digits ("0" - "9"), and hyphens ("-"), and SHOULD NOT exceed 20 characters in length. + * The identifier MUST be unique within the context of the registry. + */ + private final String algorithmName; + private final Set supportedAlgorithms; + + SigningAlgorithm(final String algorithmName, final Set supportedAlgorithms) { + this.algorithmName = algorithmName; + if (supportedAlgorithms != null) { + this.supportedAlgorithms = Collections.unmodifiableSet(supportedAlgorithms); + } else { + this.supportedAlgorithms = null; + } + } + + /** + * Returns the SigningAlgorithm with the specified name. + * + * @param name the name of the signing algorithm. + * @return the SigningAlgorithm + */ + public static SigningAlgorithm get(final String name) { + final SigningAlgorithm algorithmName = aliases.get(name); + if (algorithmName != null) { + return algorithmName; + } + throw new UnsupportedAlgorithmException(name); + } + + public String getAlgorithmName() { + return algorithmName; + } + + public Set getSupportedAlgorithms() { + return this.supportedAlgorithms; + } + + @Override + public String toString() { + return getAlgorithmName(); + } +} \ No newline at end of file diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/UnparsableSignatureException.java b/net-security/src/main/java/org/xbib/net/security/signatures/UnparsableSignatureException.java new file mode 100644 index 0000000..3aa98d1 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/UnparsableSignatureException.java @@ -0,0 +1,9 @@ +package org.xbib.net.security.signatures; + +@SuppressWarnings("serial") +public class UnparsableSignatureException extends AuthenticationException { + + public UnparsableSignatureException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/UnsupportedAlgorithmException.java b/net-security/src/main/java/org/xbib/net/security/signatures/UnsupportedAlgorithmException.java new file mode 100644 index 0000000..dadb9f2 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/UnsupportedAlgorithmException.java @@ -0,0 +1,13 @@ +package org.xbib.net.security.signatures; + +@SuppressWarnings("serial") +public class UnsupportedAlgorithmException extends AuthenticationException { + + public UnsupportedAlgorithmException(final String message) { + super(message); + } + + public UnsupportedAlgorithmException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/signatures/Verifier.java b/net-security/src/main/java/org/xbib/net/security/signatures/Verifier.java new file mode 100644 index 0000000..ca6c8ec --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/signatures/Verifier.java @@ -0,0 +1,126 @@ +package org.xbib.net.security.signatures; + +import javax.crypto.Mac; +import java.io.IOException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SignatureException; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +/** + * A new instance of the Verifier class needs to be created for each signature. + */ +public class Verifier { + + private final Verify verify; + + private final Signature signature; + + private final Algorithm algorithm; + + private final Provider provider; + + /** + * Constructs a verifier object with the specified key and signature object. + * + * @param key The key used to verify the signature. + * @param signature The signature object. + */ + public Verifier(final Key key, final Signature signature) { + this(key, signature, null); + } + + public Verifier(final Key key, final Signature signature, final Provider provider) { + requireNonNull(key, "Key cannot be null"); + this.signature = requireNonNull(signature, "Signature cannot be null"); + this.algorithm = signature.getAlgorithm(); + this.provider = provider; + if (java.security.Signature.class.equals(algorithm.getType())) { + this.verify = new Asymmetric((PublicKey) key); + } else if (Mac.class.equals(algorithm.getType())) { + this.verify = new Symmetric(key); + } else { + throw new UnsupportedAlgorithmException(String.format("Unknown Algorithm type %s %s", algorithm.getPortableName(), algorithm.getType().getName())); + } + try { + verify.verify("validation".getBytes()); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new IllegalStateException("Can't initialise the Signer using the provided algorithm and key", e); + } + } + + public boolean verify(final String method, final String uri, final Map headers) throws IOException, NoSuchAlgorithmException, SignatureException { + signature.verifySignatureValidityDates(); + final String signingString = createSigningString(method, uri, headers); + return verify.verify(signingString.getBytes()); + } + + public String createSigningString(final String method, final String uri, final Map headers) throws IOException { + return Signatures.createSigningString(signature.getHeaders(), method, uri, headers, + signature.getSignatureCreationTimeMilliseconds(), + signature.getSignatureExpirationTimeMilliseconds()); + } + + private interface Verify { + boolean verify(byte[] signingStringBytes); + } + + private class Asymmetric implements Verify { + + private final PublicKey key; + + private Asymmetric(final PublicKey key) { + this.key = key; + } + + @Override + public boolean verify(final byte[] signingStringBytes) { + try { + final java.security.Signature instance = provider == null ? + java.security.Signature.getInstance(algorithm.getJvmName()) : + java.security.Signature.getInstance(algorithm.getJvmName(), provider); + if (signature.getParameterSpec() != null) { + instance.setParameter(signature.getParameterSpec()); + } + instance.initVerify(key); + instance.update(signingStringBytes); + return instance.verify(Base64.decodeBase64(signature.getSignature().getBytes())); + } catch (final NoSuchAlgorithmException e) { + throw new UnsupportedAlgorithmException(algorithm.getJvmName()); + } catch (final Exception e) { + throw new IllegalStateException(e); + } + } + } + + private class Symmetric implements Verify { + + private final Key key; + + private Symmetric(final Key key) { + this.key = key; + } + + @Override + public boolean verify(final byte[] signingStringBytes) { + try { + final Mac mac = provider == null ? Mac.getInstance(algorithm.getJvmName()) : Mac.getInstance(algorithm.getJvmName(), provider); + mac.init(key); + final byte[] hash = mac.doFinal(signingStringBytes); + final byte[] encoded = Base64.encodeBase64(hash); + return MessageDigest.isEqual(encoded, signature.getSignature().getBytes()); + } catch (final NoSuchAlgorithmException e) { + throw new UnsupportedAlgorithmException(algorithm.getJvmName()); + } catch (final Exception e) { + throw new IllegalStateException(e); + } + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/SSLFactory.java b/net-security/src/main/java/org/xbib/net/security/ssl/SSLFactory.java new file mode 100644 index 0000000..9c62997 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/SSLFactory.java @@ -0,0 +1,802 @@ +package org.xbib.net.security.ssl; + +import org.xbib.net.security.ssl.exception.GenericKeyStoreException; +import org.xbib.net.security.ssl.exception.GenericSecurityException; +import org.xbib.net.security.ssl.model.KeyStoreHolder; +import org.xbib.net.security.ssl.model.SSLMaterial; +import org.xbib.net.security.ssl.trustmanager.ChainAndAuthTypeValidator; +import org.xbib.net.security.ssl.trustmanager.ChainAndAuthTypeWithSSLEngineValidator; +import org.xbib.net.security.ssl.trustmanager.ChainAndAuthTypeWithSocketValidator; +import org.xbib.net.security.ssl.trustmanager.TrustAnchorTrustOptions; +import org.xbib.net.security.ssl.trustmanager.TrustStoreTrustOptions; +import org.xbib.net.security.ssl.util.HostnameVerifierUtils; +import org.xbib.net.security.ssl.util.KeyManagerUtils; +import org.xbib.net.security.ssl.util.KeyStoreUtils; +import org.xbib.net.security.ssl.util.SSLContextUtils; +import org.xbib.net.security.ssl.util.SSLParametersUtils; +import org.xbib.net.security.ssl.util.SSLSessionUtils; +import org.xbib.net.security.ssl.util.SSLSocketUtils; +import org.xbib.net.security.ssl.util.StringUtils; +import org.xbib.net.security.ssl.util.TrustManagerUtils; +import org.xbib.net.security.ssl.util.UriUtils; +import org.xbib.net.security.ssl.util.ValidationUtils; + +import javax.net.ssl.CertPathTrustManagerParameters; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.Key; +import java.security.KeyStore; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +public final class SSLFactory { + + private final SSLMaterial sslMaterial; + + private SSLFactory(SSLMaterial sslMaterial) { + this.sslMaterial = sslMaterial; + } + + public SSLContext getSslContext() { + return sslMaterial.getSslContext(); + } + + public SSLSocketFactory getSslSocketFactory() { + return SSLSocketUtils.createSslSocketFactory(sslMaterial.getSslContext(), getSslParameters()); + } + + public SSLServerSocketFactory getSslServerSocketFactory() { + return SSLSocketUtils.createSslServerSocketFactory(sslMaterial.getSslContext(), getSslParameters()); + } + + public Optional getKeyManager() { + return Optional.ofNullable(sslMaterial.getKeyManager()); + } + + public Optional getKeyManagerFactory() { + return getKeyManager().map(KeyManagerUtils::createKeyManagerFactory); + } + + public Optional getTrustManager() { + return Optional.ofNullable(sslMaterial.getTrustManager()); + } + + public Optional getTrustManagerFactory() { + return getTrustManager().map(TrustManagerUtils::createTrustManagerFactory); + } + + public List getTrustedCertificates() { + return getTrustManager() + .map(X509ExtendedTrustManager::getAcceptedIssuers) + .map(Arrays::asList) + .map(Collections::unmodifiableList) + .orElseGet(Collections::emptyList); + } + + public HostnameVerifier getHostnameVerifier() { + return sslMaterial.getHostnameVerifier(); + } + + public List getCiphers() { + return sslMaterial.getCiphers(); + } + + public List getProtocols() { + return sslMaterial.getProtocols(); + } + + public SSLParameters getSslParameters() { + return SSLParametersUtils.copy(sslMaterial.getSslParameters()); + } + + public SSLEngine getSSLEngine() { + return getSSLEngine(null, null); + } + + public SSLEngine getSSLEngine(String peerHost, Integer peerPort) { + SSLEngine sslEngine; + if (nonNull(peerHost) && nonNull(peerPort)) { + sslEngine = sslMaterial.getSslContext().createSSLEngine(peerHost, peerPort); + } else { + sslEngine = sslMaterial.getSslContext().createSSLEngine(); + } + + sslEngine.setSSLParameters(getSslParameters()); + return sslEngine; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private static final String TRUST_STORE_VALIDATION_EXCEPTION_MESSAGE = "TrustStore details are empty, which are required to be present when SSL/TLS is enabled"; + private static final String IDENTITY_VALIDATION_EXCEPTION_MESSAGE = "Identity details are empty, which are required to be present when SSL/TLS is enabled"; + private static final String IDENTITY_AND_TRUST_MATERIAL_VALIDATION_EXCEPTION_MESSAGE = "Could not create instance of SSLFactory because Identity " + + "and Trust material are not present. Please provide at least a Trust material."; + private static final String CERTIFICATE_VALIDATION_EXCEPTION_MESSAGE = "Failed to load the certificate(s). No certificate has been provided."; + private static final String SYSTEM_PROPERTY_VALIDATION_EXCEPTION_MESSAGE = "Failed to load the System property for [%s] because it does not contain any value"; + + private String sslContextAlgorithm = "TLS"; + private Provider securityProvider = null; + private String securityProviderName = null; + private SecureRandom secureRandom = null; + private HostnameVerifier hostnameVerifier = HostnameVerifierUtils.createBasic(); + + private final List identities = new ArrayList<>(); + private final List trustStores = new ArrayList<>(); + private final List identityManagers = new ArrayList<>(); + private final List trustManagers = new ArrayList<>(); + private final SSLParameters sslParameters = new SSLParameters(); + private final Map> preferredAliasToHost = new HashMap<>(); + private final List protocols = new ArrayList<>(); + private final List ciphers = new ArrayList<>(); + + private boolean swappableKeyManagerEnabled = false; + private boolean swappableTrustManagerEnabled = false; + + private int sessionTimeoutInSeconds = -1; + private int sessionCacheSizeInBytes = -1; + + private ChainAndAuthTypeValidator chainAndAuthTypeValidator = null; + private ChainAndAuthTypeWithSocketValidator chainAndAuthTypeWithSocketValidator = null; + private ChainAndAuthTypeWithSSLEngineValidator chainAndAuthTypeWithSSLEngineValidator = null; + + private Builder() {} + + public Builder withSystemTrustMaterial() { + TrustManagerUtils.createTrustManagerWithSystemTrustedCertificates().ifPresent(trustManagers::add); + return this; + } + + public Builder withDefaultTrustMaterial() { + trustManagers.add(TrustManagerUtils.createTrustManagerWithJdkTrustedCertificates()); + return this; + } + + public Builder withSystemPropertyDerivedTrustMaterial() { + return withSystemPropertyDerivedMaterial( + "javax.net.ssl.trustStore", + "javax.net.ssl.trustStorePassword", + "javax.net.ssl.trustStoreType", + "javax.net.ssl.trustStoreProvider", + this::withTrustMaterial + ); + } + + /** + * A shorter method for using the unsafe trust material + * + * @see Builder#withTrustingAllCertificatesWithoutValidation() + * @return {@link Builder} + */ + public Builder withUnsafeTrustMaterial() { + return withTrustingAllCertificatesWithoutValidation(); + } + + public Builder withDummyTrustMaterial() { + trustManagers.add(TrustManagerUtils.createDummyTrustManager()); + return this; + } + + /** + * Enables the possibility to swap the underlying TrustManager at runtime. + * After this option has been enabled the TrustManager can be swapped + * with {@link TrustManagerUtils#swapTrustManager(X509TrustManager, X509TrustManager) TrustManagerUtils#swapTrustManager(swappableTrustManager, newTrustManager)} + * + * @return {@link Builder} + */ + public Builder withSwappableTrustMaterial() { + swappableTrustManagerEnabled = true; + return this; + } + + public Builder withTrustMaterial(T trustManager) { + trustManagers.add(TrustManagerUtils.wrapIfNeeded(trustManager)); + return this; + } + + public Builder withTrustMaterial(T managerFactoryParameters) { + trustManagers.add(TrustManagerUtils.createTrustManager(managerFactoryParameters)); + return this; + } + + public Builder withTrustMaterial(T trustManager, + TrustStoreTrustOptions trustOptions) { + + KeyStore trustStore = KeyStoreUtils.createTrustStore(Arrays.asList(trustManager.getAcceptedIssuers())); + return withTrustMaterial(trustStore, trustOptions); + } + + public Builder withTrustMaterial(T trustManagerFactory) { + X509ExtendedTrustManager trustManager = TrustManagerUtils.getTrustManager(trustManagerFactory); + this.trustManagers.add(trustManager); + return this; + } + + public Builder withTrustMaterial(String trustStorePath, char[] trustStorePassword) { + return withTrustMaterial(trustStorePath, trustStorePassword, KeyStore.getDefaultType()); + } + + public Builder withTrustMaterial(String trustStorePath, + char[] trustStorePassword, + TrustStoreTrustOptions trustOptions) { + + return withTrustMaterial(trustStorePath, trustStorePassword, KeyStore.getDefaultType(), trustOptions); + } + + public Builder withTrustMaterial(String trustStorePath, char[] trustStorePassword, String trustStoreType) { + if (StringUtils.isBlank(trustStorePath) || StringUtils.isBlank(trustStoreType)) { + throw new GenericKeyStoreException(TRUST_STORE_VALIDATION_EXCEPTION_MESSAGE); + } + + KeyStore trustStore = KeyStoreUtils.loadKeyStore(trustStorePath, trustStorePassword, trustStoreType); + trustStores.add(trustStore); + + return this; + } + + public Builder withTrustMaterial(String trustStorePath, + char[] trustStorePassword, + String trustStoreType, + TrustStoreTrustOptions trustOptions) { + + if (StringUtils.isBlank(trustStorePath) || StringUtils.isBlank(trustStoreType)) { + throw new GenericKeyStoreException(TRUST_STORE_VALIDATION_EXCEPTION_MESSAGE); + } + + KeyStore trustStore = KeyStoreUtils.loadKeyStore(trustStorePath, trustStorePassword, trustStoreType); + return withTrustMaterial(trustStore, trustOptions); + } + + public Builder withTrustMaterial(Path trustStorePath, char[] trustStorePassword) { + return withTrustMaterial(trustStorePath, trustStorePassword, KeyStore.getDefaultType()); + } + + public Builder withTrustMaterial(Path trustStorePath, + char[] trustStorePassword, + TrustStoreTrustOptions trustOptions) { + + return withTrustMaterial(trustStorePath, trustStorePassword, KeyStore.getDefaultType(), trustOptions); + } + + public Builder withTrustMaterial(Path trustStorePath, char[] trustStorePassword, String trustStoreType) { + if (isNull(trustStorePath) || StringUtils.isBlank(trustStoreType)) { + throw new GenericKeyStoreException(TRUST_STORE_VALIDATION_EXCEPTION_MESSAGE); + } + + KeyStore trustStore = KeyStoreUtils.loadKeyStore(trustStorePath, trustStorePassword, trustStoreType); + trustStores.add(trustStore); + + return this; + } + + private Builder withTrustMaterial(Path trustStorePath, char[] trustStorePassword, String trustStoreType, String securityProviderName) { + if (isNull(trustStorePath)) { + throw new GenericKeyStoreException(TRUST_STORE_VALIDATION_EXCEPTION_MESSAGE); + } + + KeyStore trustStore = KeyStoreUtils.loadKeyStore(trustStorePath, trustStorePassword, trustStoreType); + + X509ExtendedTrustManager trustManager = securityProviderName == null + ? TrustManagerUtils.createTrustManager(trustStore, TrustManagerFactory.getDefaultAlgorithm()) + : TrustManagerUtils.createTrustManager(trustStore, TrustManagerFactory.getDefaultAlgorithm(), securityProviderName); + + trustManagers.add(trustManager); + + return this; + } + + public Builder withTrustMaterial(Path trustStorePath, + char[] trustStorePassword, + String trustStoreType, + TrustStoreTrustOptions trustOptions) { + + if (isNull(trustStorePath) || StringUtils.isBlank(trustStoreType)) { + throw new GenericKeyStoreException(TRUST_STORE_VALIDATION_EXCEPTION_MESSAGE); + } + + KeyStore trustStore = KeyStoreUtils.loadKeyStore(trustStorePath, trustStorePassword, trustStoreType); + return withTrustMaterial(trustStore, trustOptions); + } + + public Builder withTrustMaterial(InputStream trustStoreStream, char[] trustStorePassword) { + return withTrustMaterial(trustStoreStream, trustStorePassword, KeyStore.getDefaultType()); + } + + public Builder withTrustMaterial(InputStream trustStoreStream, + char[] trustStorePassword, + TrustStoreTrustOptions trustOptions) { + + return withTrustMaterial(trustStoreStream, trustStorePassword, KeyStore.getDefaultType(), trustOptions); + } + + public Builder withTrustMaterial(InputStream trustStoreStream, char[] trustStorePassword, String trustStoreType) { + KeyStore trustStore = KeyStoreUtils.loadKeyStore(trustStoreStream, trustStorePassword, trustStoreType); + trustStores.add(trustStore); + return this; + } + + public Builder withTrustMaterial(InputStream trustStoreStream, + char[] trustStorePassword, + String trustStoreType, TrustStoreTrustOptions trustOptions) { + + KeyStore trustStore = KeyStoreUtils.loadKeyStore(trustStoreStream, trustStorePassword, trustStoreType); + return withTrustMaterial(trustStore, trustOptions); + } + + public Builder withTrustMaterial(KeyStore trustStore) { + validateKeyStore(trustStore, TRUST_STORE_VALIDATION_EXCEPTION_MESSAGE); + trustStores.add(trustStore); + + return this; + } + + public Builder withTrustMaterial(KeyStore trustStore, TrustStoreTrustOptions trustOptions) { + try { + CertPathTrustManagerParameters certPathTrustManagerParameters = trustOptions.apply(trustStore); + return withTrustMaterial(certPathTrustManagerParameters); + } catch (Exception e) { + throw new GenericSecurityException(e); + } + } + + public Builder withTrustMaterial(Set certificates, TrustAnchorTrustOptions trustOptions) { + try { + Set trustAnchors = certificates.stream() + .map(certificate -> new TrustAnchor(certificate, null)) + .collect(Collectors.toSet()); + + CertPathTrustManagerParameters certPathTrustManagerParameters = trustOptions.apply(trustAnchors); + return withTrustMaterial(certPathTrustManagerParameters); + } catch (Exception e) { + throw new GenericSecurityException(e); + } + } + + public final Builder withTrustMaterial(T[] certificates, + TrustStoreTrustOptions trustOptions) { + + return withTrustMaterial(Arrays.asList(certificates), trustOptions); + } + + public Builder withTrustMaterial(List certificates) { + KeyStore trustStore = KeyStoreUtils.createTrustStore(ValidationUtils.requireNotEmpty(certificates, CERTIFICATE_VALIDATION_EXCEPTION_MESSAGE)); + trustStores.add(trustStore); + return this; + } + + public Builder withTrustMaterial(List certificates, + TrustStoreTrustOptions trustOptions) { + KeyStore trustStore = KeyStoreUtils.createTrustStore(ValidationUtils.requireNotEmpty(certificates, CERTIFICATE_VALIDATION_EXCEPTION_MESSAGE)); + return withTrustMaterial(trustStore, trustOptions); + } + + public Builder withSystemPropertyDerivedIdentityMaterial() { + return withSystemPropertyDerivedMaterial( + "javax.net.ssl.keyStore", + "javax.net.ssl.keyStorePassword", + "javax.net.ssl.keyStoreType", + "javax.net.ssl.keyStoreProvider", + this::withIdentityMaterial + ); + } + + public Builder withIdentityMaterial(String identityStorePath, char[] identityStorePassword) { + return withIdentityMaterial(identityStorePath, identityStorePassword, identityStorePassword, KeyStore.getDefaultType()); + } + + public Builder withIdentityMaterial(String identityStorePath, char[] identityStorePassword, char[] identityPassword) { + return withIdentityMaterial(identityStorePath, identityStorePassword, identityPassword, KeyStore.getDefaultType()); + } + + public Builder withIdentityMaterial(String identityStorePath, char[] identityStorePassword, String identityStoreType) { + return withIdentityMaterial(identityStorePath, identityStorePassword, identityStorePassword, identityStoreType); + } + + public Builder withIdentityMaterial(String identityStorePath, char[] identityStorePassword, char[] identityPassword, String identityStoreType) { + if (StringUtils.isBlank(identityStorePath) || StringUtils.isBlank(identityStoreType)) { + throw new GenericKeyStoreException(IDENTITY_VALIDATION_EXCEPTION_MESSAGE); + } + + KeyStore identity = KeyStoreUtils.loadKeyStore(identityStorePath, identityStorePassword, identityStoreType); + KeyStoreHolder identityHolder = new KeyStoreHolder(identity, identityPassword); + identities.add(identityHolder); + return this; + } + + public Builder withIdentityMaterial(Path identityStorePath, char[] identityStorePassword) { + return withIdentityMaterial(identityStorePath, identityStorePassword, identityStorePassword, KeyStore.getDefaultType()); + } + + public Builder withIdentityMaterial(Path identityStorePath, char[] identityStorePassword, char[] identityPassword) { + return withIdentityMaterial(identityStorePath, identityStorePassword, identityPassword, KeyStore.getDefaultType()); + } + + public Builder withIdentityMaterial(Path identityStorePath, char[] identityStorePassword, String identityStoreType) { + return withIdentityMaterial(identityStorePath, identityStorePassword, identityStorePassword, identityStoreType); + } + + public Builder withIdentityMaterial(Path identityStorePath, char[] identityStorePassword, char[] identityPassword, String identityStoreType) { + if (isNull(identityStorePath) || StringUtils.isBlank(identityStoreType)) { + throw new GenericKeyStoreException(IDENTITY_VALIDATION_EXCEPTION_MESSAGE); + } + + KeyStore identity = KeyStoreUtils.loadKeyStore(identityStorePath, identityStorePassword, identityStoreType); + KeyStoreHolder identityHolder = new KeyStoreHolder(identity, identityPassword); + identities.add(identityHolder); + return this; + } + + private Builder withIdentityMaterial(Path identityStorePath, char[] identityStorePassword, String identityStoreType, String securityProviderName) { + if (isNull(identityStorePath)) { + throw new GenericKeyStoreException(IDENTITY_VALIDATION_EXCEPTION_MESSAGE); + } + + KeyStore identity = KeyStoreUtils.loadKeyStore(identityStorePath, identityStorePassword, identityStoreType); + + X509ExtendedKeyManager keyManager = securityProviderName == null + ? KeyManagerUtils.createKeyManager(identity, identityStorePassword, KeyManagerFactory.getDefaultAlgorithm()) + : KeyManagerUtils.createKeyManager(identity, identityStorePassword, KeyManagerFactory.getDefaultAlgorithm(), securityProviderName); + + identityManagers.add(keyManager); + return this; + } + + public Builder withIdentityMaterial(InputStream identityStream, char[] identityStorePassword) { + return withIdentityMaterial(identityStream, identityStorePassword, identityStorePassword); + } + + public Builder withIdentityMaterial(InputStream identityStream, char[] identityStorePassword, char[] identityPassword) { + return withIdentityMaterial(identityStream, identityStorePassword, identityPassword, KeyStore.getDefaultType()); + } + + public Builder withIdentityMaterial(InputStream identityStream, char[] identityStorePassword, String identityStoreType) { + return withIdentityMaterial(identityStream, identityStorePassword, identityStorePassword, identityStoreType); + } + + public Builder withIdentityMaterial(InputStream identityStream, char[] identityStorePassword, char[] identityPassword, String identityStoreType) { + if (isNull(identityStream) || StringUtils.isBlank(identityStoreType)) { + throw new GenericKeyStoreException(IDENTITY_VALIDATION_EXCEPTION_MESSAGE); + } + + KeyStore identity = KeyStoreUtils.loadKeyStore(identityStream, identityStorePassword, identityStoreType); + KeyStoreHolder identityHolder = new KeyStoreHolder(identity, identityPassword); + identities.add(identityHolder); + return this; + } + + public Builder withIdentityMaterial(KeyStore identityStore, char[] identityPassword) { + validateKeyStore(identityStore, IDENTITY_VALIDATION_EXCEPTION_MESSAGE); + KeyStoreHolder identityHolder = new KeyStoreHolder(identityStore, identityPassword); + identities.add(identityHolder); + return this; + } + + public Builder withIdentityMaterial(Key privateKey, + char[] privateKeyPassword, + Collection certificateChain) { + return withIdentityMaterial(privateKey, privateKeyPassword, null, certificateChain); + } + + public Builder withIdentityMaterial(Key privateKey, + char[] privateKeyPassword, + String alias, + Collection certificateChain) { + KeyStore identityStore = KeyStoreUtils.createIdentityStore(privateKey, privateKeyPassword, alias, certificateChain); + identities.add(new KeyStoreHolder(identityStore, privateKeyPassword)); + return this; + } + + public Builder withIdentityMaterial(T keyManager) { + identityManagers.add(KeyManagerUtils.wrapIfNeeded(keyManager)); + return this; + } + + public Builder withIdentityMaterial(T keyManagerFactory) { + X509ExtendedKeyManager keyManager = KeyManagerUtils.getKeyManager(keyManagerFactory); + this.identityManagers.add(keyManager); + return this; + } + + public Builder withDummyIdentityMaterial() { + this.identityManagers.add(KeyManagerUtils.createDummyKeyManager()); + return this; + } + + /** + * Enables the possibility to swap the underlying KeyManager at runtime. + * After this option has been enabled the KeyManager can be swapped + * with {@link KeyManagerUtils#swapKeyManager(X509KeyManager, X509KeyManager) KeyManagerUtils#swapKeyManager(swappableKeyManager, newKeyManager)} + * + * @return {@link Builder} + */ + public Builder withSwappableIdentityMaterial() { + swappableKeyManagerEnabled = true; + return this; + } + + private void validateKeyStore(KeyStore keyStore, String exceptionMessage) { + if (isNull(keyStore)) { + throw new GenericKeyStoreException(exceptionMessage); + } + } + + public Builder withIdentityRoute(String alias, String... hosts) { + return withIdentityRoute( + alias, + Arrays.stream(hosts) + .map(URI::create) + .collect(Collectors.toList()) + ); + } + + public Builder withIdentityRoute(Map> aliasesToHosts) { + aliasesToHosts.entrySet().stream() + .map(aliasToHosts -> new AbstractMap.SimpleEntry<>( + aliasToHosts.getKey(), + aliasToHosts.getValue().stream() + .map(URI::create) + .collect(Collectors.toList()))) + .forEach(aliasToHosts -> withIdentityRoute(aliasToHosts.getKey(), aliasToHosts.getValue())); + return this; + } + + private Builder withIdentityRoute(String alias, List hosts) { + if (StringUtils.isBlank(alias)) { + throw new IllegalArgumentException("alias should be present"); + } + + ValidationUtils.requireNotEmpty(hosts, String.format("At least one host should be present. No host(s) found for the given alias: [%s]", alias)); + + for (URI host : hosts) { + UriUtils.validate(host); + + if (preferredAliasToHost.containsKey(alias)) { + preferredAliasToHost.get(alias).add(host); + } else { + preferredAliasToHost.put(alias, new ArrayList<>(Collections.singletonList(host))); + } + } + return this; + } + + public Builder withHostnameVerifier(T hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + return this; + } + + public Builder withUnsafeHostnameVerifier() { + this.hostnameVerifier = HostnameVerifierUtils.createUnsafe(); + return this; + } + + public Builder withCiphers(Iterable ciphers) { + ciphers.forEach(this.ciphers::add); + return this; + } + + public Builder withSystemPropertyDerivedCiphers() { + ciphers.addAll(extractPropertyValues("https.cipherSuites")); + return this; + } + + public Builder withProtocols(String... protocols) { + this.protocols.addAll(Arrays.asList(protocols)); + return this; + } + + public Builder withSystemPropertyDerivedProtocols() { + protocols.addAll(extractPropertyValues("https.protocols")); + return this; + } + + private List extractPropertyValues(String systemProperty) { + String propertyValue = ValidationUtils.requireNotBlank(System.getProperty(systemProperty), String.format(SYSTEM_PROPERTY_VALIDATION_EXCEPTION_MESSAGE, systemProperty)); + + List propertyValues = Arrays.stream(propertyValue.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .distinct().collect(Collectors.toList()); + + return ValidationUtils.requireNotEmpty(propertyValues, String.format(SYSTEM_PROPERTY_VALIDATION_EXCEPTION_MESSAGE, systemProperty)); + } + + public Builder withNeedClientAuthentication() { + return withNeedClientAuthentication(true); + } + + public Builder withNeedClientAuthentication(boolean needClientAuthentication) { + sslParameters.setNeedClientAuth(needClientAuthentication); + return this; + } + + public Builder withWantClientAuthentication() { + return withWantClientAuthentication(true); + } + + public Builder withWantClientAuthentication(boolean wantClientAuthentication) { + sslParameters.setWantClientAuth(wantClientAuthentication); + return this; + } + + public Builder withSessionTimeout(int timeoutInSeconds) { + this.sessionTimeoutInSeconds = timeoutInSeconds; + return this; + } + + public Builder withSessionCacheSize(int cacheSizeInBytes) { + this.sessionCacheSizeInBytes = cacheSizeInBytes; + return this; + } + + public Builder withSslContextAlgorithm(String sslContextAlgorithm) { + this.sslContextAlgorithm = sslContextAlgorithm; + return this; + } + + public Builder withSecurityProvider(T securityProvider) { + this.securityProvider = securityProvider; + return this; + } + + public Builder withSecurityProvider(String securityProviderName) { + this.securityProviderName = securityProviderName; + return this; + } + + public Builder withSecureRandom(T secureRandom) { + this.secureRandom = secureRandom; + return this; + } + + public Builder withTrustingAllCertificatesWithoutValidation() { + trustManagers.add(TrustManagerUtils.createUnsafeTrustManager()); + return this; + } + + public Builder withTrustEnhancer(ChainAndAuthTypeValidator validator) { + this.chainAndAuthTypeValidator = validator; + return this; + } + + @SuppressWarnings("overloads") + public Builder withTrustEnhancer(ChainAndAuthTypeWithSocketValidator validator) { + this.chainAndAuthTypeWithSocketValidator = validator; + return this; + } + + @SuppressWarnings("overloads") + public Builder withTrustEnhancer(ChainAndAuthTypeWithSSLEngineValidator validator) { + this.chainAndAuthTypeWithSSLEngineValidator = validator; + return this; + } + + private Builder withSystemPropertyDerivedMaterial(String keyStorePathProperty, + String keyStorePasswordProperty, + String keyStoreTypeProperty, + String securityProviderNameProperty, + QuadConsumer keyStorePropertyConsumer) { + Path keystore = Optional.ofNullable(System.getProperty(keyStorePathProperty)) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .map(Paths::get) + .orElse(null); + char[] keystorePassword = Optional.ofNullable(System.getProperty(keyStorePasswordProperty)) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .map(String::toCharArray) + .orElse(null); + String keystoreType = Optional.ofNullable(System.getProperty(keyStoreTypeProperty)) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .orElseGet(KeyStore::getDefaultType); + String securityProvideName = Optional.ofNullable(System.getProperty(securityProviderNameProperty)) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .orElse(null); + keyStorePropertyConsumer.accept(keystore, keystorePassword, keystoreType, securityProvideName); + return this; + } + + private interface QuadConsumer { + void accept(T t, U u, V v, W w); + } + + public SSLFactory build() { + if (!isIdentityMaterialPresent() && !isTrustMaterialPresent()) { + throw new GenericSecurityException(IDENTITY_AND_TRUST_MATERIAL_VALIDATION_EXCEPTION_MESSAGE); + } + X509ExtendedKeyManager keyManager = isIdentityMaterialPresent() ? createKeyManager() : null; + X509ExtendedTrustManager trustManager = isTrustMaterialPresent() ? createTrustManager() : null; + SSLContext sslContext = SSLContextUtils.createSslContext( + keyManager, + trustManager, + secureRandom, + sslContextAlgorithm, + securityProviderName, + securityProvider + ); + if (sessionTimeoutInSeconds >= 0) { + SSLSessionUtils.updateSessionTimeout(sslContext, sessionTimeoutInSeconds); + } + if (sessionCacheSizeInBytes >= 0) { + SSLSessionUtils.updateSessionCacheSize(sslContext, sessionCacheSizeInBytes); + } + sslParameters.setCipherSuites(ciphers.isEmpty() ? null : ciphers.stream().distinct().toArray(String[]::new)); + sslParameters.setProtocols(protocols.isEmpty() ? null : protocols.stream().distinct().toArray(String[]::new)); + SSLParameters baseSslParameters = SSLParametersUtils.merge(sslParameters, sslContext.getDefaultSSLParameters()); + SSLMaterial sslMaterial = new SSLMaterial.Builder() + .withSslContext(sslContext) + .withKeyManager(keyManager) + .withTrustManager(trustManager) + .withSslParameters(baseSslParameters) + .withHostnameVerifier(hostnameVerifier) + .withCiphers(Collections.unmodifiableList(Arrays.asList(baseSslParameters.getCipherSuites()))) + .withProtocols(Collections.unmodifiableList(Arrays.asList(baseSslParameters.getProtocols()))) + .build(); + return new SSLFactory(sslMaterial); + } + + private boolean isTrustMaterialPresent() { + return !trustStores.isEmpty() || !trustManagers.isEmpty(); + } + + private boolean isIdentityMaterialPresent() { + return !identities.isEmpty() || !identityManagers.isEmpty(); + } + + private X509ExtendedKeyManager createKeyManager() { + return KeyManagerUtils.keyManagerBuilder() + .withKeyManagers(identityManagers) + .withIdentities(identities) + .withSwappableKeyManager(swappableKeyManagerEnabled) + .withIdentityRoute(preferredAliasToHost) + .build(); + } + + private X509ExtendedTrustManager createTrustManager() { + return TrustManagerUtils.trustManagerBuilder() + .withTrustManagers(trustManagers) + .withTrustStores(trustStores) + .withSwappableTrustManager(swappableTrustManagerEnabled) + .withTrustEnhancer(chainAndAuthTypeValidator) + .withTrustEnhancer(chainAndAuthTypeWithSocketValidator) + .withTrustEnhancer(chainAndAuthTypeWithSSLEngineValidator) + .build(); + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericCertificateException.java b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericCertificateException.java new file mode 100644 index 0000000..2b3c6e4 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericCertificateException.java @@ -0,0 +1,18 @@ +package org.xbib.net.security.ssl.exception; + +@SuppressWarnings("serial") +public final class GenericCertificateException extends GenericSecurityException { + + public GenericCertificateException(Throwable cause) { + super(cause); + } + + public GenericCertificateException(String message) { + super(message); + } + + public GenericCertificateException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericIOException.java b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericIOException.java new file mode 100644 index 0000000..0323749 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericIOException.java @@ -0,0 +1,14 @@ +package org.xbib.net.security.ssl.exception; + +@SuppressWarnings("serial") +public final class GenericIOException extends GenericSecurityException { + + public GenericIOException(Throwable cause) { + super(cause); + } + + public GenericIOException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericKeyManagerException.java b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericKeyManagerException.java new file mode 100644 index 0000000..6f99414 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericKeyManagerException.java @@ -0,0 +1,14 @@ +package org.xbib.net.security.ssl.exception; + +@SuppressWarnings("serial") +public final class GenericKeyManagerException extends GenericSecurityException { + + public GenericKeyManagerException(String message) { + super(message); + } + + public GenericKeyManagerException(Throwable cause) { + super(cause); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericKeyStoreException.java b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericKeyStoreException.java new file mode 100644 index 0000000..495998d --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericKeyStoreException.java @@ -0,0 +1,17 @@ +package org.xbib.net.security.ssl.exception; + +@SuppressWarnings("serial") +public final class GenericKeyStoreException extends GenericSecurityException { + + public GenericKeyStoreException(String message) { + super(message); + } + + public GenericKeyStoreException(Throwable cause) { + super(cause); + } + + public GenericKeyStoreException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericSSLContextException.java b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericSSLContextException.java new file mode 100644 index 0000000..a0543c9 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericSSLContextException.java @@ -0,0 +1,10 @@ +package org.xbib.net.security.ssl.exception; + +@SuppressWarnings("serial") +public final class GenericSSLContextException extends GenericSecurityException { + + public GenericSSLContextException(Throwable cause) { + super(cause); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericSecurityException.java b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericSecurityException.java new file mode 100644 index 0000000..0068f7a --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericSecurityException.java @@ -0,0 +1,18 @@ +package org.xbib.net.security.ssl.exception; + +@SuppressWarnings("serial") +public class GenericSecurityException extends RuntimeException { + + public GenericSecurityException(String message) { + super(message); + } + + public GenericSecurityException(String message, Throwable cause) { + super(message, cause); + } + + public GenericSecurityException(Throwable cause) { + super(cause); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericTrustManagerException.java b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericTrustManagerException.java new file mode 100644 index 0000000..8bb7084 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/exception/GenericTrustManagerException.java @@ -0,0 +1,14 @@ +package org.xbib.net.security.ssl.exception; + +@SuppressWarnings("serial") +public final class GenericTrustManagerException extends GenericSecurityException { + + public GenericTrustManagerException(String message) { + super(message); + } + + public GenericTrustManagerException(Throwable cause) { + super(cause); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/hostnameverifier/BasicHostNameVerifier.java b/net-security/src/main/java/org/xbib/net/security/ssl/hostnameverifier/BasicHostNameVerifier.java new file mode 100644 index 0000000..b584f88 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/hostnameverifier/BasicHostNameVerifier.java @@ -0,0 +1,29 @@ +package org.xbib.net.security.ssl.hostnameverifier; + +import org.xbib.net.security.ssl.util.HostnameVerifierUtils; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link HostnameVerifierUtils HostnameVerifierUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + */ +public final class BasicHostNameVerifier implements HostnameVerifier { + + private static final HostnameVerifier INSTANCE = new BasicHostNameVerifier(); + + private BasicHostNameVerifier() {} + + @Override + public boolean verify(String host, SSLSession sslSession) { + return host.equalsIgnoreCase(sslSession.getPeerHost()); + } + + public static HostnameVerifier getInstance() { + return INSTANCE; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/hostnameverifier/UnsafeHostNameVerifier.java b/net-security/src/main/java/org/xbib/net/security/ssl/hostnameverifier/UnsafeHostNameVerifier.java new file mode 100644 index 0000000..686ac54 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/hostnameverifier/UnsafeHostNameVerifier.java @@ -0,0 +1,29 @@ +package org.xbib.net.security.ssl.hostnameverifier; + +import org.xbib.net.security.ssl.util.HostnameVerifierUtils; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link HostnameVerifierUtils HostnameVerifierUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + */ +public final class UnsafeHostNameVerifier implements HostnameVerifier { + + private static final HostnameVerifier INSTANCE = new UnsafeHostNameVerifier(); + + private UnsafeHostNameVerifier() {} + + @Override + public boolean verify(String host, SSLSession sslSession) { + return true; + } + + public static HostnameVerifier getInstance() { + return INSTANCE; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/CombinableX509ExtendedKeyManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/CombinableX509ExtendedKeyManager.java new file mode 100644 index 0000000..02ca77e --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/CombinableX509ExtendedKeyManager.java @@ -0,0 +1,38 @@ +package org.xbib.net.security.ssl.keymanager; + +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +interface CombinableX509ExtendedKeyManager extends X509KeyManager { + + List getKeyManagers(); + + default T extractInnerField(Function keyManagerMapper, Predicate predicate) { + return getKeyManagers().stream() + .map(keyManagerMapper) + .filter(predicate) + .findFirst() + .orElse(null); + } + + default String[] getAliases(Function aliasExtractor) { + List aliases = getKeyManagers().stream() + .map(aliasExtractor) + .filter(Objects::nonNull) + .flatMap(Arrays::stream) + .collect(Collectors.toList()); + + return aliases.isEmpty() ? null : aliases.toArray(new String[]{}); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/CompositeX509ExtendedKeyManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/CompositeX509ExtendedKeyManager.java new file mode 100644 index 0000000..4e6316a --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/CompositeX509ExtendedKeyManager.java @@ -0,0 +1,179 @@ +package org.xbib.net.security.ssl.keymanager; + +import org.xbib.net.security.ssl.util.KeyManagerUtils; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.X509ExtendedKeyManager; +import java.net.Socket; +import java.net.URI; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Represents an ordered list of {@link X509ExtendedKeyManager} with most-preferred managers first. + * This is necessary because of the fine-print on {@link javax.net.ssl.SSLContext#init}: + * Only the first instance of a particular key and/or key manager implementation type in the + * array is used. (For example, only the first javax.net.ssl.X509KeyManager in the array will be used.) + * The KeyManager can be build from one or more of any combination provided within the + * {@link KeyManagerUtils.KeyManagerBuilder KeyManagerUtils.KeyManagerBuilder}. + * This includes: + *

    + *     - Any amount of custom KeyManagers
    + *     - Any amount of custom Identities
    + * 
    + * + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link KeyManagerUtils KeyManagerUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + * + * @see + * http://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm + * + * @see + * http://codyaray.com/2013/04/java-ssl-with-multiple-keystores + * + */ +public final class CompositeX509ExtendedKeyManager extends X509ExtendedKeyManager implements CombinableX509ExtendedKeyManager, RoutableX509ExtendedKeyManager { + + private final List keyManagers; + + private final Map> preferredAliasToHost; + + /** + * Creates a new {@link CompositeX509ExtendedKeyManager}. + * + * @param keyManagers the {@link X509ExtendedKeyManager}, ordered with the most-preferred managers first. + */ + public CompositeX509ExtendedKeyManager(List keyManagers) { + this(keyManagers, Collections.emptyMap()); + } + + /** + * Creates a new {@link CompositeX509ExtendedKeyManager}. + * + * @param keyManagers the {@link X509ExtendedKeyManager}, ordered with the most-preferred managers first. + * @param preferredAliasToHost the preferred client alias to be used for the given host + */ + public CompositeX509ExtendedKeyManager(List keyManagers, + Map> preferredAliasToHost) { + this.keyManagers = Collections.unmodifiableList(keyManagers); + this.preferredAliasToHost = new HashMap<>(preferredAliasToHost); + } + + /** + * Chooses the first non-null client alias returned from the delegate + * {@link X509ExtendedKeyManager}, or {@code null} if there are no matches. + */ + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return chooseClientAlias( + socket, + this::containsInetSocketAddress, + this::extractHostAndPort, + keyManager -> keyManager.chooseClientAlias(keyType, issuers, socket) + ); + } + + /** + * Chooses the first non-null client alias returned from the delegate + * {@link X509ExtendedKeyManager}, or {@code null} if there are no matches. + */ + @Override + public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine sslEngine) { + return chooseClientAlias( + sslEngine, + Objects::nonNull, + this::extractHostAndPort, + keyManager -> keyManager.chooseEngineClientAlias(keyTypes, issuers, sslEngine) + ); + } + + /** + * Chooses the first non-null server alias returned from the delegate + * {@link X509ExtendedKeyManager}, or {@code null} if there are no matches. + */ + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return chooseServerAlias( + socket, + SSLSocket.class::isInstance, + aSocket -> ((SSLSocket) aSocket).getHandshakeSession(), + keyManager -> keyManager.chooseServerAlias(keyType, issuers, socket) + ); + } + + /** + * Chooses the first non-null server alias returned from the delegate + * {@link X509ExtendedKeyManager}, or {@code null} if there are no matches. + */ + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine sslEngine) { + return chooseServerAlias( + sslEngine, + Objects::nonNull, + SSLEngine::getHandshakeSession, + keyManager -> keyManager.chooseEngineServerAlias(keyType, issuers, sslEngine) + ); + } + + /** + * Returns the first non-null private key associated with the + * given alias, or {@code null} if the alias can't be found. + */ + @Override + public PrivateKey getPrivateKey(String alias) { + return extractInnerField( + keyManager -> keyManager.getPrivateKey(alias), + Objects::nonNull + ); + } + + /** + * Returns the first non-null certificate chain associated with the + * given alias, or {@code null} if the alias can't be found. + */ + @Override + public X509Certificate[] getCertificateChain(String alias) { + return extractInnerField( + keyManager -> keyManager.getCertificateChain(alias), + chain -> chain != null && chain.length > 0 + ); + } + + /** + * Get all matching aliases for authenticating the client side of a + * secure socket, or {@code null} if there are no matches. + */ + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return getAliases(keyManager -> keyManager.getClientAliases(keyType, issuers)); + } + + /** + * Get all matching aliases for authenticating the server side of a + * secure socket, or {@code null} if there are no matches. + */ + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return getAliases(keyManager -> keyManager.getServerAliases(keyType, issuers)); + } + + @Override + public List getKeyManagers() { + return keyManagers; + } + + @Override + public Map> getIdentityRoute() { + return preferredAliasToHost; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/DelegatingKeyManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/DelegatingKeyManager.java new file mode 100644 index 0000000..e252213 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/DelegatingKeyManager.java @@ -0,0 +1,63 @@ +package org.xbib.net.security.ssl.keymanager; + +import org.xbib.net.security.ssl.util.ValidationUtils; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +abstract class DelegatingKeyManager extends X509ExtendedKeyManager { + + private static final String NO_KEY_MANAGER_EXCEPTION_MESSAGE = "No valid KeyManager has been provided. KeyManager must be present, but was absent."; + + T keyManager; + + DelegatingKeyManager(T keyManager) { + this.keyManager = ValidationUtils.requireNotNull(keyManager, NO_KEY_MANAGER_EXCEPTION_MESSAGE); + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return keyManager.chooseClientAlias(keyType, issuers, socket); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return keyManager.chooseServerAlias(keyType, issuers, socket); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return keyManager.getPrivateKey(alias); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return keyManager.getCertificateChain(alias); + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return keyManager.getClientAliases(keyType, issuers); + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return keyManager.getServerAliases(keyType, issuers); + } + + @Override + public abstract String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine); + + @Override + public abstract String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine); + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/DelegatingX509ExtendedKeyManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/DelegatingX509ExtendedKeyManager.java new file mode 100644 index 0000000..de090df --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/DelegatingX509ExtendedKeyManager.java @@ -0,0 +1,27 @@ +package org.xbib.net.security.ssl.keymanager; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; +import java.security.Principal; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +class DelegatingX509ExtendedKeyManager extends DelegatingKeyManager { + + public DelegatingX509ExtendedKeyManager(X509ExtendedKeyManager keyManager) { + super(keyManager); + } + + @Override + public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine sslEngine) { + return keyManager.chooseEngineClientAlias(keyTypes, issuers, sslEngine); + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine sslEngine) { + return keyManager.chooseEngineServerAlias(keyType, issuers, sslEngine); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/DummyX509ExtendedKeyManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/DummyX509ExtendedKeyManager.java new file mode 100644 index 0000000..114aa20 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/DummyX509ExtendedKeyManager.java @@ -0,0 +1,64 @@ +package org.xbib.net.security.ssl.keymanager; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +public final class DummyX509ExtendedKeyManager extends X509ExtendedKeyManager { + + private static final X509ExtendedKeyManager INSTANCE = new DummyX509ExtendedKeyManager(); + + private DummyX509ExtendedKeyManager() {} + + public static X509ExtendedKeyManager getInstance() { + return INSTANCE; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return null; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine sslEngine) { + return null; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return null; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine sslEngine) { + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return null; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return null; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/HotSwappableX509ExtendedKeyManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/HotSwappableX509ExtendedKeyManager.java new file mode 100644 index 0000000..6655563 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/HotSwappableX509ExtendedKeyManager.java @@ -0,0 +1,24 @@ +package org.xbib.net.security.ssl.keymanager; + +import org.xbib.net.security.ssl.util.KeyManagerUtils; +import org.xbib.net.security.ssl.util.ValidationUtils; + +import javax.net.ssl.X509ExtendedKeyManager; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link KeyManagerUtils KeyManagerUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + */ +public final class HotSwappableX509ExtendedKeyManager extends DelegatingX509ExtendedKeyManager { + + public HotSwappableX509ExtendedKeyManager(X509ExtendedKeyManager keyManager) { + super(keyManager); + } + + public void setKeyManager(X509ExtendedKeyManager keyManager) { + this.keyManager = ValidationUtils.requireNotNull(keyManager, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("KeyManager")); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/KeyManagerFactorySpiWrapper.java b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/KeyManagerFactorySpiWrapper.java new file mode 100644 index 0000000..bb57531 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/KeyManagerFactorySpiWrapper.java @@ -0,0 +1,38 @@ +package org.xbib.net.security.ssl.keymanager; + +import org.xbib.net.security.ssl.util.ValidationUtils; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactorySpi; +import javax.net.ssl.ManagerFactoryParameters; +import java.security.KeyStore; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +class KeyManagerFactorySpiWrapper extends KeyManagerFactorySpi { + + private static final String NO_KEY_MANAGER_EXCEPTION_MESSAGE = "No valid KeyManager has been provided. KeyManager must be present, but was absent."; + + private final KeyManager[] keyManagers; + + KeyManagerFactorySpiWrapper(KeyManager keyManager) { + ValidationUtils.requireNotNull(keyManager, NO_KEY_MANAGER_EXCEPTION_MESSAGE); + this.keyManagers = new KeyManager[]{keyManager}; + } + + @Override + protected void engineInit(KeyStore keyStore, char[] keyStorePassword) { + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { + } + + @Override + protected KeyManager[] engineGetKeyManagers() { + return keyManagers; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/KeyManagerFactoryWrapper.java b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/KeyManagerFactoryWrapper.java new file mode 100644 index 0000000..00007fc --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/KeyManagerFactoryWrapper.java @@ -0,0 +1,25 @@ +package org.xbib.net.security.ssl.keymanager; + +import org.xbib.net.security.ssl.util.KeyManagerUtils; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import java.security.Provider; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link KeyManagerUtils KeyManagerUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + */ +public final class KeyManagerFactoryWrapper extends KeyManagerFactory { + + private static final String KEY_MANAGER_FACTORY_ALGORITHM = "no-algorithm"; + + private static final Provider PROVIDER = new Provider("", "1.0", "") {}; + + public KeyManagerFactoryWrapper(KeyManager keyManager) { + super(new KeyManagerFactorySpiWrapper(keyManager), PROVIDER, KEY_MANAGER_FACTORY_ALGORITHM); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/RoutableX509ExtendedKeyManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/RoutableX509ExtendedKeyManager.java new file mode 100644 index 0000000..67c929b --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/RoutableX509ExtendedKeyManager.java @@ -0,0 +1,122 @@ +package org.xbib.net.security.ssl.keymanager; + +import javax.net.ssl.ExtendedSSLSession; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSession; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URI; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.Set; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +interface RoutableX509ExtendedKeyManager extends CombinableX509ExtendedKeyManager, X509KeyManager { + + Predicate NON_NULL = Objects::nonNull; + + Map> getIdentityRoute(); + + default String chooseClientAlias(T object, + Predicate predicate, + Function> hostToPortExtractor, + Function aliasExtractor) { + + return chooseAlias(() -> getPreferredClientAlias(object, predicate, hostToPortExtractor), aliasExtractor); + } + + default String getPreferredClientAlias(T object, Predicate predicate, Function> hostToPortExtractor) { + if (getIdentityRoute().isEmpty()) { + return null; + } + + if (predicate.test(object)) { + Entry hostToPort = hostToPortExtractor.apply(object); + return getPreferredClientAlias(hostToPort.getKey(), hostToPort.getValue()); + } + + return null; + } + + default String getPreferredClientAlias(String peerHost, int peerPort) { + return getIdentityRoute().entrySet().stream() + .filter(entry -> entry.getValue().stream().anyMatch(uri -> uri.getHost().contains(peerHost))) + .filter(entry -> entry.getValue().stream().anyMatch(uri -> uri.getPort() == peerPort)) + .findFirst() + .map(Entry::getKey) + .orElse(null); + } + + default String chooseServerAlias(T object, + Predicate predicate, + Function sslSessionExtractor, + Function aliasExtractor) { + + return chooseAlias(() -> getPreferredServerAlias(object, predicate, sslSessionExtractor), aliasExtractor); + } + + default String getPreferredServerAlias(T object, Predicate predicate, Function sslSessionExtractor) { + if (getIdentityRoute().isEmpty()) { + return null; + } + + if (predicate.test(object)) { + SSLSession sslSession = sslSessionExtractor.apply(object); + if (sslSession instanceof ExtendedSSLSession) { + List requestedServerNames = ((ExtendedSSLSession) sslSession).getRequestedServerNames(); + Set hostnames = requestedServerNames.stream() + .map(sniServerName -> new String(sniServerName.getEncoded())) + .collect(Collectors.toSet()); + + return getPreferredServerAlias(hostnames); + } + } + + return null; + } + + default String getPreferredServerAlias(Set hostnames) { + return getIdentityRoute().entrySet().stream() + .filter(entry -> entry.getValue().stream().anyMatch(uri -> hostnames.stream().anyMatch(hostname -> uri.getHost().contains(hostname)))) + .findFirst() + .map(Entry::getKey) + .orElse(null); + } + + default String chooseAlias(Supplier preferredAliasSupplier, Function aliasExtractor) { + String preferredAlias = preferredAliasSupplier.get(); + + if (preferredAlias != null) { + return extractInnerField(aliasExtractor, NON_NULL.and(preferredAlias::equals)); + } else { + return extractInnerField(aliasExtractor, NON_NULL); + } + } + + default boolean containsInetSocketAddress(Socket socket) { + return socket != null && socket.getRemoteSocketAddress() instanceof InetSocketAddress; + } + + default Entry extractHostAndPort(Socket socket) { + InetSocketAddress address = (InetSocketAddress) socket.getRemoteSocketAddress(); + return new AbstractMap.SimpleImmutableEntry<>(address.getHostName(), address.getPort()); + } + + default Entry extractHostAndPort(SSLEngine sslEngine) { + return new AbstractMap.SimpleImmutableEntry<>(sslEngine.getPeerHost(), sslEngine.getPeerPort()); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/X509KeyManagerWrapper.java b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/X509KeyManagerWrapper.java new file mode 100644 index 0000000..8d9a231 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/keymanager/X509KeyManagerWrapper.java @@ -0,0 +1,31 @@ +package org.xbib.net.security.ssl.keymanager; + +import org.xbib.net.security.ssl.util.KeyManagerUtils; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509KeyManager; +import java.security.Principal; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link KeyManagerUtils KeyManagerUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + */ +public final class X509KeyManagerWrapper extends DelegatingKeyManager { + + public X509KeyManagerWrapper(X509KeyManager keyManager) { + super(keyManager); + } + + @Override + public String chooseEngineClientAlias(String[] keyTypes, Principal[] issuers, SSLEngine sslEngine) { + return keyManager.chooseClientAlias(keyTypes, issuers, null); + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine sslEngine) { + return keyManager.chooseServerAlias(keyType, issuers, null); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/model/KeyStoreHolder.java b/net-security/src/main/java/org/xbib/net/security/ssl/model/KeyStoreHolder.java new file mode 100644 index 0000000..9650ae4 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/model/KeyStoreHolder.java @@ -0,0 +1,23 @@ +package org.xbib.net.security.ssl.model; + +import java.security.KeyStore; + +public final class KeyStoreHolder { + + private final KeyStore keyStore; + private final char[] keyPassword; + + public KeyStoreHolder(KeyStore keyStore, char[] keyPassword) { + this.keyStore = keyStore; + this.keyPassword = keyPassword; + } + + public KeyStore getKeyStore() { + return keyStore; + } + + public char[] getKeyPassword() { + return keyPassword; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/model/SSLMaterial.java b/net-security/src/main/java/org/xbib/net/security/ssl/model/SSLMaterial.java new file mode 100644 index 0000000..4aa4eb1 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/model/SSLMaterial.java @@ -0,0 +1,112 @@ +package org.xbib.net.security.ssl.model; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import java.util.List; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +public final class SSLMaterial { + + private SSLContext sslContext; + private X509ExtendedKeyManager keyManager; + private X509ExtendedTrustManager trustManager; + private HostnameVerifier hostnameVerifier; + private SSLParameters sslParameters; + private List ciphers; + private List protocols; + + private SSLMaterial() {} + + public SSLContext getSslContext() { + return sslContext; + } + + public X509ExtendedKeyManager getKeyManager() { + return keyManager; + } + + public X509ExtendedTrustManager getTrustManager() { + return trustManager; + } + + public SSLParameters getSslParameters() { + return sslParameters; + } + + public HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + public List getCiphers() { + return ciphers; + } + + public List getProtocols() { + return protocols; + } + + public static class Builder { + + private SSLContext sslContext; + private X509ExtendedKeyManager keyManager; + private X509ExtendedTrustManager trustManager; + private HostnameVerifier hostnameVerifier; + private SSLParameters sslParameters; + private List ciphers; + private List protocols; + + public Builder withSslContext(SSLContext sslContext) { + this.sslContext = sslContext; + return this; + } + + public Builder withHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + return this; + } + + public Builder withSslParameters(SSLParameters sslParameters) { + this.sslParameters = sslParameters; + return this; + } + + public Builder withCiphers(List ciphers) { + this.ciphers = ciphers; + return this; + } + + public Builder withProtocols(List protocols) { + this.protocols = protocols; + return this; + } + + public Builder withKeyManager(X509ExtendedKeyManager keyManager) { + this.keyManager = keyManager; + return this; + } + + public Builder withTrustManager(X509ExtendedTrustManager trustManager) { + this.trustManager = trustManager; + return this; + } + + public SSLMaterial build() { + SSLMaterial sslMaterial = new SSLMaterial(); + sslMaterial.sslContext = sslContext; + sslMaterial.keyManager = keyManager; + sslMaterial.trustManager = trustManager; + sslMaterial.hostnameVerifier = hostnameVerifier; + sslMaterial.sslParameters = sslParameters; + sslMaterial.ciphers = ciphers; + sslMaterial.protocols = protocols; + return sslMaterial; + } + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/socket/CompositeSSLServerSocketFactory.java b/net-security/src/main/java/org/xbib/net/security/ssl/socket/CompositeSSLServerSocketFactory.java new file mode 100644 index 0000000..282048c --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/socket/CompositeSSLServerSocketFactory.java @@ -0,0 +1,71 @@ +package org.xbib.net.security.ssl.socket; + +import org.xbib.net.security.ssl.util.SSLSocketUtils; +import org.xbib.net.security.ssl.util.ValidationUtils; + +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link SSLSocketUtils SSLSocketUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + */ +public final class CompositeSSLServerSocketFactory extends SSLServerSocketFactory { + + private final SSLServerSocketFactory sslServerSocketFactory; + private final SSLParameters sslParameters; + + public CompositeSSLServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, SSLParameters sslParameters) { + this.sslServerSocketFactory = ValidationUtils.requireNotNull(sslServerSocketFactory, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("SSLServerSocketFactory")); + this.sslParameters = ValidationUtils.requireNotNull(sslParameters, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("SSLParameters")); + } + + @Override + public String[] getDefaultCipherSuites() { + return sslParameters.getCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sslParameters.getCipherSuites(); + } + + @Override + public ServerSocket createServerSocket() throws IOException { + ServerSocket serverSocket = sslServerSocketFactory.createServerSocket(); + return withSslParameters(serverSocket); + } + + @Override + public ServerSocket createServerSocket(int port) throws IOException { + ServerSocket serverSocket = sslServerSocketFactory.createServerSocket(port); + return withSslParameters(serverSocket); + } + + @Override + public ServerSocket createServerSocket(int port, int backlog) throws IOException { + ServerSocket serverSocket = sslServerSocketFactory.createServerSocket(port, backlog); + return withSslParameters(serverSocket); + } + + @Override + public ServerSocket createServerSocket(int port, int backlog, InetAddress ifAddress) throws IOException { + ServerSocket serverSocket = sslServerSocketFactory.createServerSocket(port, backlog, ifAddress); + return withSslParameters(serverSocket); + } + + private ServerSocket withSslParameters(ServerSocket socket) { + if (socket instanceof SSLServerSocket) { + SSLServerSocket sslSocket = (SSLServerSocket) socket; + sslSocket.setSSLParameters(sslParameters); + } + return socket; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/socket/CompositeSSLSocketFactory.java b/net-security/src/main/java/org/xbib/net/security/ssl/socket/CompositeSSLSocketFactory.java new file mode 100644 index 0000000..536f094 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/socket/CompositeSSLSocketFactory.java @@ -0,0 +1,91 @@ +package org.xbib.net.security.ssl.socket; + +import org.xbib.net.security.ssl.util.SSLSocketUtils; +import org.xbib.net.security.ssl.util.ValidationUtils; + +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link SSLSocketUtils SSLSocketUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + */ +public final class CompositeSSLSocketFactory extends SSLSocketFactory { + + private final SSLSocketFactory sslSocketFactory; + private final SSLParameters sslParameters; + + public CompositeSSLSocketFactory(SSLSocketFactory sslSocketFactory, SSLParameters sslParameters) { + this.sslSocketFactory = ValidationUtils.requireNotNull(sslSocketFactory, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("SSLSocketFactory")); + this.sslParameters = ValidationUtils.requireNotNull(sslParameters, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("SSLParameters")); + } + + @Override + public String[] getDefaultCipherSuites() { + return sslParameters.getCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sslParameters.getCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + Socket socket = sslSocketFactory.createSocket(); + return withSslParameters(socket); + } + + @Override + public Socket createSocket(Socket socket, InputStream inputStream, boolean autoClosable) throws IOException { + Socket newSocket = sslSocketFactory.createSocket(socket, inputStream, autoClosable); + return withSslParameters(newSocket); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClosable) throws IOException { + Socket newSocket = sslSocketFactory.createSocket(socket, host, port, autoClosable); + return withSslParameters(newSocket); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + Socket socket = sslSocketFactory.createSocket(host, port); + return withSslParameters(socket); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException, UnknownHostException { + Socket socket = sslSocketFactory.createSocket(host, port, localAddress, localPort); + return withSslParameters(socket); + } + + @Override + public Socket createSocket(InetAddress address, int port) throws IOException { + Socket socket = sslSocketFactory.createSocket(address, port); + return withSslParameters(socket); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + Socket socket = sslSocketFactory.createSocket(address, port, localAddress, localPort); + return withSslParameters(socket); + } + + private Socket withSslParameters(Socket socket) { + if (socket instanceof SSLSocket) { + SSLSocket sslSocket = (SSLSocket) socket; + sslSocket.setSSLParameters(sslParameters); + } + return socket; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/CertificateCapturingX509ExtendedTrustManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/CertificateCapturingX509ExtendedTrustManager.java new file mode 100644 index 0000000..990ab7a --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/CertificateCapturingX509ExtendedTrustManager.java @@ -0,0 +1,60 @@ +package org.xbib.net.security.ssl.trustmanager; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +public class CertificateCapturingX509ExtendedTrustManager extends DelegatingX509ExtendedTrustManager { + + private final List certificatesCollector; + + public CertificateCapturingX509ExtendedTrustManager(X509ExtendedTrustManager trustManager, List certificatesCollector) { + super(trustManager); + this.certificatesCollector = certificatesCollector; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + certificatesCollector.addAll(Arrays.asList(chain)); + super.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + certificatesCollector.addAll(Arrays.asList(chain)); + super.checkServerTrusted(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + certificatesCollector.addAll(Arrays.asList(chain)); + super.checkClientTrusted(chain, authType, socket); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + certificatesCollector.addAll(Arrays.asList(chain)); + super.checkClientTrusted(chain, authType, sslEngine); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + certificatesCollector.addAll(Arrays.asList(chain)); + super.checkServerTrusted(chain, authType, socket); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + certificatesCollector.addAll(Arrays.asList(chain)); + super.checkServerTrusted(chain, authType, sslEngine); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/ChainAndAuthTypeValidator.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/ChainAndAuthTypeValidator.java new file mode 100644 index 0000000..aca13ea --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/ChainAndAuthTypeValidator.java @@ -0,0 +1,21 @@ +package org.xbib.net.security.ssl.trustmanager; + +import java.security.cert.X509Certificate; +import java.util.Objects; + +@FunctionalInterface +public interface ChainAndAuthTypeValidator { + + boolean test(X509Certificate[] certificateChain, String authType); + + default ChainAndAuthTypeValidator and(ChainAndAuthTypeValidator other) { + Objects.requireNonNull(other); + return (certificateChain, authType) -> test(certificateChain, authType) && other.test(certificateChain, authType); + } + + default ChainAndAuthTypeValidator or(ChainAndAuthTypeValidator other) { + Objects.requireNonNull(other); + return (certificateChain, authType) -> test(certificateChain, authType) || other.test(certificateChain, authType); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/ChainAndAuthTypeWithSSLEngineValidator.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/ChainAndAuthTypeWithSSLEngineValidator.java new file mode 100644 index 0000000..cd29cfd --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/ChainAndAuthTypeWithSSLEngineValidator.java @@ -0,0 +1,22 @@ +package org.xbib.net.security.ssl.trustmanager; + +import javax.net.ssl.SSLEngine; +import java.security.cert.X509Certificate; +import java.util.Objects; + +@FunctionalInterface +public interface ChainAndAuthTypeWithSSLEngineValidator { + + boolean test(X509Certificate[] certificateChain, String authType, SSLEngine sslEngine); + + default ChainAndAuthTypeWithSSLEngineValidator and(ChainAndAuthTypeWithSSLEngineValidator other) { + Objects.requireNonNull(other); + return (certificateChain, authType, sslEngine) -> test(certificateChain, authType, sslEngine) && other.test(certificateChain, authType, sslEngine); + } + + default ChainAndAuthTypeWithSSLEngineValidator or(ChainAndAuthTypeWithSSLEngineValidator other) { + Objects.requireNonNull(other); + return (certificateChain, authType, sslEngine) -> test(certificateChain, authType, sslEngine) || other.test(certificateChain, authType, sslEngine); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/ChainAndAuthTypeWithSocketValidator.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/ChainAndAuthTypeWithSocketValidator.java new file mode 100644 index 0000000..161fd69 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/ChainAndAuthTypeWithSocketValidator.java @@ -0,0 +1,22 @@ +package org.xbib.net.security.ssl.trustmanager; + +import java.net.Socket; +import java.security.cert.X509Certificate; +import java.util.Objects; + +@FunctionalInterface +public interface ChainAndAuthTypeWithSocketValidator { + + boolean test(X509Certificate[] certificateChain, String authType, Socket socket); + + default ChainAndAuthTypeWithSocketValidator and(ChainAndAuthTypeWithSocketValidator other) { + Objects.requireNonNull(other); + return (certificateChain, authType, socket) -> test(certificateChain, authType, socket) && other.test(certificateChain, authType, socket); + } + + default ChainAndAuthTypeWithSocketValidator or(ChainAndAuthTypeWithSocketValidator other) { + Objects.requireNonNull(other); + return (certificateChain, authType, socket) -> test(certificateChain, authType, socket) || other.test(certificateChain, authType, socket); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/CombinableX509TrustManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/CombinableX509TrustManager.java new file mode 100644 index 0000000..bc841d2 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/CombinableX509TrustManager.java @@ -0,0 +1,36 @@ +package org.xbib.net.security.ssl.trustmanager; + +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.List; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +interface CombinableX509TrustManager extends X509TrustManager { + + String CERTIFICATE_EXCEPTION_MESSAGE = "None of the TrustManagers trust this certificate chain"; + + List getTrustManagers(); + + default void checkTrusted(TrustManagerConsumer callBackConsumer) throws CertificateException { + List certificateExceptions = new ArrayList<>(); + for (X509ExtendedTrustManager trustManager : getTrustManagers()) { + try { + callBackConsumer.checkTrusted(trustManager); + return; + } catch (CertificateException e) { + certificateExceptions.add(e); + } + } + + CertificateException certificateException = new CertificateException(CERTIFICATE_EXCEPTION_MESSAGE); + certificateExceptions.forEach(certificateException::addSuppressed); + + throw certificateException; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/CompositeX509ExtendedTrustManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/CompositeX509ExtendedTrustManager.java new file mode 100644 index 0000000..ffbd243 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/CompositeX509ExtendedTrustManager.java @@ -0,0 +1,99 @@ +package org.xbib.net.security.ssl.trustmanager; + +import org.xbib.net.security.ssl.util.TrustManagerUtils; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * {@link CompositeX509ExtendedTrustManager} is a wrapper for a collection of TrustManagers. + * It has the ability to validate a certificate chain against multiple TrustManagers. + * If any one of the composed managers trusts a certificate chain, then it is trusted by the composite manager. + * The TrustManager can be build from one or more of any combination provided within the + * {@link TrustManagerUtils.TrustManagerBuilder TrustManagerUtils.TrustManagerBuilder}. + * This includes: + *
    + *     - Any amount of custom TrustManagers
    + *     - Any amount of custom TrustStores
    + * 
    + * + *

    + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link TrustManagerUtils TrustManagerUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + *

    + * + * @see + * http://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm + * + * @see + * http://codyaray.com/2013/04/java-ssl-with-multiple-keystores + * + * + */ +public final class CompositeX509ExtendedTrustManager extends X509ExtendedTrustManager implements CombinableX509TrustManager { + + private static final String CLIENT_CERTIFICATE_LOG_MESSAGE = "Received the following client certificate: [{0}]"; + + private static final String SERVER_CERTIFICATE_LOG_MESSAGE = "Received the following server certificate: [{0}]"; + + private final List trustManagers; + private final X509Certificate[] acceptedIssuers; + + public CompositeX509ExtendedTrustManager(List trustManagers) { + this.trustManagers = Collections.unmodifiableList(trustManagers); + this.acceptedIssuers = trustManagers.stream() + .map(X509ExtendedTrustManager::getAcceptedIssuers) + .flatMap(Arrays::stream) + .distinct() + .toArray(X509Certificate[]::new); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + checkTrusted(trustManager -> trustManager.checkClientTrusted(chain, authType)); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + checkTrusted(trustManager -> trustManager.checkClientTrusted(chain, authType, socket)); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + checkTrusted(trustManager -> trustManager.checkClientTrusted(chain, authType, sslEngine)); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + checkTrusted(trustManager -> trustManager.checkServerTrusted(chain, authType)); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + checkTrusted(trustManager -> trustManager.checkServerTrusted(chain, authType, socket)); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + checkTrusted(trustManager -> trustManager.checkServerTrusted(chain, authType, sslEngine)); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return Arrays.copyOf(acceptedIssuers, acceptedIssuers.length); + } + + @Override + public List getTrustManagers() { + return trustManagers; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/DelegatingTrustManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/DelegatingTrustManager.java new file mode 100644 index 0000000..1326ebb --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/DelegatingTrustManager.java @@ -0,0 +1,53 @@ +package org.xbib.net.security.ssl.trustmanager; + +import org.xbib.net.security.ssl.util.ValidationUtils; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +abstract class DelegatingTrustManager extends X509ExtendedTrustManager { + + T trustManager; + + DelegatingTrustManager(T trustManager) { + this.trustManager = ValidationUtils.requireNotNull(trustManager, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("TrustManager")); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + trustManager.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + trustManager.checkServerTrusted(chain, authType); + } + + @Override + public abstract void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException; + + @Override + public abstract void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException; + + @Override + public abstract void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException; + + @Override + public abstract void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException; + + @Override + public X509Certificate[] getAcceptedIssuers() { + X509Certificate[] acceptedIssuers = trustManager.getAcceptedIssuers(); + return Arrays.copyOf(acceptedIssuers, acceptedIssuers.length); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/DelegatingX509ExtendedTrustManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/DelegatingX509ExtendedTrustManager.java new file mode 100644 index 0000000..e59f248 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/DelegatingX509ExtendedTrustManager.java @@ -0,0 +1,39 @@ +package org.xbib.net.security.ssl.trustmanager; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +class DelegatingX509ExtendedTrustManager extends DelegatingTrustManager { + + DelegatingX509ExtendedTrustManager(X509ExtendedTrustManager trustManager) { + super(trustManager); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + trustManager.checkClientTrusted(chain, authType, socket); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + trustManager.checkClientTrusted(chain, authType, sslEngine); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + trustManager.checkServerTrusted(chain, authType, socket); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + trustManager.checkServerTrusted(chain, authType, sslEngine); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/DummyX509ExtendedTrustManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/DummyX509ExtendedTrustManager.java new file mode 100644 index 0000000..ca30da6 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/DummyX509ExtendedTrustManager.java @@ -0,0 +1,60 @@ +package org.xbib.net.security.ssl.trustmanager; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +public final class DummyX509ExtendedTrustManager extends X509ExtendedTrustManager { + + private static final X509ExtendedTrustManager INSTANCE = new DummyX509ExtendedTrustManager(); + private static final X509Certificate[] EMPTY_CERTIFICATES = new X509Certificate[0]; + private static final String MISSING_IMPLEMENTATION = "No X509ExtendedTrustManager implementation available"; + + private DummyX509ExtendedTrustManager() {} + + public static X509ExtendedTrustManager getInstance() { + return INSTANCE; + } + + @Override + public void checkClientTrusted(X509Certificate[] certificates, String authType) throws CertificateException { + throw new CertificateException(MISSING_IMPLEMENTATION); + } + + @Override + public void checkClientTrusted(X509Certificate[] certificates, String authType, Socket socket) throws CertificateException { + throw new CertificateException(MISSING_IMPLEMENTATION); + } + + @Override + public void checkClientTrusted(X509Certificate[] certificates, String authType, SSLEngine sslEngine) throws CertificateException { + throw new CertificateException(MISSING_IMPLEMENTATION); + } + + @Override + public void checkServerTrusted(X509Certificate[] certificates, String authType) throws CertificateException { + throw new CertificateException(MISSING_IMPLEMENTATION); + } + + @Override + public void checkServerTrusted(X509Certificate[] certificates, String authType, Socket socket) throws CertificateException { + throw new CertificateException(MISSING_IMPLEMENTATION); + } + + @Override + public void checkServerTrusted(X509Certificate[] certificates, String authType, SSLEngine sslEngine) throws CertificateException { + throw new CertificateException(MISSING_IMPLEMENTATION); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return EMPTY_CERTIFICATES; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/EnhanceableX509ExtendedTrustManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/EnhanceableX509ExtendedTrustManager.java new file mode 100644 index 0000000..678f072 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/EnhanceableX509ExtendedTrustManager.java @@ -0,0 +1,79 @@ +package org.xbib.net.security.ssl.trustmanager; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +public final class EnhanceableX509ExtendedTrustManager extends DelegatingX509ExtendedTrustManager { + + private final ChainAndAuthTypeValidator chainAndAuthTypeValidator; + private final ChainAndAuthTypeWithSocketValidator chainAndAuthTypeWithSocketValidator; + private final ChainAndAuthTypeWithSSLEngineValidator chainAndAuthTypeWithSSLEngineValidator; + + public EnhanceableX509ExtendedTrustManager( + X509ExtendedTrustManager trustManager, + ChainAndAuthTypeValidator chainAndAuthTypeValidator, + ChainAndAuthTypeWithSocketValidator chainAndAuthTypeWithSocketValidator, + ChainAndAuthTypeWithSSLEngineValidator chainAndAuthTypeWithSSLEngineValidator) { + + super(trustManager); + this.chainAndAuthTypeValidator = chainAndAuthTypeValidator; + this.chainAndAuthTypeWithSocketValidator = chainAndAuthTypeWithSocketValidator; + this.chainAndAuthTypeWithSSLEngineValidator = chainAndAuthTypeWithSSLEngineValidator; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (chainAndAuthTypeValidator != null && chainAndAuthTypeValidator.test(chain, authType)) { + return; + } + super.checkClientTrusted(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + if (chainAndAuthTypeWithSocketValidator != null && chainAndAuthTypeWithSocketValidator.test(chain, authType, socket)) { + return; + } + super.checkClientTrusted(chain, authType, socket); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + if (chainAndAuthTypeWithSSLEngineValidator != null && chainAndAuthTypeWithSSLEngineValidator.test(chain, authType, sslEngine)) { + return; + } + super.checkClientTrusted(chain, authType, sslEngine); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (chainAndAuthTypeValidator != null && chainAndAuthTypeValidator.test(chain, authType)) { + return; + } + super.checkServerTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + if (chainAndAuthTypeWithSocketValidator != null && chainAndAuthTypeWithSocketValidator.test(chain, authType, socket)) { + return; + } + super.checkServerTrusted(chain, authType, socket); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + if (chainAndAuthTypeWithSSLEngineValidator != null && chainAndAuthTypeWithSSLEngineValidator.test(chain, authType, sslEngine)) { + return; + } + super.checkServerTrusted(chain, authType, sslEngine); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/HotSwappableX509ExtendedTrustManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/HotSwappableX509ExtendedTrustManager.java new file mode 100644 index 0000000..ad6161c --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/HotSwappableX509ExtendedTrustManager.java @@ -0,0 +1,24 @@ +package org.xbib.net.security.ssl.trustmanager; + +import org.xbib.net.security.ssl.util.TrustManagerUtils; +import org.xbib.net.security.ssl.util.ValidationUtils; + +import javax.net.ssl.X509ExtendedTrustManager; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link TrustManagerUtils TrustManagerUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + */ +public class HotSwappableX509ExtendedTrustManager extends DelegatingX509ExtendedTrustManager { + + public HotSwappableX509ExtendedTrustManager(X509ExtendedTrustManager trustManager) { + super(trustManager); + } + + public void setTrustManager(X509ExtendedTrustManager trustManager) { + this.trustManager = ValidationUtils.requireNotNull(trustManager, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("TrustManager")); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustAnchorTrustOptions.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustAnchorTrustOptions.java new file mode 100644 index 0000000..60a82bf --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustAnchorTrustOptions.java @@ -0,0 +1,13 @@ +package org.xbib.net.security.ssl.trustmanager; + +import javax.net.ssl.CertPathTrustManagerParameters; +import java.security.cert.TrustAnchor; +import java.util.Set; + +@FunctionalInterface +public interface TrustAnchorTrustOptions extends TrustOptions, R> { + + @Override + R apply(Set trustAnchors) throws Exception; + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustManagerConsumer.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustManagerConsumer.java new file mode 100644 index 0000000..dac9ae3 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustManagerConsumer.java @@ -0,0 +1,15 @@ +package org.xbib.net.security.ssl.trustmanager; + +import javax.net.ssl.X509ExtendedTrustManager; +import java.security.cert.CertificateException; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +@FunctionalInterface +interface TrustManagerConsumer { + + void checkTrusted(X509ExtendedTrustManager trustManager) throws CertificateException; + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustManagerFactorySpiWrapper.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustManagerFactorySpiWrapper.java new file mode 100644 index 0000000..306d74d --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustManagerFactorySpiWrapper.java @@ -0,0 +1,36 @@ +package org.xbib.net.security.ssl.trustmanager; + +import org.xbib.net.security.ssl.util.ValidationUtils; + +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactorySpi; +import java.security.KeyStore; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + */ +class TrustManagerFactorySpiWrapper extends TrustManagerFactorySpi { + + private final TrustManager[] trustManagers; + + TrustManagerFactorySpiWrapper(TrustManager trustManager) { + ValidationUtils.requireNotNull(trustManager, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("TrustManager")); + this.trustManagers = new TrustManager[]{trustManager}; + } + + @Override + protected void engineInit(KeyStore keyStore) { + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { + } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return trustManagers; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustManagerFactoryWrapper.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustManagerFactoryWrapper.java new file mode 100644 index 0000000..0e4c06c --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustManagerFactoryWrapper.java @@ -0,0 +1,24 @@ +package org.xbib.net.security.ssl.trustmanager; + +import org.xbib.net.security.ssl.util.TrustManagerUtils; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import java.security.Provider; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link TrustManagerUtils TrustManagerUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + */ +public final class TrustManagerFactoryWrapper extends TrustManagerFactory { + + private static final String TRUST_MANAGER_FACTORY_ALGORITHM = "no-algorithm"; + + private static final Provider PROVIDER = new Provider("", "1.0", "") {}; + + public TrustManagerFactoryWrapper(TrustManager trustManager) { + super(new TrustManagerFactorySpiWrapper(trustManager), PROVIDER, TRUST_MANAGER_FACTORY_ALGORITHM); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustOptions.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustOptions.java new file mode 100644 index 0000000..611fbca --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustOptions.java @@ -0,0 +1,10 @@ +package org.xbib.net.security.ssl.trustmanager; + +import javax.net.ssl.ManagerFactoryParameters; + +@FunctionalInterface +public interface TrustOptions { + + R apply(T input) throws Exception; + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustStoreTrustOptions.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustStoreTrustOptions.java new file mode 100644 index 0000000..8ab1c89 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/TrustStoreTrustOptions.java @@ -0,0 +1,12 @@ +package org.xbib.net.security.ssl.trustmanager; + +import javax.net.ssl.CertPathTrustManagerParameters; +import java.security.KeyStore; + +@FunctionalInterface +public interface TrustStoreTrustOptions extends TrustOptions { + + @Override + R apply(KeyStore trustStore) throws Exception; + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/UnsafeX509ExtendedTrustManager.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/UnsafeX509ExtendedTrustManager.java new file mode 100644 index 0000000..dacbbca --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/UnsafeX509ExtendedTrustManager.java @@ -0,0 +1,77 @@ +package org.xbib.net.security.ssl.trustmanager; + +import org.xbib.net.security.ssl.util.TrustManagerUtils; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.Socket; +import java.security.cert.X509Certificate; + +/** + * An insecure {@link UnsafeX509ExtendedTrustManager TrustManager} that trusts all X.509 certificates without any verification. + *

    + * NOTE: + * Never use this {@link UnsafeX509ExtendedTrustManager} in production. + * It is purely for testing purposes, and thus it is very insecure. + *

    + *
    + * Suppressed warning: java:S4830 - "Server certificates should be verified during SSL/TLS connections" + * This TrustManager doesn't validate certificates and should not be used at production. + * It is just meant to be used for testing purposes and it is designed not to verify server certificates. + * + *

    + *
    + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link TrustManagerUtils TrustManagerUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + *

    + */ +@SuppressWarnings("java:S4830") +public final class UnsafeX509ExtendedTrustManager extends X509ExtendedTrustManager { + + private static final X509ExtendedTrustManager INSTANCE = new UnsafeX509ExtendedTrustManager(); + private static final X509Certificate[] EMPTY_CERTIFICATES = new X509Certificate[0]; + + private UnsafeX509ExtendedTrustManager() {} + + public static X509ExtendedTrustManager getInstance() { + return INSTANCE; + } + + @Override + public void checkClientTrusted(X509Certificate[] certificates, String authType) { + // ignore certificate validation + } + + @Override + public void checkClientTrusted(X509Certificate[] certificates, String authType, Socket socket) { + // ignore certificate validation + } + + @Override + public void checkClientTrusted(X509Certificate[] certificates, String authType, SSLEngine sslEngine) { + // ignore certificate validation + } + + @Override + public void checkServerTrusted(X509Certificate[] certificates, String authType) { + // ignore certificate validation + } + + @Override + public void checkServerTrusted(X509Certificate[] certificates, String authType, Socket socket) { + // ignore certificate validation + } + + @Override + public void checkServerTrusted(X509Certificate[] certificates, String authType, SSLEngine sslEngine) { + // ignore certificate validation + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return EMPTY_CERTIFICATES; + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/X509TrustManagerWrapper.java b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/X509TrustManagerWrapper.java new file mode 100644 index 0000000..81cb83d --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/trustmanager/X509TrustManagerWrapper.java @@ -0,0 +1,42 @@ +package org.xbib.net.security.ssl.trustmanager; + +import org.xbib.net.security.ssl.util.TrustManagerUtils; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509TrustManager; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * NOTE: + * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time. + * Instead use the {@link TrustManagerUtils TrustManagerUtils} which provides the same functionality + * while it has a stable API because it is part of the public API. + */ +public final class X509TrustManagerWrapper extends DelegatingTrustManager { + + public X509TrustManagerWrapper(X509TrustManager trustManager) { + super(trustManager); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + trustManager.checkClientTrusted(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + trustManager.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + trustManager.checkServerTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + trustManager.checkServerTrusted(chain, authType); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/CertificateExtractorUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/CertificateExtractorUtils.java new file mode 100644 index 0000000..e602aa9 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/CertificateExtractorUtils.java @@ -0,0 +1,165 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.SSLFactory; +import org.xbib.net.security.ssl.exception.GenericCertificateException; +import org.xbib.net.security.ssl.exception.GenericIOException; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +class CertificateExtractorUtils { + + private static final Pattern CA_ISSUERS_AUTHORITY_INFO_ACCESS = Pattern.compile("(?s)^AuthorityInfoAccess\\h+\\[\\R\\s*\\[\\R.*?accessMethod:\\h+caIssuers\\R\\h*accessLocation: URIName:\\h+(https?://\\S+)", Pattern.MULTILINE); + + private static CertificateExtractorUtils instance; + + private final SSLFactory sslFactory; + private final SSLSocketFactory unsafeSslSocketFactory; + private final SSLSocketFactory certificateCapturingSslSocketFactory; + private final List certificatesCollector; + + private CertificateExtractorUtils() { + certificatesCollector = new ArrayList<>(); + + X509ExtendedTrustManager certificateCapturingTrustManager = TrustManagerUtils.createCertificateCapturingTrustManager(certificatesCollector); + + sslFactory = SSLFactory.builder() + .withTrustMaterial(certificateCapturingTrustManager) + .build(); + + certificateCapturingSslSocketFactory = sslFactory.getSslSocketFactory(); + unsafeSslSocketFactory = SSLSocketUtils.createUnsafeSslSocketFactory(); + } + + static CertificateExtractorUtils getInstance() { + if (instance == null) { + instance = new CertificateExtractorUtils(); + } else { + instance.certificatesCollector.clear(); + SSLSessionUtils.invalidateCaches(instance.sslFactory); + } + return instance; + } + + List getCertificateFromExternalSource(String url) { + try { + URL parsedUrl = new URL(url); + if ("https".equalsIgnoreCase(parsedUrl.getProtocol())) { + HttpsURLConnection connection = (HttpsURLConnection) parsedUrl.openConnection(); + connection.setSSLSocketFactory(certificateCapturingSslSocketFactory); + connection.connect(); + connection.disconnect(); + + List rootCa = getRootCaFromChainIfPossible(certificatesCollector); + return Stream.of(certificatesCollector, rootCa) + .flatMap(Collection::stream) + .distinct() + .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + } else { + return Collections.emptyList(); + } + } catch (IOException e) { + throw new GenericIOException(String.format("Failed getting certificate from: [%s]", url), e); + } + } + + List getRootCaFromChainIfPossible(List certificates) { + if (!certificates.isEmpty()) { + X509Certificate certificate = certificates.get(certificates.size() - 1); + String issuer = certificate.getIssuerX500Principal().getName(); + String subject = certificate.getSubjectX500Principal().getName(); + + boolean isSelfSignedCertificate = issuer.equals(subject); + if (!isSelfSignedCertificate) { + return getRootCaIfPossible(certificate); + } + } + return Collections.emptyList(); + } + + List getRootCaIfPossible(X509Certificate x509Certificate) { + List rootCaFromAuthorityInfoAccessExtension = getRootCaFromAuthorityInfoAccessExtensionIfPresent(x509Certificate); + if (!rootCaFromAuthorityInfoAccessExtension.isEmpty()) { + return rootCaFromAuthorityInfoAccessExtension; + } + + List rootCaFromJdkTrustedCertificates = getRootCaFromJdkTrustedCertificates(x509Certificate); + if (!rootCaFromJdkTrustedCertificates.isEmpty()) { + return rootCaFromJdkTrustedCertificates; + } + + return Collections.emptyList(); + } + + List getRootCaFromAuthorityInfoAccessExtensionIfPresent(X509Certificate certificate) { + String certificateContent = certificate.toString(); + Matcher caIssuersMatcher = CA_ISSUERS_AUTHORITY_INFO_ACCESS.matcher(certificateContent); + if (caIssuersMatcher.find()) { + String issuerLocation = caIssuersMatcher.group(1); + return getCertificatesFromRemoteFile(URI.create(issuerLocation), certificate); + } + + return Collections.emptyList(); + } + + List getCertificatesFromRemoteFile(URI uri, X509Certificate intermediateCertificate) { + try { + URL url = uri.toURL(); + URLConnection connection = url.openConnection(); + if (connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setSSLSocketFactory(unsafeSslSocketFactory); + } + + InputStream inputStream = connection.getInputStream(); + List certificates = CertificateUtils.parseDerCertificate(inputStream).stream() + .filter(X509Certificate.class::isInstance) + .map(X509Certificate.class::cast) + .filter(issuer -> isIssuerOfIntermediateCertificate(intermediateCertificate, issuer)) + .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + + inputStream.close(); + + return certificates; + } catch (IOException e) { + throw new GenericCertificateException(e); + } + } + + List getRootCaFromJdkTrustedCertificates(X509Certificate intermediateCertificate) { + List jdkTrustedCertificates = CertificateUtils.getJdkTrustedCertificates(); + + return jdkTrustedCertificates.stream() + .filter(issuer -> isIssuerOfIntermediateCertificate(intermediateCertificate, issuer)) + .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + } + + boolean isIssuerOfIntermediateCertificate(X509Certificate intermediateCertificate, X509Certificate issuer) { + try { + intermediateCertificate.verify(issuer.getPublicKey()); + return true; + } catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException | SignatureException e) { + return false; + } + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/CertificateUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/CertificateUtils.java new file mode 100644 index 0000000..a4ba120 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/CertificateUtils.java @@ -0,0 +1,260 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.exception.GenericCertificateException; +import org.xbib.net.security.ssl.exception.GenericIOException; + +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class CertificateUtils { + + private static final String CERTIFICATE_TYPE = "X.509"; + private static final String P7B_HEADER = "-----BEGIN PKCS7-----"; + private static final String P7B_FOOTER = "-----END PKCS7-----"; + private static final String PEM_HEADER = "-----BEGIN CERTIFICATE-----"; + private static final String PEM_FOOTER = "-----END CERTIFICATE-----"; + private static final Pattern PEM_PATTERN = Pattern.compile(PEM_HEADER + "(.*?)" + PEM_FOOTER, Pattern.DOTALL); + private static final Pattern P7B_PATTERN = Pattern.compile(P7B_HEADER + "(.*?)" + P7B_FOOTER, Pattern.DOTALL); + + private static final String EMPTY = ""; + + private CertificateUtils() {} + + public static String generateAlias(Certificate certificate) { + if (certificate instanceof X509Certificate) { + return ((X509Certificate) certificate) + .getSubjectX500Principal() + .getName() + .toLowerCase(Locale.ENGLISH); + } else { + return UUID.randomUUID().toString().toLowerCase(Locale.ENGLISH); + } + } + + /** + * Loads certificates from the classpath and maps it into a list of {@link Certificate}. + *
    + * Supported input format: PEM, P7B and DER + */ + public static List loadCertificate(String... certificatePaths) { + return loadCertificate(certificatePath -> + ValidationUtils.requireNotNull( + CertificateUtils.class.getClassLoader().getResourceAsStream(certificatePath), + String.format("Failed to load the certificate from the classpath for the given path: [%s]", certificatePath)), + certificatePaths + ); + } + + /** + * Loads certificates from the filesystem and maps it into a list of {@link Certificate}. + *
    + * Supported input format: PEM, P7B and DER + */ + public static List loadCertificate(Path... certificatePaths) { + return loadCertificate(certificatePath -> { + try { + return Files.newInputStream(certificatePath, StandardOpenOption.READ); + } catch (IOException exception) { + throw new GenericIOException(exception); + } + }, certificatePaths); + } + + /** + * Loads certificates from multiple InputStreams and maps it into a list of {@link Certificate}. + *
    + * Supported input format: PEM, P7B and DER + */ + public static List loadCertificate(InputStream... certificateStreams) { + return loadCertificate(certificateStream -> + ValidationUtils.requireNotNull(certificateStream, "Failed to load the certificate from the provided InputStream because it is null"), + certificateStreams + ); + } + + private static List loadCertificate(Function resourceMapper, T[] resources) { + List certificates = new ArrayList<>(); + for (T resource : resources) { + try (InputStream certificateStream = resourceMapper.apply(resource)) { + certificates.addAll(parseCertificate(certificateStream)); + } catch (Exception e) { + throw new GenericIOException(e); + } + } + + return Collections.unmodifiableList(certificates); + } + + /** + * Tries to map the InputStream to a list of {@link Certificate}. + * It assumes that the content of the InputStream is either PEM, P7B or DER. + * The InputStream will copied into an OutputStream so it can be read multiple times. + */ + private static List parseCertificate(InputStream certificateStream) { + List certificates; + byte[] certificateData = IOUtils.copyToByteArray(certificateStream); + String certificateContent = new String(certificateData, StandardCharsets.UTF_8); + + if (isPemFormatted(certificateContent)) { + certificates = parsePemCertificate(certificateContent); + } else if(isP7bFormatted(certificateContent)) { + certificates = parseP7bCertificate(certificateContent); + } else { + certificates = parseDerCertificate(new ByteArrayInputStream(certificateData)); + } + + return certificates; + } + + private static boolean isPemFormatted(String certificateContent) { + return PEM_PATTERN.matcher(certificateContent).find(); + } + + private static boolean isP7bFormatted(String certificateContent) { + return P7B_PATTERN.matcher(certificateContent).find(); + } + + /** + * Parses PEM formatted certificates containing a + * header as -----BEGIN CERTIFICATE----- and footer as -----END CERTIFICATE----- + * or header as -----BEGIN PKCS7----- and footer as -----END PKCS7----- + * with a base64 encoded data between the header and footer. + */ + public static List parsePemCertificate(String certificateContent) { + Matcher pemMatcher = PEM_PATTERN.matcher(certificateContent); + return parseCertificate(pemMatcher); + } + + /** + * Parses P7B formatted certificates containing a + * header as -----BEGIN PKCS7----- and footer as -----END PKCS7----- + * with a base64 encoded data between the header and footer. + */ + public static List parseP7bCertificate(String certificateContent) { + Matcher p7bMatcher = P7B_PATTERN.matcher(certificateContent); + return parseCertificate(p7bMatcher); + } + + private static List parseCertificate(Matcher certificateMatcher) { + List certificates = new ArrayList<>(); + while (certificateMatcher.find()) { + String certificate = certificateMatcher.group(1); + String sanitizedCertificate = certificate.replaceAll("[\\n|\\r]+", EMPTY).trim(); + byte[] decodedCertificate = Base64.getDecoder().decode(sanitizedCertificate); + ByteArrayInputStream certificateAsInputStream = new ByteArrayInputStream(decodedCertificate); + List parsedCertificates = CertificateUtils.parseDerCertificate(certificateAsInputStream); + certificates.addAll(parsedCertificates); + IOUtils.closeSilently(certificateAsInputStream); + } + + return Collections.unmodifiableList(certificates); + } + + public static List parseDerCertificate(InputStream certificateStream) { + try(BufferedInputStream bufferedCertificateStream = new BufferedInputStream(certificateStream)) { + CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_TYPE); + return certificateFactory.generateCertificates(bufferedCertificateStream).stream() + .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + } catch (CertificateException | IOException e) { + throw new GenericCertificateException("There is no valid certificate present to parse. Please make sure to supply a valid der formatted certificate", e); + } + } + + public static List getJdkTrustedCertificates() { + return Collections.unmodifiableList( + Arrays.asList( + TrustManagerUtils.createTrustManagerWithJdkTrustedCertificates().getAcceptedIssuers() + ) + ); + } + + public static List getSystemTrustedCertificates() { + return TrustManagerUtils.createTrustManagerWithSystemTrustedCertificates() + .map(X509TrustManager::getAcceptedIssuers) + .map(Arrays::asList) + .map(Collections::unmodifiableList) + .orElseGet(Collections::emptyList); + } + + public static Map> getCertificateAsPem(String... urls) { + return getCertificateAsPem(Arrays.asList(urls)); + } + + public static Map> getCertificateAsPem(List urls) { + Map> certificates = CertificateUtils.getCertificate(urls) + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> CertificateUtils.convertToPem(entry.getValue()))); + + return Collections.unmodifiableMap(certificates); + } + + public static Map> getCertificate(String... urls) { + return CertificateUtils.getCertificate(Arrays.asList(urls)); + } + + public static Map> getCertificate(List urls) { + return urls.stream() + .map(url -> new AbstractMap.SimpleEntry<>(url, CertificateExtractorUtils.getInstance().getCertificateFromExternalSource(url))) + .collect(Collectors.collectingAndThen(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue), Collections::unmodifiableMap)); + } + + public static List convertToPem(List certificates) { + return certificates.stream() + .map(CertificateUtils::convertToPem) + .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + } + + public static String convertToPem(Certificate certificate) { + try { + byte[] encodedCertificate = certificate.getEncoded(); + byte[] base64EncodedCertificate = Base64.getEncoder().encode(encodedCertificate); + String parsedCertificate = new String(base64EncodedCertificate); + + List certificateContainer = Stream.of(parsedCertificate.split("(?<=\\G.{64})")) + .collect(Collectors.toCollection(ArrayList::new)); + certificateContainer.add(0, PEM_HEADER); + certificateContainer.add(PEM_FOOTER); + + if (certificate instanceof X509Certificate) { + X509Certificate x509Certificate = (X509Certificate) certificate; + X500Principal issuer = x509Certificate.getIssuerX500Principal(); + certificateContainer.add(0, String.format("issuer=%s", issuer.getName())); + X500Principal subject = x509Certificate.getSubjectX500Principal(); + certificateContainer.add(0, String.format("subject=%s", subject.getName())); + } + + return String.join(System.lineSeparator(), certificateContainer); + } catch (CertificateEncodingException e) { + throw new GenericCertificateException(e); + } + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/HostnameVerifierUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/HostnameVerifierUtils.java new file mode 100644 index 0000000..3ae8b0b --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/HostnameVerifierUtils.java @@ -0,0 +1,20 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.hostnameverifier.BasicHostNameVerifier; +import org.xbib.net.security.ssl.hostnameverifier.UnsafeHostNameVerifier; + +import javax.net.ssl.HostnameVerifier; + +public final class HostnameVerifierUtils { + + private HostnameVerifierUtils() {} + + public static HostnameVerifier createBasic() { + return BasicHostNameVerifier.getInstance(); + } + + public static HostnameVerifier createUnsafe() { + return UnsafeHostNameVerifier.getInstance(); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/IOUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/IOUtils.java new file mode 100644 index 0000000..0f5deea --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/IOUtils.java @@ -0,0 +1,66 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.exception.GenericIOException; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.stream.Collectors; + +public final class IOUtils { + + private IOUtils() {} + + static String getContent(InputStream inputStream) { + try (InputStreamReader inputStreamReader = new InputStreamReader(ValidationUtils.requireNotNull(inputStream, + ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("InputStream")), StandardCharsets.UTF_8); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + + return bufferedReader.lines() + .collect(Collectors.joining(System.lineSeparator())); + } catch (Exception e) { + throw new GenericIOException(e); + } + } + + static byte[] copyToByteArray(InputStream inputStream) { + try(ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) > -1) { + outputStream.write(buffer, 0, length); + } + outputStream.flush(); + return outputStream.toByteArray(); + } catch (Exception e) { + throw new GenericIOException(e); + } + } + + static void closeSilently(AutoCloseable autoCloseable) { + try { + autoCloseable.close(); + } catch (Exception ignored) { + //ignore exception + } + } + + static InputStream getResourceAsStream(String name) { + return IOUtils.class.getClassLoader().getResourceAsStream(name); + } + + static InputStream getFileAsStream(Path path) { + try { + return Files.newInputStream(path, StandardOpenOption.READ); + } catch (IOException e) { + throw new GenericIOException(e); + } + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/KeyManagerUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/KeyManagerUtils.java new file mode 100644 index 0000000..b229c8f --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/KeyManagerUtils.java @@ -0,0 +1,341 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.exception.GenericKeyManagerException; +import org.xbib.net.security.ssl.keymanager.CompositeX509ExtendedKeyManager; +import org.xbib.net.security.ssl.keymanager.DummyX509ExtendedKeyManager; +import org.xbib.net.security.ssl.keymanager.HotSwappableX509ExtendedKeyManager; +import org.xbib.net.security.ssl.keymanager.KeyManagerFactoryWrapper; +import org.xbib.net.security.ssl.keymanager.X509KeyManagerWrapper; +import org.xbib.net.security.ssl.model.KeyStoreHolder; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; +import java.net.URI; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public final class KeyManagerUtils { + + private KeyManagerUtils() {} + + public static X509ExtendedKeyManager combine(X509KeyManager... keyManagers) { + return combine(Arrays.asList(keyManagers)); + } + + public static X509ExtendedKeyManager combine(List keyManagers) { + return KeyManagerUtils.keyManagerBuilder() + .withKeyManagers(keyManagers) + .build(); + } + + public static X509ExtendedKeyManager[] toArray(T keyManager) { + return new X509ExtendedKeyManager[]{KeyManagerUtils.wrapIfNeeded(keyManager)}; + } + + public static X509ExtendedKeyManager createKeyManager(KeyStoreHolder... keyStoreHolders) { + return Arrays.stream(keyStoreHolders) + .map(keyStoreHolder -> createKeyManager(keyStoreHolder.getKeyStore(), keyStoreHolder.getKeyPassword())) + .collect(Collectors.collectingAndThen(Collectors.toList(), KeyManagerUtils::combine)); + } + + public static X509ExtendedKeyManager createKeyManager(KeyStore keyStore, char[] keyPassword) { + return createKeyManager(keyStore, keyPassword, KeyManagerFactory.getDefaultAlgorithm()); + } + + public static X509ExtendedKeyManager createKeyManager(KeyStore keyStore, char[] keyPassword, String keyManagerFactoryAlgorithm) { + try { + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm); + return createKeyManager(keyStore, keyPassword, keyManagerFactory); + } catch (NoSuchAlgorithmException e) { + throw new GenericKeyManagerException(e); + } + } + + public static X509ExtendedKeyManager createKeyManager(KeyStore keyStore, char[] keyPassword, String keyManagerFactoryAlgorithm, String securityProviderName) { + try { + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm, securityProviderName); + return createKeyManager(keyStore, keyPassword, keyManagerFactory); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new GenericKeyManagerException(e); + } + } + + public static X509ExtendedKeyManager createKeyManager(KeyStore keyStore, char[] keyPassword, String keyManagerFactoryAlgorithm, Provider securityProvider) { + try { + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm, securityProvider); + return createKeyManager(keyStore, keyPassword, keyManagerFactory); + } catch (NoSuchAlgorithmException e) { + throw new GenericKeyManagerException(e); + } + } + + public static X509ExtendedKeyManager createKeyManager(KeyStore keyStore, char[] keyPassword, KeyManagerFactory keyManagerFactory) { + try { + keyManagerFactory.init(keyStore, keyPassword); + return KeyManagerUtils.getKeyManager(keyManagerFactory); + } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { + throw new GenericKeyManagerException(e); + } + } + + public static X509ExtendedKeyManager createKeyManager(KeyStore keyStore, Map aliasToPassword) { + List keyManagers = new ArrayList<>(); + + for (Entry entry : aliasToPassword.entrySet()) { + try { + String alias = entry.getKey(); + char[] password = entry.getValue(); + + if (keyStore.isKeyEntry(alias)) { + Key key = keyStore.getKey(alias, password); + Certificate[] certificateChain = keyStore.getCertificateChain(alias); + + KeyStore identityStore = KeyStoreUtils.createIdentityStore(key, password, certificateChain); + X509ExtendedKeyManager keyManager = KeyManagerUtils.createKeyManager(identityStore, password); + keyManagers.add(keyManager); + } + } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { + throw new GenericKeyManagerException(e); + } + } + + ValidationUtils.requireNotEmpty(keyManagers, () -> new GenericKeyManagerException("Could not create any KeyManager from the given KeyStore, Alias and Password")); + return KeyManagerUtils.combine(keyManagers); + } + + public static X509ExtendedKeyManager wrapIfNeeded(X509KeyManager keyManager) { + if (keyManager instanceof X509ExtendedKeyManager) { + return (X509ExtendedKeyManager) keyManager; + } else { + return new X509KeyManagerWrapper(keyManager); + } + } + + public static KeyManagerFactory createKeyManagerFactory(KeyManager keyManager) { + return new KeyManagerFactoryWrapper(keyManager); + } + + public static X509ExtendedKeyManager getKeyManager(T keyManagerFactory) { + return Arrays.stream(keyManagerFactory.getKeyManagers()) + .filter(X509KeyManager.class::isInstance) + .map(X509KeyManager.class::cast) + .map(KeyManagerUtils::wrapIfNeeded) + .collect(Collectors.collectingAndThen(Collectors.toList(), KeyManagerUtils::combine)); + } + + public static X509ExtendedKeyManager createDummyKeyManager() { + return DummyX509ExtendedKeyManager.getInstance(); + } + + /** + * Wraps the given KeyManager into an instance of a Hot Swappable KeyManager + * This type of KeyManager has the capability of swapping in and out different KeyManagers at runtime. + * + * @param keyManager To be wrapped KeyManager + * @return Swappable KeyManager + */ + public static X509ExtendedKeyManager createSwappableKeyManager(X509KeyManager keyManager) { + return new HotSwappableX509ExtendedKeyManager(KeyManagerUtils.wrapIfNeeded(keyManager)); + } + + /** + * Swaps the internal KeyManager instance with the given keyManager object. + * The baseKeyManager should be an instance of {@link HotSwappableX509ExtendedKeyManager} + * and can be created with {@link KeyManagerUtils#createSwappableKeyManager(X509KeyManager)} + * + * @param baseKeyManager an instance of {@link HotSwappableX509ExtendedKeyManager} + * @param newKeyManager to be injected instance of a KeyManager + * @throws GenericKeyManagerException if {@code baseKeyManager} is not instance of {@link HotSwappableX509ExtendedKeyManager} + */ + public static void swapKeyManager(X509KeyManager baseKeyManager, X509KeyManager newKeyManager) { + if (newKeyManager instanceof HotSwappableX509ExtendedKeyManager) { + throw new GenericKeyManagerException( + String.format("The newKeyManager should not be an instance of [%s]", HotSwappableX509ExtendedKeyManager.class.getName()) + ); + } + + if (baseKeyManager instanceof HotSwappableX509ExtendedKeyManager) { + ((HotSwappableX509ExtendedKeyManager) baseKeyManager).setKeyManager(KeyManagerUtils.wrapIfNeeded(newKeyManager)); + } else { + throw new GenericKeyManagerException( + String.format("The baseKeyManager is from the instance of [%s] and should be an instance of [%s].", + baseKeyManager.getClass().getName(), + HotSwappableX509ExtendedKeyManager.class.getName()) + ); + } + } + + public static void addIdentityRoute(X509ExtendedKeyManager keyManager, String alias, String... hosts) { + addIdentityRoute(keyManager, alias, hosts, false); + } + + public static void overrideIdentityRoute(X509ExtendedKeyManager keyManager, String alias, String... hosts) { + addIdentityRoute(keyManager, alias, hosts, true); + } + + private static void addIdentityRoute(X509ExtendedKeyManager keyManager, + String alias, + String[] hosts, + boolean overrideExistingRouteEnabled) { + + ValidationUtils.requireNotNull(keyManager, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("KeyManager")); + ValidationUtils.requireNotNull(alias, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("Alias")); + ValidationUtils.requireNotNull(keyManager, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("Host")); + + if (keyManager instanceof CompositeX509ExtendedKeyManager) { + CompositeX509ExtendedKeyManager compositeX509ExtendedKeyManager = (CompositeX509ExtendedKeyManager) keyManager; + Map> aliasToHosts = compositeX509ExtendedKeyManager.getIdentityRoute(); + + List uris = new ArrayList<>(); + for (String host : hosts) { + URI uri = URI.create(host); + UriUtils.validate(uri); + uris.add(uri); + } + + if (overrideExistingRouteEnabled && aliasToHosts.containsKey(alias)) { + aliasToHosts.get(alias).clear(); + } + + for (URI uri : uris) { + if (aliasToHosts.containsKey(alias)) { + aliasToHosts.get(alias).add(uri); + } else { + aliasToHosts.put(alias, new ArrayList<>(Collections.singleton(uri))); + } + } + } else { + throw new GenericKeyManagerException(String.format( + "KeyManager should be an instance of: [%s], but received: [%s]", + CompositeX509ExtendedKeyManager.class.getName(), + keyManager.getClass().getName())); + } + } + + public static Map> getIdentityRoute(X509ExtendedKeyManager keyManager) { + ValidationUtils.requireNotNull(keyManager, ValidationUtils.GENERIC_EXCEPTION_MESSAGE.apply("KeyManager")); + + if (keyManager instanceof CompositeX509ExtendedKeyManager) { + return ((CompositeX509ExtendedKeyManager) keyManager) + .getIdentityRoute() + .entrySet().stream() + .collect(Collectors.collectingAndThen( + Collectors.toMap( + Entry::getKey, + hosts -> hosts.getValue().stream() + .map(URI::toString) + .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList))), + Collections::unmodifiableMap) + ); + } else { + throw new GenericKeyManagerException(String.format( + "KeyManager should be an instance of: [%s], but received: [%s]", + CompositeX509ExtendedKeyManager.class.getName(), + keyManager.getClass().getName())); + } + } + + private static List unwrapIfPossible(X509ExtendedKeyManager keyManager) { + if (keyManager instanceof CompositeX509ExtendedKeyManager) { + List keyManagers = new ArrayList<>(); + for (X509ExtendedKeyManager innerKeyManager : ((CompositeX509ExtendedKeyManager) keyManager).getKeyManagers()) { + List unwrappedKeyManagers = KeyManagerUtils.unwrapIfPossible(innerKeyManager); + keyManagers.addAll(unwrappedKeyManagers); + } + return keyManagers; + } else { + return Collections.singletonList(keyManager); + } + } + + public static KeyManagerBuilder keyManagerBuilder() { + return new KeyManagerBuilder(); + } + + public static final class KeyManagerBuilder { + + private static final String EMPTY_KEY_MANAGER_EXCEPTION = "Input does not contain KeyManagers"; + + private final List keyManagers = new ArrayList<>(); + private final Map> aliasToHost = new HashMap<>(); + private boolean swappableKeyManagerEnabled = false; + + private KeyManagerBuilder() {} + + public KeyManagerBuilder withKeyManagers(List keyManagers) { + for (X509KeyManager keyManager : keyManagers) { + withKeyManager(keyManager); + } + return this; + } + + public KeyManagerBuilder withKeyManager(T keyManager) { + this.keyManagers.add(KeyManagerUtils.wrapIfNeeded(keyManager)); + return this; + } + + public KeyManagerBuilder withIdentities(List identities) { + for (KeyStoreHolder identity : identities) { + this.keyManagers.add(KeyManagerUtils.createKeyManager(identity.getKeyStore(), identity.getKeyPassword())); + } + return this; + } + + public KeyManagerBuilder withIdentity(T identity, char[] identityPassword, String keyManagerAlgorithm) { + this.keyManagers.add(KeyManagerUtils.createKeyManager(identity, identityPassword, keyManagerAlgorithm)); + return this; + } + + public KeyManagerBuilder withSwappableKeyManager(boolean swappableKeyManagerEnabled) { + this.swappableKeyManagerEnabled = swappableKeyManagerEnabled; + return this; + } + + public KeyManagerBuilder withIdentityRoute(Map> aliasToHost) { + this.aliasToHost.putAll(aliasToHost); + return this; + } + + public X509ExtendedKeyManager build() { + ValidationUtils.requireNotEmpty(keyManagers, () -> new GenericKeyManagerException(EMPTY_KEY_MANAGER_EXCEPTION)); + + X509ExtendedKeyManager keyManager; + if (keyManagers.size() == 1) { + keyManager = keyManagers.get(0); + } else { + keyManager = keyManagers.stream() + .map(KeyManagerUtils::unwrapIfPossible) + .flatMap(Collection::stream) + .collect(Collectors.collectingAndThen( + Collectors.toList(), + extendedKeyManagers -> new CompositeX509ExtendedKeyManager(extendedKeyManagers, aliasToHost) + )); + } + + if (swappableKeyManagerEnabled) { + keyManager = KeyManagerUtils.createSwappableKeyManager(keyManager); + } + + return keyManager; + } + + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/KeyStoreUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/KeyStoreUtils.java new file mode 100644 index 0000000..7e49660 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/KeyStoreUtils.java @@ -0,0 +1,213 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.exception.GenericKeyStoreException; + +import javax.net.ssl.X509TrustManager; + +import static java.util.Objects.isNull; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +public final class KeyStoreUtils { + + private static final String KEYSTORE_TYPE = "PKCS12"; + + private KeyStoreUtils() {} + + public static KeyStore loadKeyStore(String keystorePath, char[] keystorePassword) { + return loadKeyStore(keystorePath, keystorePassword, KeyStore.getDefaultType()); + } + + public static KeyStore loadKeyStore(String keystorePath, char[] keystorePassword, String keystoreType) { + try (InputStream keystoreInputStream = KeyStoreUtils.class.getClassLoader().getResourceAsStream(keystorePath)) { + return loadKeyStore(keystoreInputStream, keystorePassword, keystoreType); + } catch (Exception e) { + throw new GenericKeyStoreException(e); + } + } + + public static KeyStore loadKeyStore(Path keystorePath, char[] keystorePassword) { + return loadKeyStore(keystorePath, keystorePassword, KeyStore.getDefaultType()); + } + + public static KeyStore loadKeyStore(Path keystorePath, char[] keystorePassword, String keystoreType) { + try (InputStream keystoreInputStream = Files.newInputStream(keystorePath, StandardOpenOption.READ)) { + return loadKeyStore(keystoreInputStream, keystorePassword, keystoreType); + } catch (Exception e) { + throw new GenericKeyStoreException(e); + } + } + + public static KeyStore loadKeyStore(InputStream keystoreInputStream, char[] keystorePassword) { + return loadKeyStore(keystoreInputStream, keystorePassword, KeyStore.getDefaultType()); + } + + public static KeyStore loadKeyStore(InputStream keystoreInputStream, char[] keystorePassword, String keystoreType) { + if (isNull(keystoreInputStream)) { + throw new GenericKeyStoreException("KeyStore is not present for the giving input"); + } + try { + KeyStore keystore = KeyStore.getInstance(keystoreType); + keystore.load(keystoreInputStream, keystorePassword); + return keystore; + } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { + throw new GenericKeyStoreException(e); + } + } + + public static KeyStore createIdentityStore(Key privateKey, + char[] privateKeyPassword, + String alias, + Collection certificateChain) { + return createIdentityStore(privateKey, privateKeyPassword, alias, certificateChain.toArray(new Certificate[]{})); + } + + public static KeyStore createIdentityStore(Key privateKey, + char[] privateKeyPassword, + Collection certificateChain) { + return createIdentityStore(privateKey, privateKeyPassword, null, certificateChain.toArray(new Certificate[]{})); + } + + public static KeyStore createIdentityStore(Key privateKey, + char[] privateKeyPassword, + T[] certificateChain) { + return createIdentityStore(privateKey, privateKeyPassword, null, certificateChain); + } + + public static KeyStore createIdentityStore(Key privateKey, + char[] privateKeyPassword, + String alias, + T[] certificateChain) { + try { + KeyStore keyStore = createKeyStore(); + String privateKeyAlias = StringUtils.isBlank(alias) ? CertificateUtils.generateAlias(certificateChain[0]) : alias; + keyStore.setKeyEntry(privateKeyAlias, privateKey, privateKeyPassword, certificateChain); + return keyStore; + } catch (KeyStoreException e) { + throw new GenericKeyStoreException(e); + } + } + + public static KeyStore createKeyStore() { + return createKeyStore("".toCharArray()); + } + + public static KeyStore createKeyStore(char[] keyStorePassword) { + return createKeyStore(KEYSTORE_TYPE, keyStorePassword); + } + + public static KeyStore createKeyStore(String keyStoreType, char[] keyStorePassword) { + try { + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(null, keyStorePassword); + return keyStore; + } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { + throw new GenericKeyStoreException(e); + } + } + + @SafeVarargs + public static KeyStore createTrustStore(T... trustManagers) { + List certificates = new ArrayList<>(); + for (T trustManager : trustManagers) { + certificates.addAll(Arrays.asList(trustManager.getAcceptedIssuers())); + } + return createTrustStore(ValidationUtils.requireNotEmpty(certificates, "Could not create TrustStore because the provided TrustManager does not contain any trusted certificates")); + } + + public static KeyStore createTrustStore(List certificates) { + try { + KeyStore trustStore = createKeyStore(); + for (T certificate : ValidationUtils.requireNotEmpty(certificates, "Could not create TrustStore because certificate is absent")) { + String alias = CertificateUtils.generateAlias(certificate); + + if (trustStore.containsAlias(alias)) { + for (int number = 0; number < 1000; number++) { + String mayBeUniqueAlias = alias + "-" + number; + if (!trustStore.containsAlias(mayBeUniqueAlias)) { + alias = mayBeUniqueAlias; + break; + } + } + } + + trustStore.setCertificateEntry(alias, certificate); + } + return trustStore; + } catch (KeyStoreException e) { + throw new GenericKeyStoreException(e); + } + } + + public static List loadSystemKeyStores() { + List keyStores = new ArrayList<>(); + String operatingSystem = System.getProperty("os.name").toLowerCase(); + if (operatingSystem.contains("windows")) { + KeyStore windowsRootKeyStore = createKeyStore("Windows-ROOT", null); + KeyStore windowsMyKeyStore = createKeyStore("Windows-MY", null); + keyStores.add(windowsRootKeyStore); + keyStores.add(windowsMyKeyStore); + } + if (operatingSystem.contains("mac")) { + KeyStore macKeyStore = createKeyStore("KeychainStore", null); + keyStores.add(macKeyStore); + } + return Collections.unmodifiableList(keyStores); + } + + public static int countAmountOfTrustMaterial(KeyStore keyStore) { + return amountOfSpecifiedMaterial(keyStore, KeyStore::isCertificateEntry, Integer.MAX_VALUE); + } + + public static int countAmountOfIdentityMaterial(KeyStore keyStore) { + return amountOfSpecifiedMaterial(keyStore, KeyStore::isKeyEntry, Integer.MAX_VALUE); + } + + public static boolean containsTrustMaterial(KeyStore keyStore) { + return amountOfSpecifiedMaterial(keyStore, KeyStore::isCertificateEntry, 1) > 0; + } + + public static boolean containsIdentityMaterial(KeyStore keyStore) { + return amountOfSpecifiedMaterial(keyStore, KeyStore::isKeyEntry, 1) > 0; + } + + private static int amountOfSpecifiedMaterial(KeyStore keyStore, + KeyStoreBiPredicate predicate, + int upperBoundaryForMaterialCounter) { + + try { + int materialCounter = 0; + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements() && materialCounter < upperBoundaryForMaterialCounter) { + String alias = aliases.nextElement(); + if (predicate.test(keyStore, alias)) { + materialCounter++; + } + } + return materialCounter; + } catch (KeyStoreException e) { + throw new GenericKeyStoreException(e); + } + } + + private interface KeyStoreBiPredicate { + boolean test(T t, U u) throws KeyStoreException; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLContextUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLContextUtils.java new file mode 100644 index 0000000..84811e7 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLContextUtils.java @@ -0,0 +1,111 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.exception.GenericSSLContextException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.SecureRandom; +import java.util.List; + +import static java.util.Objects.nonNull; + +public final class SSLContextUtils { + + private static final String DEFAULT_SSL_CONTEXT_ALGORITHM = "TLS"; + + private SSLContextUtils() { + } + + public static SSLContext createSslContext(List keyManagers, List trustManagers) { + return createSslContext(keyManagers, trustManagers, null); + } + + public static SSLContext createSslContext(List keyManagers, List trustManagers, SecureRandom secureRandom) { + return createSslContext(keyManagers, trustManagers, secureRandom, DEFAULT_SSL_CONTEXT_ALGORITHM, (Provider) null); + } + + public static SSLContext createSslContext( + List keyManagers, + List trustManagers, + SecureRandom secureRandom, + String sslContextAlgorithm, + Provider securityProvider) { + + return createSslContext( + !keyManagers.isEmpty() ? KeyManagerUtils.combine(keyManagers) : null, + !trustManagers.isEmpty() ? TrustManagerUtils.combine(trustManagers) : null, + secureRandom, + sslContextAlgorithm, + null, + securityProvider + ); + } + + public static SSLContext createSslContext( + List keyManagers, + List trustManagers, + SecureRandom secureRandom, + String sslContextAlgorithm, + String securityProviderName) { + + return createSslContext( + !keyManagers.isEmpty() ? KeyManagerUtils.combine(keyManagers) : null, + !trustManagers.isEmpty() ? TrustManagerUtils.combine(trustManagers) : null, + secureRandom, + sslContextAlgorithm, + securityProviderName, + null + ); + } + + public static SSLContext createSslContext( + X509KeyManager keyManager, + X509TrustManager trustManager, + SecureRandom secureRandom, + String sslContextAlgorithm, + String securityProviderName, + Provider securityProvider) { + + return createSslContext( + keyManager != null ? KeyManagerUtils.toArray(keyManager) : null, + trustManager != null ? TrustManagerUtils.toArray(trustManager) : null, + secureRandom, + sslContextAlgorithm, + securityProviderName, + securityProvider + ); + } + + private static SSLContext createSslContext( + X509ExtendedKeyManager[] keyManagers, + X509ExtendedTrustManager[] trustManagers, + SecureRandom secureRandom, + String sslContextAlgorithm, + String securityProviderName, + Provider securityProvider) { + + try { + SSLContext sslContext; + if (nonNull(securityProvider)) { + sslContext = SSLContext.getInstance(sslContextAlgorithm, securityProvider); + } else if (nonNull(securityProviderName)) { + sslContext = SSLContext.getInstance(sslContextAlgorithm, securityProviderName); + } else { + sslContext = SSLContext.getInstance(sslContextAlgorithm); + } + + sslContext.init(keyManagers, trustManagers, secureRandom); + return sslContext; + } catch (NoSuchAlgorithmException | KeyManagementException | NoSuchProviderException e) { + throw new GenericSSLContextException(e); + } + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLFactoryUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLFactoryUtils.java new file mode 100644 index 0000000..4ebdd6b --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLFactoryUtils.java @@ -0,0 +1,34 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.SSLFactory; + +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Function; + +public final class SSLFactoryUtils { + + private SSLFactoryUtils() {} + + /** + * Reloads the ssl material for the KeyManager and / or TrustManager within the base SSLFactory if present and if it is swappable. + * Other properties such as ciphers, protocols, secure-random, {@link javax.net.ssl.HostnameVerifier} and {@link javax.net.ssl.SSLParameters} will not be reloaded. + */ + public static void reload(SSLFactory baseSslFactory, SSLFactory updatedSslFactory) { + reload(baseSslFactory, updatedSslFactory, SSLFactory::getKeyManager, KeyManagerUtils::swapKeyManager); + reload(baseSslFactory, updatedSslFactory, SSLFactory::getTrustManager, TrustManagerUtils::swapTrustManager); + SSLSessionUtils.invalidateCaches(baseSslFactory); + } + + private static void reload(SSLFactory baseSslFactory, + SSLFactory updatedSslFactory, + Function> mapper, BiConsumer consumer) { + + Optional baseManager = mapper.apply(baseSslFactory); + Optional updatedManager = mapper.apply(updatedSslFactory); + if (baseManager.isPresent() && updatedManager.isPresent()) { + consumer.accept(baseManager.get(), updatedManager.get()); + } + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLParametersUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLParametersUtils.java new file mode 100644 index 0000000..66cba1d --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLParametersUtils.java @@ -0,0 +1,48 @@ +package org.xbib.net.security.ssl.util; + +import javax.net.ssl.SSLParameters; +import java.util.Optional; + +public final class SSLParametersUtils { + + private SSLParametersUtils() { + } + + public static SSLParameters copy(SSLParameters source) { + SSLParameters target = new SSLParameters(); + target.setProtocols(source.getProtocols()); + target.setCipherSuites(source.getCipherSuites()); + if (source.getWantClientAuth()) { + target.setWantClientAuth(true); + } + + if (source.getNeedClientAuth()) { + target.setNeedClientAuth(true); + } + return target; + } + + public static SSLParameters merge(SSLParameters baseSslParameters, SSLParameters alternativeSslParameters) { + SSLParameters target = new SSLParameters(); + + String[] ciphers = Optional.ofNullable(baseSslParameters.getCipherSuites()) + .orElseGet(alternativeSslParameters::getCipherSuites); + String[] protocols = Optional.ofNullable(baseSslParameters.getProtocols()) + .orElseGet(alternativeSslParameters::getProtocols); + + target.setCipherSuites(ciphers); + target.setProtocols(protocols); + + boolean wantClientAuth = baseSslParameters.getWantClientAuth() ? baseSslParameters.getWantClientAuth() : alternativeSslParameters.getWantClientAuth(); + if (wantClientAuth) { + target.setWantClientAuth(true); + } + + boolean needClientAuth = baseSslParameters.getNeedClientAuth() ? baseSslParameters.getNeedClientAuth() : alternativeSslParameters.getNeedClientAuth(); + if (needClientAuth) { + target.setNeedClientAuth(true); + } + + return target; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLSessionUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLSessionUtils.java new file mode 100644 index 0000000..322cec0 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLSessionUtils.java @@ -0,0 +1,163 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.SSLFactory; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionContext; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.LongFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public final class SSLSessionUtils { + + private static final LongFunction EPOCH_TIME_MAPPER = epochTime -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochTime), ZoneOffset.UTC); + + private SSLSessionUtils() {} + + public static void invalidateCaches(SSLFactory sslFactory) { + invalidateServerCaches(sslFactory); + invalidateClientCaches(sslFactory); + } + + public static void invalidateServerCaches(SSLFactory sslFactory) { + invalidateServerCaches(sslFactory.getSslContext()); + } + + public static void invalidateClientCaches(SSLFactory sslFactory) { + invalidateClientCaches(sslFactory.getSslContext()); + } + + public static void invalidateCaches(SSLContext sslContext) { + invalidateServerCaches(sslContext); + invalidateClientCaches(sslContext); + } + + public static void invalidateServerCaches(SSLContext sslContext) { + invalidateCaches(sslContext.getServerSessionContext()); + } + + public static void invalidateClientCaches(SSLContext sslContext) { + invalidateCaches(sslContext.getClientSessionContext()); + } + + public static void invalidateCaches(SSLSessionContext sslSessionContext) { + SSLSessionUtils.getSslSessions(sslSessionContext).forEach(SSLSession::invalidate); + } + + public static void invalidateCachesBefore(SSLFactory sslFactory, ZonedDateTime upperBoundary) { + invalidateCachesBefore(sslFactory.getSslContext(), upperBoundary); + } + + public static void invalidateCachesBefore(SSLContext sslContext, ZonedDateTime upperBoundary) { + invalidateCachesBefore(sslContext.getServerSessionContext(), upperBoundary); + invalidateCachesBefore(sslContext.getClientSessionContext(), upperBoundary); + } + + public static void invalidateCachesBefore(SSLSessionContext sslSessionContext, ZonedDateTime upperBoundary) { + invalidateCachesWithTimeStamp(sslSessionContext, sslSessionCreationTime -> sslSessionCreationTime.isBefore(upperBoundary)); + } + + public static void invalidateCachesAfter(SSLFactory sslFactory, ZonedDateTime lowerBoundary) { + invalidateCachesAfter(sslFactory.getSslContext(), lowerBoundary); + } + + public static void invalidateCachesAfter(SSLContext sslContext, ZonedDateTime lowerBoundary) { + invalidateCachesAfter(sslContext.getServerSessionContext(), lowerBoundary); + invalidateCachesAfter(sslContext.getClientSessionContext(), lowerBoundary); + } + + public static void invalidateCachesAfter(SSLSessionContext sslSessionContext, ZonedDateTime lowerBoundary) { + invalidateCachesWithTimeStamp(sslSessionContext, sslSessionCreationTime -> sslSessionCreationTime.isAfter(lowerBoundary)); + } + + public static void invalidateCachesBetween(SSLFactory sslFactory, ZonedDateTime lowerBoundary, ZonedDateTime upperBoundary) { + invalidateCachesBetween(sslFactory.getSslContext(), lowerBoundary, upperBoundary); + } + + public static void invalidateCachesBetween(SSLContext sslContext, ZonedDateTime lowerBoundary, ZonedDateTime upperBoundary) { + invalidateCachesBetween(sslContext.getServerSessionContext(), lowerBoundary, upperBoundary); + invalidateCachesBetween(sslContext.getClientSessionContext(), lowerBoundary, upperBoundary); + } + + public static void invalidateCachesBetween(SSLSessionContext sslSessionContext, ZonedDateTime lowerBoundary, ZonedDateTime upperBoundary) { + Predicate isAfterLowerBoundary = sslSessionCreationTime -> sslSessionCreationTime.isAfter(lowerBoundary); + Predicate isBeforeUpperBoundary = sslSessionCreationTime -> sslSessionCreationTime.isBefore(upperBoundary); + + invalidateCachesWithTimeStamp(sslSessionContext, isAfterLowerBoundary.and(isBeforeUpperBoundary)); + } + + private static void invalidateCachesWithTimeStamp(SSLSessionContext sslSessionContext, Predicate timeStampPredicate) { + SSLSessionUtils.getSslSessions(sslSessionContext).stream() + .filter(sslSession -> timeStampPredicate.test(EPOCH_TIME_MAPPER.apply(sslSession.getCreationTime()))) + .forEach(SSLSession::invalidate); + } + + public static void updateSessionTimeout(SSLFactory sslFactory, int timeoutInSeconds) { + updateSessionTimeout(sslFactory.getSslContext(), timeoutInSeconds); + } + + public static void updateSessionTimeout(SSLContext sslContext, int timeoutInSeconds) { + validateSessionTimeout(timeoutInSeconds); + + sslContext.getClientSessionContext().setSessionTimeout(timeoutInSeconds); + sslContext.getServerSessionContext().setSessionTimeout(timeoutInSeconds); + } + + public static void updateSessionCacheSize(SSLFactory sslFactory, int cacheSizeInBytes) { + updateSessionCacheSize(sslFactory.getSslContext(), cacheSizeInBytes); + } + + public static void updateSessionCacheSize(SSLContext sslContext, int cacheSizeInBytes) { + validateSessionCacheSize(cacheSizeInBytes); + + sslContext.getClientSessionContext().setSessionCacheSize(cacheSizeInBytes); + sslContext.getServerSessionContext().setSessionCacheSize(cacheSizeInBytes); + } + + public static void validateSessionTimeout(int timeoutInSeconds) { + if (timeoutInSeconds < 0) { + throw new IllegalArgumentException(String.format( + "Unsupported timeout has been provided. Timeout should be equal or greater than [%d], but received [%d]", + 0, timeoutInSeconds)); + } + } + + public static void validateSessionCacheSize(int cacheSizeInBytes) { + if (cacheSizeInBytes < 0) { + throw new IllegalArgumentException(String.format( + "Unsupported cache size has been provided. Cache size should be equal or greater than [%d], but received [%d]", + 0, cacheSizeInBytes)); + } + } + + public static List getServerSslSessions(SSLFactory sslFactory) { + return getServerSslSessions(sslFactory.getSslContext()); + } + + public static List getServerSslSessions(SSLContext sslContext) { + return getSslSessions(sslContext.getServerSessionContext()); + } + + public static List getClientSslSessions(SSLFactory sslFactory) { + return getClientSslSessions(sslFactory.getSslContext()); + } + + public static List getClientSslSessions(SSLContext sslContext) { + return getSslSessions(sslContext.getClientSessionContext()); + } + + public static List getSslSessions(SSLSessionContext sslSessionContext) { + return Collections.list(sslSessionContext.getIds()).stream() + .map(sslSessionContext::getSession) + .filter(Objects::nonNull) + .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + } + +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLSocketUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLSocketUtils.java new file mode 100644 index 0000000..c9e1aa5 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/SSLSocketUtils.java @@ -0,0 +1,38 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.SSLFactory; +import org.xbib.net.security.ssl.socket.CompositeSSLServerSocketFactory; +import org.xbib.net.security.ssl.socket.CompositeSSLSocketFactory; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocketFactory; + +public final class SSLSocketUtils { + + private SSLSocketUtils() {} + + public static SSLSocketFactory createSslSocketFactory(SSLContext sslContext, SSLParameters sslParameters) { + return new CompositeSSLSocketFactory(sslContext.getSocketFactory(), sslParameters); + } + + public static SSLSocketFactory createSslSocketFactory(SSLSocketFactory sslSocketFactory, SSLParameters sslParameters) { + return new CompositeSSLSocketFactory(sslSocketFactory, sslParameters); + } + + public static SSLSocketFactory createUnsafeSslSocketFactory() { + return SSLFactory.builder() + .withUnsafeTrustMaterial() + .build() + .getSslSocketFactory(); + } + + public static SSLServerSocketFactory createSslServerSocketFactory(SSLContext sslContext, SSLParameters sslParameters) { + return new CompositeSSLServerSocketFactory(sslContext.getServerSocketFactory(), sslParameters); + } + + public static SSLServerSocketFactory createSslServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, SSLParameters sslParameters) { + return new CompositeSSLServerSocketFactory(sslServerSocketFactory, sslParameters); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/StringUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/StringUtils.java new file mode 100644 index 0000000..3576c27 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/StringUtils.java @@ -0,0 +1,24 @@ +package org.xbib.net.security.ssl.util; + +import static java.util.Objects.isNull; + +public final class StringUtils { + + private StringUtils() {} + + public static boolean isBlank(CharSequence charSequence) { + int length = isNull(charSequence) ? 0 : charSequence.length(); + if (length != 0) { + for (int i = 0; i < length; ++i) { + if (!Character.isWhitespace(charSequence.charAt(i))) { + return false; + } + } + } + return true; + } + + public static boolean isNotBlank(CharSequence charSequence) { + return !isBlank(charSequence); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/TrustManagerUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/TrustManagerUtils.java new file mode 100644 index 0000000..e387bcd --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/TrustManagerUtils.java @@ -0,0 +1,370 @@ +package org.xbib.net.security.ssl.util; + +import org.xbib.net.security.ssl.exception.GenericTrustManagerException; +import org.xbib.net.security.ssl.trustmanager.CertificateCapturingX509ExtendedTrustManager; +import org.xbib.net.security.ssl.trustmanager.ChainAndAuthTypeValidator; +import org.xbib.net.security.ssl.trustmanager.ChainAndAuthTypeWithSSLEngineValidator; +import org.xbib.net.security.ssl.trustmanager.ChainAndAuthTypeWithSocketValidator; +import org.xbib.net.security.ssl.trustmanager.CompositeX509ExtendedTrustManager; +import org.xbib.net.security.ssl.trustmanager.DummyX509ExtendedTrustManager; +import org.xbib.net.security.ssl.trustmanager.EnhanceableX509ExtendedTrustManager; +import org.xbib.net.security.ssl.trustmanager.HotSwappableX509ExtendedTrustManager; +import org.xbib.net.security.ssl.trustmanager.TrustManagerFactoryWrapper; +import org.xbib.net.security.ssl.trustmanager.UnsafeX509ExtendedTrustManager; +import org.xbib.net.security.ssl.trustmanager.X509TrustManagerWrapper; + +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public final class TrustManagerUtils { + + private TrustManagerUtils() {} + + public static X509ExtendedTrustManager combine(X509TrustManager... trustManagers) { + return combine(Arrays.asList(trustManagers)); + } + + public static X509ExtendedTrustManager combine(List trustManagers) { + return TrustManagerUtils.trustManagerBuilder() + .withTrustManagers(trustManagers) + .build(); + } + + public static X509ExtendedTrustManager[] toArray(T trustManager) { + return new X509ExtendedTrustManager[]{TrustManagerUtils.wrapIfNeeded(trustManager)}; + } + + public static X509ExtendedTrustManager createTrustManagerWithJdkTrustedCertificates() { + return createTrustManager((KeyStore) null); + } + + public static Optional createTrustManagerWithSystemTrustedCertificates() { + List trustStores = KeyStoreUtils.loadSystemKeyStores(); + if (trustStores.isEmpty()) { + return Optional.empty(); + } + + X509ExtendedTrustManager trustManager = createTrustManager(trustStores.toArray(new KeyStore[]{})); + return Optional.of(trustManager); + } + + public static X509ExtendedTrustManager createTrustManager(KeyStore... trustStores) { + return Arrays.stream(trustStores) + .map(TrustManagerUtils::createTrustManager) + .collect(Collectors.collectingAndThen(Collectors.toList(), TrustManagerUtils::combine)); + } + + public static X509ExtendedTrustManager createTrustManager(KeyStore trustStore) { + return createTrustManager(trustStore, TrustManagerFactory.getDefaultAlgorithm()); + } + + public static X509ExtendedTrustManager createTrustManager(KeyStore trustStore, String trustManagerFactoryAlgorithm) { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustManagerFactoryAlgorithm); + return createTrustManager(trustStore, trustManagerFactory); + } catch (NoSuchAlgorithmException e) { + throw new GenericTrustManagerException(e); + } + } + + public static X509ExtendedTrustManager createTrustManager(KeyStore trustStore, String trustManagerFactoryAlgorithm, String securityProviderName) { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustManagerFactoryAlgorithm, securityProviderName); + return createTrustManager(trustStore, trustManagerFactory); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new GenericTrustManagerException(e); + } + } + + public static X509ExtendedTrustManager createTrustManager(KeyStore trustStore, String trustManagerFactoryAlgorithm, Provider securityProvider) { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustManagerFactoryAlgorithm, securityProvider); + return createTrustManager(trustStore, trustManagerFactory); + } catch (NoSuchAlgorithmException e) { + throw new GenericTrustManagerException(e); + } + } + + public static X509ExtendedTrustManager createTrustManager(KeyStore trustStore, TrustManagerFactory trustManagerFactory) { + try { + trustManagerFactory.init(trustStore); + return TrustManagerUtils.getTrustManager(trustManagerFactory); + } catch (KeyStoreException e) { + throw new GenericTrustManagerException(e); + } + } + + public static X509ExtendedTrustManager createTrustManager(ManagerFactoryParameters... managerFactoryParameters) { + return Arrays.stream(managerFactoryParameters) + .map(TrustManagerUtils::createTrustManager) + .collect(Collectors.collectingAndThen(Collectors.toList(), TrustManagerUtils::combine)); + } + + public static X509ExtendedTrustManager createTrustManager(ManagerFactoryParameters managerFactoryParameters) { + return createTrustManager(managerFactoryParameters, TrustManagerFactory.getDefaultAlgorithm()); + } + + public static X509ExtendedTrustManager createTrustManager(ManagerFactoryParameters managerFactoryParameters, String trustManagerFactoryAlgorithm) { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustManagerFactoryAlgorithm); + return createTrustManager(managerFactoryParameters, trustManagerFactory); + } catch (NoSuchAlgorithmException e) { + throw new GenericTrustManagerException(e); + } + } + + public static X509ExtendedTrustManager createTrustManager(ManagerFactoryParameters managerFactoryParameters, String trustManagerFactoryAlgorithm, String securityProviderName) { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustManagerFactoryAlgorithm, securityProviderName); + return createTrustManager(managerFactoryParameters, trustManagerFactory); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new GenericTrustManagerException(e); + } + } + + public static X509ExtendedTrustManager createTrustManager(ManagerFactoryParameters managerFactoryParameters, String trustManagerFactoryAlgorithm, Provider securityProvider) { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustManagerFactoryAlgorithm, securityProvider); + return createTrustManager(managerFactoryParameters, trustManagerFactory); + } catch (NoSuchAlgorithmException e) { + throw new GenericTrustManagerException(e); + } + } + + public static X509ExtendedTrustManager createTrustManager(ManagerFactoryParameters managerFactoryParameters, TrustManagerFactory trustManagerFactory) { + try { + trustManagerFactory.init(managerFactoryParameters); + return TrustManagerUtils.getTrustManager(trustManagerFactory); + } catch (InvalidAlgorithmParameterException e) { + throw new GenericTrustManagerException(e); + } + } + + public static X509ExtendedTrustManager createUnsafeTrustManager() { + return UnsafeX509ExtendedTrustManager.getInstance(); + } + + public static X509ExtendedTrustManager createDummyTrustManager() { + return DummyX509ExtendedTrustManager.getInstance(); + } + + public static X509ExtendedTrustManager createCertificateCapturingTrustManager(List certificatesCollector) { + return createCertificateCapturingTrustManager(TrustManagerUtils.createUnsafeTrustManager(), certificatesCollector); + } + + public static X509ExtendedTrustManager createCertificateCapturingTrustManager(X509TrustManager baseTrustManager, List certificatesCollector) { + return new CertificateCapturingX509ExtendedTrustManager(wrapIfNeeded(baseTrustManager), certificatesCollector); + } + + public static X509ExtendedTrustManager wrapIfNeeded(X509TrustManager trustManager) { + if (trustManager instanceof X509ExtendedTrustManager) { + return (X509ExtendedTrustManager) trustManager; + } else { + return new X509TrustManagerWrapper(trustManager); + } + } + + public static TrustManagerFactory createTrustManagerFactory(TrustManager trustManager) { + return new TrustManagerFactoryWrapper(trustManager); + } + + public static X509ExtendedTrustManager getTrustManager(T trustManagerFactory) { + return Arrays.stream(trustManagerFactory.getTrustManagers()) + .filter(X509TrustManager.class::isInstance) + .map(X509TrustManager.class::cast) + .map(TrustManagerUtils::wrapIfNeeded) + .collect(Collectors.collectingAndThen(Collectors.toList(), TrustManagerUtils::combine)); + } + + /** + * Wraps the given TrustManager into an instance of a Hot Swappable TrustManager. + * This type of TrustManager has the capability of swapping in and out different TrustManagers at runtime. + * + * @param trustManager To be wrapped TrustManager + * @return Swappable TrustManager + */ + public static X509ExtendedTrustManager createSwappableTrustManager(X509TrustManager trustManager) { + return new HotSwappableX509ExtendedTrustManager(TrustManagerUtils.wrapIfNeeded(trustManager)); + } + + /** + * Swaps the internal TrustManager instance with the given trustManager object. + * The baseTrustManager should be an instance of {@link HotSwappableX509ExtendedTrustManager} + * and can be created with {@link TrustManagerUtils#createSwappableTrustManager(X509TrustManager)} + * + * @param baseTrustManager an instance of {@link HotSwappableX509ExtendedTrustManager} + * @param newTrustManager to be injected instance of a TrustManager + * @throws GenericTrustManagerException if {@code baseTrustManager} is not instance of {@link HotSwappableX509ExtendedTrustManager} + */ + public static void swapTrustManager(X509TrustManager baseTrustManager, X509TrustManager newTrustManager) { + if (newTrustManager instanceof HotSwappableX509ExtendedTrustManager) { + throw new GenericTrustManagerException( + String.format("The newTrustManager should not be an instance of [%s]", HotSwappableX509ExtendedTrustManager.class.getName()) + ); + } + + if (baseTrustManager instanceof HotSwappableX509ExtendedTrustManager) { + ((HotSwappableX509ExtendedTrustManager) baseTrustManager).setTrustManager(TrustManagerUtils.wrapIfNeeded(newTrustManager)); + } else { + throw new GenericTrustManagerException( + String.format("The baseTrustManager is from the instance of [%s] and should be an instance of [%s].", + baseTrustManager.getClass().getName(), + HotSwappableX509ExtendedTrustManager.class.getName()) + ); + } + } + + public static X509ExtendedTrustManager createEnhanceableTrustManager( + X509ExtendedTrustManager trustManager, + ChainAndAuthTypeValidator chainAndAuthTypeValidator, + ChainAndAuthTypeWithSocketValidator chainAndAuthTypeWithSocketValidator, + ChainAndAuthTypeWithSSLEngineValidator chainAndAuthTypeWithSSLEngineValidator) { + + return new EnhanceableX509ExtendedTrustManager( + trustManager, + chainAndAuthTypeValidator, + chainAndAuthTypeWithSocketValidator, + chainAndAuthTypeWithSSLEngineValidator + ); + } + + private static List unwrapIfPossible(X509ExtendedTrustManager trustManager) { + List trustManagers = new ArrayList<>(); + if (trustManager instanceof CompositeX509ExtendedTrustManager) { + for (X509ExtendedTrustManager innerTrustManager : ((CompositeX509ExtendedTrustManager) trustManager).getTrustManagers()) { + List unwrappedTrustManagers = TrustManagerUtils.unwrapIfPossible(innerTrustManager); + trustManagers.addAll(unwrappedTrustManagers); + } + } else { + trustManagers.add(trustManager); + } + + return trustManagers; + } + + public static TrustManagerBuilder trustManagerBuilder() { + return new TrustManagerBuilder(); + } + + public static final class TrustManagerBuilder { + + private static final String EMPTY_TRUST_MANAGER_EXCEPTION = "Input does not contain TrustManager"; + + private TrustManagerBuilder() {} + + private final List trustManagers = new ArrayList<>(); + private boolean swappableTrustManagerEnabled = false; + + private ChainAndAuthTypeValidator chainAndAuthTypeValidator; + private ChainAndAuthTypeWithSocketValidator chainAndAuthTypeWithSocketValidator; + private ChainAndAuthTypeWithSSLEngineValidator chainAndAuthTypeWithSSLEngineValidator; + + public TrustManagerBuilder withTrustManagers(List trustManagers) { + for (T trustManager : trustManagers) { + withTrustManager(trustManager); + } + return this; + } + + public TrustManagerBuilder withTrustManager(T trustManager) { + this.trustManagers.add(TrustManagerUtils.wrapIfNeeded(trustManager)); + return this; + } + + public TrustManagerBuilder withTrustStores(List trustStores) { + for (KeyStore trustStore : trustStores) { + this.trustManagers.add(TrustManagerUtils.createTrustManager(trustStore)); + } + return this; + } + + public TrustManagerBuilder withTrustStore(T trustStore) { + this.trustManagers.add(TrustManagerUtils.createTrustManager(trustStore)); + return this; + } + + public TrustManagerBuilder withTrustStore(T trustStore, String trustManagerAlgorithm) { + this.trustManagers.add(TrustManagerUtils.createTrustManager(trustStore, trustManagerAlgorithm)); + return this; + } + + public TrustManagerBuilder withSwappableTrustManager(boolean swappableTrustManagerEnabled) { + this.swappableTrustManagerEnabled = swappableTrustManagerEnabled; + return this; + } + + public TrustManagerBuilder withTrustEnhancer(ChainAndAuthTypeValidator validator) { + this.chainAndAuthTypeValidator = validator; + return this; + } + + @SuppressWarnings("overloads") + public TrustManagerBuilder withTrustEnhancer(ChainAndAuthTypeWithSocketValidator validator) { + this.chainAndAuthTypeWithSocketValidator = validator; + return this; + } + + @SuppressWarnings("overloads") + public TrustManagerBuilder withTrustEnhancer(ChainAndAuthTypeWithSSLEngineValidator validator) { + this.chainAndAuthTypeWithSSLEngineValidator = validator; + return this; + } + + public X509ExtendedTrustManager build() { + ValidationUtils.requireNotEmpty(trustManagers, () -> new GenericTrustManagerException(EMPTY_TRUST_MANAGER_EXCEPTION)); + + X509ExtendedTrustManager baseTrustManager; + + Optional unsafeTrustManager = trustManagers.stream() + .filter(UnsafeX509ExtendedTrustManager.class::isInstance) + .findAny(); + + if (unsafeTrustManager.isPresent()) { + baseTrustManager = unsafeTrustManager.get(); + } else { + if (trustManagers.size() == 1) { + baseTrustManager = trustManagers.get(0); + } else { + baseTrustManager = trustManagers.stream() + .map(TrustManagerUtils::unwrapIfPossible) + .flatMap(Collection::stream) + .filter(trustManager -> trustManager.getAcceptedIssuers().length > 0) + .collect(Collectors.collectingAndThen(Collectors.toList(), CompositeX509ExtendedTrustManager::new)); + } + + if (chainAndAuthTypeValidator != null + || chainAndAuthTypeWithSocketValidator != null + || chainAndAuthTypeWithSSLEngineValidator != null) { + baseTrustManager = TrustManagerUtils.createEnhanceableTrustManager( + baseTrustManager, + chainAndAuthTypeValidator, + chainAndAuthTypeWithSocketValidator, + chainAndAuthTypeWithSSLEngineValidator + ); + } + } + + if (swappableTrustManagerEnabled) { + baseTrustManager = TrustManagerUtils.createSwappableTrustManager(baseTrustManager); + } + + return baseTrustManager; + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/UriUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/UriUtils.java new file mode 100644 index 0000000..a264e41 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/UriUtils.java @@ -0,0 +1,24 @@ +package org.xbib.net.security.ssl.util; + +import java.net.URI; + +import static java.util.Objects.isNull; + +public final class UriUtils { + + private UriUtils() {} + + public static void validate(URI uri) { + if (isNull(uri)) { + throw new IllegalArgumentException("Host should be present"); + } + + if (isNull(uri.getHost())) { + throw new IllegalArgumentException(String.format("Hostname should be defined for the given input: [%s]", uri)); + } + + if (uri.getPort() == -1) { + throw new IllegalArgumentException(String.format("Port should be defined for the given input: [%s]", uri)); + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/ssl/util/ValidationUtils.java b/net-security/src/main/java/org/xbib/net/security/ssl/util/ValidationUtils.java new file mode 100644 index 0000000..f3e8ec9 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/ssl/util/ValidationUtils.java @@ -0,0 +1,46 @@ +package org.xbib.net.security.ssl.util; + +import java.util.List; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +public final class ValidationUtils { + + public static final UnaryOperator GENERIC_EXCEPTION_MESSAGE = objectType -> String.format("No valid %s has been provided. %s must be present, but was absent.", objectType, objectType); + + private ValidationUtils() { + } + + public static T requireNotNull(T maybeNull, String message) { + return requireNotNull(maybeNull, () -> new IllegalArgumentException(message)); + } + + public static T requireNotNull(T maybeNull, Supplier exceptionSupplier) { + if (maybeNull == null) { + throw exceptionSupplier.get(); + } + return maybeNull; + } + + public static List requireNotEmpty(List maybeNull, String message) { + return requireNotEmpty(maybeNull, () -> new IllegalArgumentException(message)); + } + + public static List requireNotEmpty(List maybeNull, Supplier exceptionSupplier) { + if (maybeNull == null || maybeNull.isEmpty()) { + throw exceptionSupplier.get(); + } + return maybeNull; + } + + public static String requireNotBlank(String maybeNull, String message) { + return requireNotBlank(maybeNull, () -> new IllegalArgumentException(message)); + } + + public static String requireNotBlank(String maybeNull, Supplier exceptionSupplier) { + if (StringUtils.isBlank(maybeNull)) { + throw exceptionSupplier.get(); + } + return maybeNull; + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/util/Asn1Object.java b/net-security/src/main/java/org/xbib/net/security/util/Asn1Object.java new file mode 100644 index 0000000..06b41c0 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/util/Asn1Object.java @@ -0,0 +1,101 @@ +package org.xbib.net.security.util; + +import java.io.IOException; +import java.math.BigInteger; + +/** + * An ASN.1 TLV. The object is not parsed. It can only handle integers and strings. + */ +public class Asn1Object { + + protected final int type; + protected final int length; + protected final byte[] value; + protected final int tag; + + /** + * Construct a ASN.1 TLV. The TLV could be either a + * constructed or primitive entity. + * The first byte in DER encoding is made of following fields, + *
    +     * -------------------------------------------------
    +     * |Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1|
    +     * -------------------------------------------------
    +     * |  Class    | CF  |     +      Type             |
    +     * -------------------------------------------------
    +     * 
    + *
      + *
    • Class: Universal, Application, Context or Private + *
    • CF: Constructed flag. If 1, the field is constructed. + *
    • Type: This is actually called tag in ASN.1. It + * indicates data type (Integer, String) or a construct + * (sequence, choice, set). + *
    + * + * @param tag Tag or Identifier + * @param length Length of the field + * @param value Encoded octet string for the field. + */ + public Asn1Object(int tag, int length, byte[] value) { + this.tag = tag; + this.type = tag & 0x1F; + this.length = length; + this.value = value; + } + + public int getType() { + return type; + } + + public int getLength() { + return length; + } + + public byte[] getValue() { + return value; + } + + public boolean isConstructed() { + return (tag & DerParser.CONSTRUCTED) == DerParser.CONSTRUCTED; + } + + public DerParser getParser() throws IOException { + if (!isConstructed()) { + throw new IOException("Invalid DER: can't parse primitive entity"); //$NON-NLS-1$ + } + return new DerParser(value); + } + + public BigInteger getInteger() throws IOException { + if (type != DerParser.INTEGER) { + throw new IOException("Invalid DER: object is not integer"); //$NON-NLS-1$ + } + return new BigInteger(value); + } + + public String getString() throws IOException { + String encoding; + switch (type) { + case DerParser.NUMERIC_STRING: + case DerParser.PRINTABLE_STRING: + case DerParser.VIDEOTEX_STRING: + case DerParser.IA5_STRING: + case DerParser.GRAPHIC_STRING: + case DerParser.ISO646_STRING: + case DerParser.GENERAL_STRING: + encoding = "ISO-8859-1"; //$NON-NLS-1$ + break; + case DerParser.BMP_STRING: + encoding = "UTF-16BE"; //$NON-NLS-1$ + break; + case DerParser.UTF8_STRING: + encoding = "UTF-8"; //$NON-NLS-1$ + break; + case DerParser.UNIVERSAL_STRING: + throw new IOException("Invalid DER: can't handle UCS-4 string"); //$NON-NLS-1$ + default: + throw new IOException("Invalid DER: object is not a string"); //$NON-NLS-1$ + } + return new String(value, encoding); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/util/DerParser.java b/net-security/src/main/java/org/xbib/net/security/util/DerParser.java new file mode 100644 index 0000000..92c642e --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/util/DerParser.java @@ -0,0 +1,107 @@ +package org.xbib.net.security.util; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; + +public class DerParser { + + public final static int UNIVERSAL = 0x00; + public final static int APPLICATION = 0x40; + public final static int CONTEXT = 0x80; + public final static int PRIVATE = 0xC0; + + public final static int CONSTRUCTED = 0x20; + + public final static int ANY = 0x00; + public final static int BOOLEAN = 0x01; + public final static int INTEGER = 0x02; + public final static int BIT_STRING = 0x03; + public final static int OCTET_STRING = 0x04; + public final static int NULL = 0x05; + public final static int OBJECT_IDENTIFIER = 0x06; + public final static int REAL = 0x09; + public final static int ENUMERATED = 0x0a; + public final static int RELATIVE_OID = 0x0d; + + public final static int SEQUENCE = 0x10; + public final static int SET = 0x11; + + public final static int NUMERIC_STRING = 0x12; + public final static int PRINTABLE_STRING = 0x13; + public final static int T61_STRING = 0x14; + public final static int VIDEOTEX_STRING = 0x15; + public final static int IA5_STRING = 0x16; + public final static int GRAPHIC_STRING = 0x19; + public final static int ISO646_STRING = 0x1A; + public final static int GENERAL_STRING = 0x1B; + + public final static int UTF8_STRING = 0x0C; + public final static int UNIVERSAL_STRING = 0x1C; + public final static int BMP_STRING = 0x1E; + + public final static int UTC_TIME = 0x17; + public final static int GENERALIZED_TIME = 0x18; + + private final InputStream inputStream; + + public DerParser(InputStream inputStream) throws IOException { + this.inputStream = inputStream; + } + + public DerParser(byte[] bytes) throws IOException { + this(new ByteArrayInputStream(bytes)); + } + + public Asn1Object read() throws IOException { + int tag = inputStream.read(); + if (tag == -1) { + throw new IOException("Invalid DER: stream too short, missing tag"); //$NON-NLS-1$ + } + int length = getLength(); + byte[] value = new byte[length]; + int n = inputStream.read(value); + if (n < length) { + throw new IOException("Invalid DER: stream too short, missing value"); //$NON-NLS-1$ + } + return new Asn1Object(tag, length, value); + } + + /** + * Decode the length of the field. Can only support length + * encoding up to 4 octets. + *

    + *

    In BER/DER encoding, length can be encoded in 2 forms, + *

      + *
    • Short form. One octet. Bit 8 has value "0" and bits 7-1 + * give the length. + *
    • Long form. Two to 127 octets (only 4 is supported here). + * Bit 8 of first octet has value "1" and bits 7-1 give the + * number of additional length octets. Second and following + * octets give the length, base 256, most significant digit first. + *
    + * + * @return The length as integer + * @throws IOException if reading fails + */ + private int getLength() throws IOException { + int i = inputStream.read(); + if (i == -1) { + throw new IOException("Invalid DER: length missing"); + } + if ((i & ~0x7F) == 0) { + return i; + } + int num = i & 0x7F; + if (i >= 0xFF || num > 4) { + throw new IOException("Invalid DER: length field too big (" + i + ")"); + } + byte[] bytes = new byte[num]; + int n = inputStream.read(bytes); + if (n < num) { + throw new IOException("Invalid DER: length too short"); + } + return new BigInteger(1, bytes).intValue(); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/util/DerUtils.java b/net-security/src/main/java/org/xbib/net/security/util/DerUtils.java new file mode 100644 index 0000000..c63dffa --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/util/DerUtils.java @@ -0,0 +1,360 @@ +package org.xbib.net.security.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.StringTokenizer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static java.util.Objects.requireNonNull; + +/** + * ASN.1 DER encoder methods necessary to process PEM files and to write a certificate signing request. + * NOTE: this API is only present for the two mentioned use cases, and is subject to change without warning. + */ +public final class DerUtils { + public static final int SEQUENCE_TAG = 0x30; + public static final int BOOLEAN_TAG = 0x01; + public static final int INTEGER_TAG = 0x02; + public static final int BIT_STRING_TAG = 0x03; + public static final int OCTET_STRING_TAG = 0x04; + public static final int NULL_TAG = 0x05; + public static final int OBJECT_IDENTIFIER_TAG = 0x06; + public static final int UTC_TIME_TAG = 0x17; + private static final DateTimeFormatter UTC_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyMMddHHmmssX").withZone(ZoneOffset.UTC); + + private DerUtils() { + } + + /** + * Encodes a sequence of encoded values. + */ + public static byte[] encodeSequence(byte[]... encodedValues) throws IOException { + return encodeConstructed(SEQUENCE_TAG, encodedValues); + } + + /** + * Decodes a sequence of encoded values. + */ + public static List decodeSequence(byte[] sequence) { + int index = 0; + //checkArgument(sequence[0] == SEQUENCE_TAG, "Expected sequence tag"); + index++; + int sequenceDataLength = decodeLength(sequence, index); + index += encodedLengthSize(sequenceDataLength); + //checkArgument(sequenceDataLength + index == sequence.length, "Invalid sequence"); + List elements = new ArrayList<>(); + while (index < sequence.length) { + int elementStart = index; + index++; + int length = decodeLength(sequence, index); + index += encodedLengthSize(length); + byte[] data = Arrays.copyOfRange(sequence, elementStart, index + length); + elements.add(data); + index += length; + } + return elements; + } + + /** + * Decodes a optional element of a sequence. + */ + public static byte[] decodeSequenceOptionalElement(byte[] element) { + int index = 0; + //checkArgument((element[0] & 0xE0) == 0xA0, "Expected optional sequence element tag"); + index++; + int length = decodeLength(element, index); + index += encodedLengthSize(length); + //checkArgument(length + index == element.length, "Invalid optional sequence element"); + return Arrays.copyOfRange(element, index, index + length); + } + + /** + * Encodes a bit string padded with the specified number of bits. + * The encoding is a byte containing the padBits followed by the value bytes. + */ + public static byte[] encodeBitString(int padBits, byte[] value) throws IOException { + //checkArgument(padBits >= 0 && padBits < 8, "Invalid pad bits"); + byte[] lengthEncoded = encodeLength(value.length + 1); + ByteArrayOutputStream out = new ByteArrayOutputStream(2 + lengthEncoded.length + value.length); + out.write(BIT_STRING_TAG); + out.write(lengthEncoded); + out.write(padBits); + out.write(value); + return out.toByteArray(); + } + + /** + * Encodes an integer. + */ + public static byte[] encodeBooleanTrue() { + return new byte[]{BOOLEAN_TAG, 0x01, (byte) 0xFF}; + } + + /** + * Encodes an integer. + */ + public static byte[] encodeInteger(long value) throws IOException { + return encodeInteger(BigInteger.valueOf(value)); + } + + /** + * Encodes an integer. + */ + public static byte[] encodeInteger(BigInteger value) throws IOException { + return encodeTag(INTEGER_TAG, value.toByteArray()); + } + + /** + * Encodes an octet string. + */ + public static byte[] encodeOctetString(byte[] value) throws IOException { + return encodeTag(OCTET_STRING_TAG, value); + } + + /** + * Encodes an octet string. + */ + public static byte[] encodeUtcTime(String value) throws IOException { + return encodeTag(UTC_TIME_TAG, value.getBytes(StandardCharsets.US_ASCII)); + } + + /** + * Encodes an octet string. + */ + public static byte[] encodeUtcTime(Instant value) throws IOException { + String utcTime = UTC_TIME_FORMATTER.format(value); + return encodeTag(UTC_TIME_TAG, utcTime.getBytes(StandardCharsets.US_ASCII)); + } + + /** + * Encodes the length of a DER value. The encoding of a 7bit value is simply the value. Values needing more than 7bits + * are encoded as a lead byte with the high bit set and containing the number of value bytes. Then the following bytes + * encode the length using the least number of bytes possible. + */ + public static byte[] encodeLength(int length) { + if (length < 128) { + return new byte[]{(byte) length}; + } + int numberOfBits = 32 - Integer.numberOfLeadingZeros(length); + int numberOfBytes = (numberOfBits + 7) / 8; + byte[] encoded = new byte[1 + numberOfBytes]; + encoded[0] = (byte) (numberOfBytes | 0x80); + for (int i = 0; i < numberOfBytes; i++) { + int byteToEncode = (numberOfBytes - i); + int shiftSize = (byteToEncode - 1) * 8; + encoded[i + 1] = (byte) (length >>> shiftSize); + } + return encoded; + } + + private static int encodedLengthSize(int length) { + if (length < 128) { + return 1; + } + int numberOfBits = 32 - Integer.numberOfLeadingZeros(length); + int numberOfBytes = (numberOfBits + 7) / 8; + return numberOfBytes + 1; + } + + static int decodeLength(byte[] buffer, int offset) { + int firstByte = buffer[offset] & 0xFF; + //checkArgument(firstByte != 0x80, "Indefinite lengths not supported in DER"); + //checkArgument(firstByte != 0xFF, "Invalid length first byte 0xFF"); + if (firstByte < 128) { + return firstByte; + } + int numberOfBytes = firstByte & 0x7F; + //checkArgument(numberOfBytes <= 4); + int length = 0; + for (int i = 0; i < numberOfBytes; i++) { + length = (length << 8) | (buffer[offset + 1 + i] & 0xFF); + } + return length; + } + + public static byte[] encodeOID(String oid) { + requireNonNull(oid, "oid is null"); + Iterator it = new StringTokenizer(oid, ".").asIterator(); + Stream targetStream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, + Spliterator.ORDERED), false); + List parts = targetStream.map(Object::toString) + .map(Integer::parseInt) + .collect(Collectors.toList()); + //checkArgument(parts.size() >= 2, "at least 2 parts are required"); + try { + ByteArrayOutputStream body = new ByteArrayOutputStream(); + body.write(parts.get(0) * 40 + parts.get(1)); + for (Integer part : parts.subList(2, parts.size())) { + writeOidPart(body, part); + } + byte[] length = encodeLength(body.size()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(OBJECT_IDENTIFIER_TAG); + out.write(length); + body.writeTo(out); + return out.toByteArray(); + } catch (IOException e) { + // this won't happen with byte array output streams + throw new UncheckedIOException(e); + } + } + + public static int[] decodeOID(byte[] buffer) throws IOException { + final byte ASN_BIT8 = (byte) 0x80; + /* + * ASN.1 objid ::= 0x06 asnlength subidentifier {subidentifier}* + * subidentifier ::= {leadingbyte}* lastbyte + * leadingbyte ::= 1 7bitvalue + * lastbyte ::= 0 7bitvalue + */ + int subidentifier; + int length; + // get the type + int offset = 0; + byte type = buffer[offset++]; + if (type != 0x06) { + throw new IOException("Wrong type. Not an OID: "); + } + length = decodeLength(buffer, offset++); + int[] oid = new int[length + 2]; + /* Handle invalid object identifier encodings of the form 06 00 robustly */ + if (length == 0) { + oid[0] = oid[1] = 0; + } + int pos = 1; + while (length > 0) { + subidentifier = 0; + int b; + do { /* shift and add in low order 7 bits */ + b = buffer[offset++]; + subidentifier = (subidentifier << 7) + (b & ~ASN_BIT8); + length--; + } while ((length > 0) && ((b & ASN_BIT8) != 0)); /* last byte has high bit clear */ + oid[pos++] = subidentifier; + } + /* + * The first two subidentifiers are encoded into the first component + * with the value (X * 40) + Y, where: + * X is the value of the first subidentifier. + * Y is the value of the second subidentifier. + */ + subidentifier = oid[1]; + if (subidentifier == 0x2B) { + oid[0] = 1; + oid[1] = 3; + } else if (subidentifier >= 0 && subidentifier < 80) { + if (subidentifier < 40) { + oid[0] = 0; + oid[1] = subidentifier; + } else { + oid[0] = 1; + oid[1] = subidentifier - 40; + } + } else { + oid[0] = 2; + oid[1] = subidentifier - 80; + } + if (pos < 2) { + pos = 2; + } + int[] value = new int[pos]; + System.arraycopy(oid, 0, value, 0, pos); + return value; + } + + public static String oidToString(int[] oid) { + StringBuilder sb = new StringBuilder(); + for (int i : oid) { + if (sb.length() > 0) { + sb.append("."); + } + sb.append(i); + } + return sb.toString(); + } + + /** + * Encode an OID number part. The encoding is a big endian variant. + */ + private static void writeOidPart(ByteArrayOutputStream out, final int number) { + if (number < 128) { + out.write((byte) number); + return; + } + + int numberOfBits = Integer.SIZE - Integer.numberOfLeadingZeros(number); + int numberOfParts = (numberOfBits + 6) / 7; + for (int i = 0; i < numberOfParts - 1; i++) { + int partToEncode = (numberOfParts - i); + int shiftSize = (partToEncode - 1) * 7; + int part = (number >>> shiftSize) & 0x7F | 0x80; + out.write(part); + } + out.write(number & 0x7f); + } + + public static byte[] encodeNull() { + return new byte[]{NULL_TAG, 0x00}; + } + + public static byte[] encodeTag(int tag, byte[] body) throws IOException { + //checkArgument(tag >= 0 && tag < 32, "Invalid tag: %s", tag); + requireNonNull(body, "body is null"); + return encodeTagInternal(tag, body); + } + + public static byte[] encodeContextSpecificTag(int tag, byte[] body) throws IOException { + //checkArgument(tag >= 0 && tag < 32, "Invalid tag: %s", tag); + requireNonNull(body, "body is null"); + int privateTag = tag | 0x80; + return encodeTagInternal(privateTag, body); + } + + private static byte[] encodeTagInternal(int tag, byte[] body) throws IOException { + //checkArgument(tag >= 0 && tag < 256, "Invalid tag: %s", tag); + byte[] lengthEncoded = encodeLength(body.length); + ByteArrayOutputStream out = new ByteArrayOutputStream(1 + lengthEncoded.length + body.length); + out.write(tag); + out.write(lengthEncoded); + out.write(body); + return out.toByteArray(); + } + + public static byte[] encodeContextSpecificSequence(int tag, byte[]... encodedValues) throws IOException { + //checkArgument(tag >= 0 && tag < 32, "Invalid tag: %s", tag); + requireNonNull(encodedValues, "body is null"); + int privateTag = tag | 0xA0; + + return encodeConstructed(privateTag, encodedValues); + } + + private static byte[] encodeConstructed(int privateTag, byte[]... encodedValues) throws IOException { + int length = 0; + for (byte[] encodedValue : encodedValues) { + length += encodedValue.length; + } + byte[] lengthEncoded = encodeLength(length); + ByteArrayOutputStream out = new ByteArrayOutputStream(1 + lengthEncoded.length + length); + out.write(privateTag); + out.write(lengthEncoded); + for (byte[] entry : encodedValues) { + out.write(entry); + } + return out.toByteArray(); + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/util/DistinguishedNameParser.java b/net-security/src/main/java/org/xbib/net/security/util/DistinguishedNameParser.java new file mode 100644 index 0000000..ceb0ff0 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/util/DistinguishedNameParser.java @@ -0,0 +1,299 @@ +package org.xbib.net.security.util; + +import javax.security.auth.x500.X500Principal; + +/** + * A distinguished name (DN) parser. + * This parser only supports extracting a string value from a DN. + * It doesn't support values in the hex-string style. + * Taken from okhttp. + */ +public final class DistinguishedNameParser { + + private final String dn; + + private final int length; + + private int pos; + + private int beg; + + private int end; + + private int cur; + + private char[] chars; + + public DistinguishedNameParser(X500Principal principal) { + this.dn = principal.getName(X500Principal.RFC2253); + this.length = this.dn.length(); + } + + private String nextAT() { + while (pos < length && chars[pos] == ' ') { + pos++; + } + if (pos == length) { + return null; + } + beg = pos; + pos++; + while (pos < length && chars[pos] != '=' && chars[pos] != ' ') { + pos++; + } + if (pos >= length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + end = pos; + if (chars[pos] == ' ') { + while (pos < length && chars[pos] != '=' && chars[pos] == ' ') { + pos++; + } + if (chars[pos] != '=' || pos == length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + } + pos++; + while (pos < length && chars[pos] == ' ') { + pos++; + } + if ((end - beg > 4) && (chars[beg + 3] == '.') && + (chars[beg] == 'O' || chars[beg] == 'o') && + (chars[beg + 1] == 'I' || chars[beg + 1] == 'i') && + (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) { + beg += 4; + } + return new String(chars, beg, end - beg); + } + + private String quotedAV() { + pos++; + beg = pos; + end = beg; + while (true) { + if (pos == length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + if (chars[pos] == '"') { + pos++; + break; + } else if (chars[pos] == '\\') { + chars[end] = getEscaped(); + } else { + chars[end] = chars[pos]; + } + pos++; + end++; + } + while (pos < length && chars[pos] == ' ') { + pos++; + } + return new String(chars, beg, end - beg); + } + + private String hexAV() { + if (pos + 4 >= length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + beg = pos; + pos++; + while (true) { + if (pos == length || chars[pos] == '+' || chars[pos] == ',' + || chars[pos] == ';') { + end = pos; + break; + } + if (chars[pos] == ' ') { + end = pos; + pos++; + while (pos < length && chars[pos] == ' ') { + pos++; + } + break; + } else if (chars[pos] >= 'A' && chars[pos] <= 'F') { + chars[pos] += 32; + } + pos++; + } + int hexLen = end - beg; + if (hexLen < 5 || (hexLen & 1) == 0) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + return new String(chars, beg, hexLen); + } + + private String escapedAV() { + beg = pos; + end = pos; + while (true) { + if (pos >= length) { + return new String(chars, beg, end - beg); + } + switch (chars[pos]) { + case '+': + case ',': + case ';': + return new String(chars, beg, end - beg); + case '\\': + chars[end++] = getEscaped(); + pos++; + break; + case ' ': + cur = end; + pos++; + chars[end++] = ' '; + for (; pos < length && chars[pos] == ' '; pos++) { + chars[end++] = ' '; + } + if (pos == length || + chars[pos] == ',' || + chars[pos] == '+' || + chars[pos] == ';') { + return new String(chars, beg, cur - beg); + } + break; + default: + chars[end++] = chars[pos]; + pos++; + } + } + } + + private char getEscaped() { + pos++; + if (pos == length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + switch (chars[pos]) { + case '"': + case '\\': + case ',': + case '=': + case '+': + case '<': + case '>': + case '#': + case ';': + case ' ': + case '*': + case '%': + case '_': + return chars[pos]; + default: + return getUTF8(); + } + } + + private char getUTF8() { + int res = getByte(pos); + pos++; + if (res < 128) { + return (char) res; + } else if (res >= 192 && res <= 247) { + int count; + if (res <= 223) { + count = 1; + res = res & 0x1F; + } else if (res <= 239) { + count = 2; + res = res & 0x0F; + } else { + count = 3; + res = res & 0x07; + } + int b; + for (int i = 0; i < count; i++) { + pos++; + if (pos == length || chars[pos] != '\\') { + return 0x3F; + } + pos++; + b = getByte(pos); + pos++; + if ((b & 0xC0) != 0x80) { + return 0x3F; + } + res = (res << 6) + (b & 0x3F); + } + return (char) res; + } else { + return 0x3F; + } + } + + private int getByte(int position) { + if (position + 1 >= length) { + throw new IllegalStateException("Malformed DN: " + dn); + } + int b1, b2; + b1 = chars[position]; + if (b1 >= '0' && b1 <= '9') { + b1 = b1 - '0'; + } else if (b1 >= 'a' && b1 <= 'f') { + b1 = b1 - 87; + } else if (b1 >= 'A' && b1 <= 'F') { + b1 = b1 - 55; + } else { + throw new IllegalStateException("Malformed DN: " + dn); + } + b2 = chars[position + 1]; + if (b2 >= '0' && b2 <= '9') { + b2 = b2 - '0'; + } else if (b2 >= 'a' && b2 <= 'f') { + b2 = b2 - 87; + } else if (b2 >= 'A' && b2 <= 'F') { + b2 = b2 - 55; + } else { + throw new IllegalStateException("Malformed DN: " + dn); + } + return (b1 << 4) + b2; + } + + public String findMostSpecific(String attributeType) { + pos = 0; + beg = 0; + end = 0; + cur = 0; + chars = dn.toCharArray(); + String attType = nextAT(); + if (attType == null) { + return null; + } + while (true) { + String attValue = ""; + if (pos == length) { + return null; + } + switch (chars[pos]) { + case '"': + attValue = quotedAV(); + break; + case '#': + attValue = hexAV(); + break; + case '+': + case ',': + case ';': + break; + default: + attValue = escapedAV(); + } + if (attributeType.equalsIgnoreCase(attType)) { + return attValue; + } + if (pos >= length) { + return null; + } + if (chars[pos] != ',' && chars[pos] != ';') { + if (chars[pos] != '+') { + throw new IllegalStateException("Malformed DN: " + dn); + } + } + pos++; + attType = nextAT(); + if (attType == null) { + throw new IllegalStateException("Malformed DN: " + dn); + } + } + } +} diff --git a/net-security/src/main/java/org/xbib/net/security/util/HexUtil.java b/net-security/src/main/java/org/xbib/net/security/util/HexUtil.java new file mode 100644 index 0000000..2964229 --- /dev/null +++ b/net-security/src/main/java/org/xbib/net/security/util/HexUtil.java @@ -0,0 +1,40 @@ +package org.xbib.net.security.util; + +import java.util.Objects; + +public class HexUtil { + + private HexUtil() { + } + + public static String toHex(byte[] data) { + StringBuilder sb = new StringBuilder(); + for (byte b : data) { + sb.append(Character.forDigit((b & 240) >> 4, 16)).append(Character.forDigit((b & 15), 16)); + } + return sb.toString(); + } + + public static byte[] fromHex(String hex) { + Objects.requireNonNull(hex); + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) fromHex(hex.charAt(i), hex.charAt(i + 1)); + } + return data; + } + + public static int fromHex(int b1, int b2) { + int i1 = Character.digit(b1, 16); + if (i1 == -1) { + throw new IllegalArgumentException("invalid character in hexadecimal: " + b1); + } + int i2 = Character.digit(b2, 16); + if (i2 == -1) { + throw new IllegalArgumentException("invalid character in hexadecimal: " + b2); + } + return (i1 << 4) + i2; + } + +} diff --git a/net-security/src/main/resources/META-INF/services/java.security.Provider b/net-security/src/main/resources/META-INF/services/java.security.Provider new file mode 100644 index 0000000..5b44397 --- /dev/null +++ b/net-security/src/main/resources/META-INF/services/java.security.Provider @@ -0,0 +1 @@ +org.xbib.net.security.eddsa.EdDSASecurityProvider \ No newline at end of file diff --git a/net-security/src/main/resources/META-INF/services/org.xbib.net.security.CertificateProvider b/net-security/src/main/resources/META-INF/services/org.xbib.net.security.CertificateProvider new file mode 100644 index 0000000..d4b7846 --- /dev/null +++ b/net-security/src/main/resources/META-INF/services/org.xbib.net.security.CertificateProvider @@ -0,0 +1 @@ +org.xbib.net.security.DefaultCertificateProvider \ No newline at end of file diff --git a/net-security/src/test/java/org/xbib/net/security/CreateKeyStoreTest.java b/net-security/src/test/java/org/xbib/net/security/CreateKeyStoreTest.java new file mode 100644 index 0000000..e668c6e --- /dev/null +++ b/net-security/src/test/java/org/xbib/net/security/CreateKeyStoreTest.java @@ -0,0 +1,160 @@ +package org.xbib.net.security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.Test; +import org.xbib.net.security.util.HexUtil; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class CreateKeyStoreTest { + + @Test + public void list() { + Arrays.stream(Security.getProviders()) + .flatMap(p -> p.entrySet().stream()) + .map(e -> (String) e.getKey()) + .filter(e -> e.startsWith("KeyStore.")) + .filter(e -> !e.endsWith("ImplementedIn")) + .map(e -> e.substring("KeyStore.".length())) + .sorted() + .forEach(System.out::println); + } + + @Test + public void testCreation() throws Exception { + String keyStorePassword = "secure"; + + InputStream keyInputStream = getClass().getResourceAsStream("/rsa.key"); + if (keyInputStream == null) { + return; + } + InputStream chainInputStream = getClass().getResourceAsStream("/rsa.crt"); + if (chainInputStream == null) { + return; + } + String privateKeyPassword = null; + + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, keyStorePassword.toCharArray()); + + // this works with "Sun", not "BC" ("PKCS12 does not support non-PrivateKeys") + byte[] encodedKey = HexUtil.fromHex("227d95a88d02cb89823b91f8e6a6d34435c8e391a5576acd0bd4d64464a6e020"); + SecretKey secretKey = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); + KeyStore.SecretKeyEntry secretKeyEntry = new KeyStore.SecretKeyEntry(secretKey); + KeyStore.ProtectionParameter protectionParameter = new KeyStore.PasswordProtection(keyStorePassword.toCharArray()); + keyStore.setEntry("secret", secretKeyEntry, protectionParameter); + + Collection certChain = null; + PrivateKey privateKey = null; + ServiceLoader certificateProviders = ServiceLoader.load(CertificateProvider.class); + boolean found = false; + for (CertificateProvider provider : certificateProviders) { + Map.Entry> entry = + provider.provide(keyInputStream, privateKeyPassword, chainInputStream); + if (entry != null) { + privateKey = entry.getKey(); + certChain = entry.getValue(); + found = true; + break; + } + } + if (!found) { + throw new CertificateException("no certificate found"); + } + KeyStore.PrivateKeyEntry privateKeyEntry = new KeyStore.PrivateKeyEntry(privateKey, + CertificateReader.orderCertificateChain(certChain).toArray(new Certificate[0])); + protectionParameter = new KeyStore.PasswordProtection(keyStorePassword.toCharArray()); + keyStore.setEntry("key", privateKeyEntry, protectionParameter); + + for (X509Certificate certificate : readTrustStore()) { + KeyStore.TrustedCertificateEntry trustedCertificateEntry = new KeyStore.TrustedCertificateEntry(certificate); + String alias = certificate.getSubjectX500Principal().getName(); + System.out.println("alias = " + alias); + keyStore.setEntry(alias, trustedCertificateEntry, null); + } + + keyStore.store(new FileOutputStream("build/keystore.pkcs12"), keyStorePassword.toCharArray()); + } + + @Test + public void testRead() throws Exception { + String keyStorePassword = "secure"; + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + Path path = Paths.get("build/keystore.pkcs12"); + if (!Files.exists(path)) { + return; // ignore this test + } + keyStore.load(Files.newInputStream(path), keyStorePassword.toCharArray()); + Enumeration e = keyStore.aliases(); + while (e.hasMoreElements()) { + String alias = e.nextElement(); + if ("secret".equals(alias)) { + SecretKey secretKey = (SecretKey) keyStore.getKey(alias, keyStorePassword.toCharArray()); + assertNotNull(secretKey); + System.out.println("secret = " + HexUtil.toHex(secretKey.getEncoded())); + } + if ("key".equals(alias)) { + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, keyStorePassword.toCharArray()); + assertNotNull(privateKey); + System.out.println("algo=" + privateKey.getAlgorithm()); + System.out.println("format=" + privateKey.getFormat()); + X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate(alias); + assertNotNull(x509Certificate); + Principal subject = x509Certificate.getSubjectX500Principal(); + String[] subjectArray = subject.toString().split(","); + for (String s : subjectArray) { + String[] str = s.trim().split("="); + String key = str[0]; + String value = str[1]; + System.out.println(key + " - " + value); + } + } else { + Certificate certificate = keyStore.getCertificate(alias); + if (certificate instanceof X509Certificate) { + X509Certificate x509Certificate = (X509Certificate) certificate; + Principal subject = x509Certificate.getSubjectX500Principal(); + System.out.println("found certificate: " + subject.toString()); + } + } + } + } + + private List readTrustStore() throws Exception { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + List trustManagers = Arrays.asList(trustManagerFactory.getTrustManagers()); + List list = trustManagers.stream() + .filter(X509TrustManager.class::isInstance) + .map(X509TrustManager.class::cast) + .map(trustManager -> Arrays.asList(trustManager.getAcceptedIssuers())) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + System.out.println("found " + list.size() + " certificates in trust manager"); + return list; + } +} diff --git a/net-security/src/test/java/org/xbib/net/security/PrivateKeyReaderTest.java b/net-security/src/test/java/org/xbib/net/security/PrivateKeyReaderTest.java new file mode 100644 index 0000000..7873f9f --- /dev/null +++ b/net-security/src/test/java/org/xbib/net/security/PrivateKeyReaderTest.java @@ -0,0 +1,99 @@ +package org.xbib.net.security; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class PrivateKeyReaderTest { + + @Test + public void testRSA() throws Exception { + InputStream inputStream = getClass().getResourceAsStream("/rsa.key"); + if (inputStream != null) { + PrivateKeyReader privateKeyReader = new PrivateKeyReader(); + PrivateKey privateKey = privateKeyReader.readPrivateKey(inputStream, null); + assertEquals("PKCS#8", privateKey.getFormat()); + } else { + fail(); + } + } + + @Test + public void testDSA() throws Exception { + InputStream inputStream = getClass().getResourceAsStream("/dsa.key"); + if (inputStream != null) { + PrivateKeyReader privateKeyReader = new PrivateKeyReader(); + PrivateKey privateKey = privateKeyReader.readPrivateKey(inputStream, null); + assertEquals("PKCS#8", privateKey.getFormat()); + } else { + fail(); + } + } + + @Test + public void testEd25519() throws Exception { + InputStream inputStream = getClass().getResourceAsStream("/ed25519.key"); + if (inputStream != null) { + PrivateKeyReader privateKeyReader = new PrivateKeyReader(); + PrivateKey privateKey = privateKeyReader.readPrivateKey(inputStream, null); + assertEquals("PKCS#8", privateKey.getFormat()); + } else { + fail(); + } + } + + @Test + public void testEc() throws Exception { + InputStream inputStream = getClass().getResourceAsStream("/ec.key"); + if (inputStream != null) { + PrivateKeyReader privateKeyReader = new PrivateKeyReader(); + PrivateKey privateKey = privateKeyReader.readPrivateKey(inputStream, null); + assertEquals("PKCS#8", privateKey.getFormat()); + } else { + fail(); + } + } + + @Test + public void testReadPKCS1() throws Exception { + final String PKCS1 = + "MIIEowIBAAKCAQEA0OIArlYES4X1XMTLDordtN/XIWFE1wvhl40RsHWM2n99+Stp" + + "CCJCcUb5FJ2/kefj/XRwB6p5IMpIZrHZqC8XXzlX5fpiFaSu2xnk17oWUKoErW27" + + "Stm098pU2RoUxWPKVl+42a8iVp8tijNElBNFALCGi0zXOhcTxMh0q1Wk0UhMJqam" + + "v5YnCKmT4THwwGYn/KeK3M7Qa+o5MoVBHLbeT9LJgEmSluVzIh44Lh6weX0bw72P" + + "8X2praOhbzg2B343MqS/rMLw6On+0i7ccEgp23vX9G5w85q4A5FSIrk4S/pyv5sO" + + "rwjCQKBW1TS0/2iB9zNkFMj5/+h7l2oqTT7sSQIDAQABAoIBADn6sXOynoiUC1IP" + + "sck8lGOTSjSSujfyrVCSsJlJV6qCfuX9va6rS8QDjjnBu531PtxoSHxoPizy2Pvg" + + "W+kKATPGR/am9DjLuFlKq7GRjoYfWyMEdVtGaKvq9ng4fBF6LHyjHz0VFrPyhQJ6" + + "TovHeXzCguYBkzAlnbAeb/vqzs/kABbOuSHVi7DsaixCoEX9zOptFYQw/l8rh68+" + + "UF2bpNNH3jOC1uN3vZtuSwCupqtN+2Mpkx2h04Rk75vWIhrnPeMgmcd3yP4LNZMR" + + "mfaynb63RRzVkNis7+NVk016SQ1oL79mrBvy5rBg3HeCeArwvqZAmOaWsLSWHzCy" + + "zlVlMTECgYEA6JlnMpC956Qi8HX5ye4Hu2ovBdbNGtH/TMkZmColJz9P7CvNkNIb" + + "Od6mvLMydbPHkhdBUDWD4rhiCKHrf5zKju1i24YqWcvuSGotWj4/KQ3+87mLZM+7" + + "daBsJBmSEVB80sgA9ItqSgOyNoNFpiDgFnlszAfb0n9XXEzB/pwSw1UCgYEA5eXI" + + "d+eKugugP+n6CluQfyxfN6WWCzfqWToCTTxPn2i12AiEssXy+kyLjupJVLWSivdo" + + "83wD5LuxFRGc9P+aKQERPhb0AFaxf1llUCXla65/x2So5xjMvtuzgQ0OktPJqJXq" + + "hYGunctsr5rje33+7vlx4xWkrL2PrQWzJabn7SUCgYEAqw3FesY/Ik7u8u+P1xSZ" + + "0xXvptek1oiAu7NYgzLbR9WjrQc5kbsyEojPDg6qmSyxI5q+iYIRj3YRgk+xpJNl" + + "0154SQCNvKPghJiw6aDFSifkytA01tp9/a8QWCwF433RjiFPsoekjvHQ6Y34dofO" + + "xDhf7lwJKPBFCrfYIqocklECgYAIPI9OHHGP8NKw94UJ0fX/WGug5sHVbQ9sWvOy" + + "KLMBlxLMxqFadlUaOpvVZvdxnX++ktajwpGxJDhX9OWWsYGobm1buB7N1E1Prrg+" + + "gt0RWpMhZa3Xeb/8Jorr2Lfo8sWK0LQyTE8hQCSIthfoWL9FeJJn/GKF/dSj8kxU" + + "0QIGMQKBgG/8U/zZ87DzfXS81P1p+CmH474wmou4KD2/zXp/lDR9+dlIUeijlIbU" + + "P6Y5xJvT33Y40giW9irShgDHjZgw0ap11K3b2HzLImdPEaBiENo735rpLs8WLK9H" + + "+yeRbiP2y9To7sTihm9Jrkctzp6sqFtKyye1+S21X1tMz8NGfXen"; + byte[] b = ("-----BEGIN RSA PRIVATE KEY-----\n" + PKCS1 + + "\n-----END RSA PRIVATE KEY-----\n").getBytes(StandardCharsets.UTF_8); + PrivateKeyReader privateKeyReader = new PrivateKeyReader(); + PrivateKey privateKey = privateKeyReader.readPrivateKey(new ByteArrayInputStream(b), null); + assertEquals("PKCS#8", privateKey.getFormat()); + assertEquals("RSA", privateKey.getAlgorithm()); + } +} diff --git a/net-security/src/test/resources/logging.properties b/net-security/src/test/resources/logging.properties new file mode 100644 index 0000000..f8086a4 --- /dev/null +++ b/net-security/src/test/resources/logging.properties @@ -0,0 +1,9 @@ +handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.FileHandler.level=ALL +java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.FileHandler.pattern=build/net.log +jdk.event.security.level=INFO diff --git a/net-socket/LICENSE.txt b/net-socket/LICENSE.txt new file mode 100644 index 0000000..25a529c --- /dev/null +++ b/net-socket/LICENSE.txt @@ -0,0 +1,662 @@ +GNU AFFERO GENERAL PUBLIC LICENSE + + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/net-socket/NOTICE.txt b/net-socket/NOTICE.txt new file mode 100644 index 0000000..09f5b8c --- /dev/null +++ b/net-socket/NOTICE.txt @@ -0,0 +1,5 @@ +Taken from + +https://github.com/OpenNMS/opennms/tree/269739ee8287cc18b5645de679b60015882809ed/core/icmp-jna + +License: Affero GPLv3 diff --git a/net-socket/README.md b/net-socket/README.md new file mode 100644 index 0000000..84ee221 --- /dev/null +++ b/net-socket/README.md @@ -0,0 +1,15 @@ +net-socket +========== + +For user permissions to bind an ICMP socket on Linux, run + +``` +# sysctl -w net.ipv4.ping_group_range="0 65535" +``` +and + +``` +# checkmodule -M -m -o ping.mod ping.te +# semodule_package -o ping.pp -m ping.mod +# semodule -i ping.pp +``` \ No newline at end of file diff --git a/net-socket/build.gradle b/net-socket/build.gradle new file mode 100644 index 0000000..843d0a5 --- /dev/null +++ b/net-socket/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation libs.jna +} diff --git a/net-socket/ping.te b/net-socket/ping.te new file mode 100644 index 0000000..23c854c --- /dev/null +++ b/net-socket/ping.te @@ -0,0 +1,11 @@ + +module ping 1.0; + +require { + type unconfined_t; + type port_t; + class icmp_socket name_bind; +} + +#============= unconfined_t ============== +allow unconfined_t port_t:icmp_socket name_bind; diff --git a/net-socket/src/main/java/module-info.java b/net-socket/src/main/java/module-info.java new file mode 100644 index 0000000..9507bd4 --- /dev/null +++ b/net-socket/src/main/java/module-info.java @@ -0,0 +1,18 @@ +module org.xbib.net.socket { + requires java.logging; + requires com.sun.jna; + exports org.xbib.net.socket; + exports org.xbib.net.socket.v4; + exports org.xbib.net.socket.v4.bsd; + exports org.xbib.net.socket.v4.datagram; + exports org.xbib.net.socket.v4.icmp; + exports org.xbib.net.socket.v4.ip; + exports org.xbib.net.socket.v4.ping; + exports org.xbib.net.socket.v4.unix; + exports org.xbib.net.socket.v6; + exports org.xbib.net.socket.v6.bsd; + exports org.xbib.net.socket.v6.datagram; + exports org.xbib.net.socket.v6.icmp; + exports org.xbib.net.socket.v6.ping; + exports org.xbib.net.socket.v6.unix; +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/Metric.java b/net-socket/src/main/java/org/xbib/net/socket/Metric.java new file mode 100644 index 0000000..74b26e5 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/Metric.java @@ -0,0 +1,62 @@ +package org.xbib.net.socket; + +import java.util.concurrent.TimeUnit; + +public class Metric { + + private int count = 0; + + private double sumOfSquaresOfDifferences = 0.0; + + private double average = 0.0; + + private long min = Long.MAX_VALUE; + + private long max = Long.MIN_VALUE; + + public Metric() { + } + + public int getCount() { + return count; + } + + public double getSumOfSquaresOfDifferences() { + return sumOfSquaresOfDifferences; + } + + public double getStandardDeviation() { + return count == 0 ? 0.0 : Math.sqrt(getSumOfSquaresOfDifferences() / getCount()); + } + + public double getAverage() { + return average; + } + + public long getMinimum() { + return min; + } + + public long getMaximum() { + return max; + } + + public void update(long sample) { + count++; + double oldAvg = average; + average += (sample - oldAvg)/ count; + sumOfSquaresOfDifferences += (sample - getAverage())*(sample - oldAvg); + min = Math.min(min, sample); + max = Math.max(max, sample); + } + + public String getSummary(TimeUnit unit) { + double nanosPerUnit = TimeUnit.NANOSECONDS.convert(1, unit); + return String.format("cnt/min/avg/max/stddev = %d/%.3f/%.3f/%.3f/%.3f", + getCount(), + getMinimum()/nanosPerUnit, + getAverage()/nanosPerUnit, + getMaximum()/nanosPerUnit, + getStandardDeviation()/nanosPerUnit); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/NetworkUnreachableException.java b/net-socket/src/main/java/org/xbib/net/socket/NetworkUnreachableException.java new file mode 100644 index 0000000..7481ed9 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/NetworkUnreachableException.java @@ -0,0 +1,9 @@ +package org.xbib.net.socket; + +@SuppressWarnings("serial") +public class NetworkUnreachableException extends Exception { + + public NetworkUnreachableException() { + super(); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/notify/SystemdNotify.java b/net-socket/src/main/java/org/xbib/net/socket/notify/SystemdNotify.java new file mode 100644 index 0000000..ced2f72 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/notify/SystemdNotify.java @@ -0,0 +1,32 @@ +package org.xbib.net.socket.notify; + +import java.io.IOException; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.CharBuffer; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class SystemdNotify { + + public SystemdNotify() { + } + + public static void sendNotify() throws IOException { + sendNotify("READY=1"); + } + + public static void sendNotify(String text) throws IOException { + String socketName = System.getenv("NOTIFY_SOCKET"); + if (socketName != null) { + Path path = Paths.get(socketName); + UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(path); + try (SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX)) { + channel.connect(socketAddress); + channel.write(StandardCharsets.US_ASCII.encode(CharBuffer.wrap(text))); + } + } + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/Addressable.java b/net-socket/src/main/java/org/xbib/net/socket/v4/Addressable.java new file mode 100644 index 0000000..7270cb3 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/Addressable.java @@ -0,0 +1,10 @@ +package org.xbib.net.socket.v4; + +import java.net.Inet4Address; + +public interface Addressable { + + Inet4Address getAddress(); + + int getPort(); +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/Constants.java b/net-socket/src/main/java/org/xbib/net/socket/v4/Constants.java new file mode 100644 index 0000000..62199b3 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/Constants.java @@ -0,0 +1,14 @@ +package org.xbib.net.socket.v4; + +public interface Constants { + + int AF_INET = 2; + + int IPPROTO_IP = 0; + + int IPPROTO_ICMP = 1; + + int IP_MTU_DISCOVER = 10; + + int IP_TOS = 1; +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/SocketFactory.java b/net-socket/src/main/java/org/xbib/net/socket/v4/SocketFactory.java new file mode 100644 index 0000000..c416516 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/SocketFactory.java @@ -0,0 +1,35 @@ +package org.xbib.net.socket.v4; + +import com.sun.jna.Platform; +import org.xbib.net.socket.v4.datagram.DatagramSocket; + +import java.lang.reflect.InvocationTargetException; + +public class SocketFactory { + + public static final int SOCK_DGRAM = Platform.isSolaris() ? 1 : 2; + + private SocketFactory() { + } + + public static DatagramSocket createDatagramSocket(int protocol, int port) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + Class implementationClass = Class.forName(getImplementationClassName()) + .asSubclass(DatagramSocket.class); + return implementationClass + .getDeclaredConstructor(Integer.TYPE, Integer.TYPE, Integer.TYPE) + .newInstance(SOCK_DGRAM, protocol, port); + } + + + private static String getImplementationClassName() { + return "org.xbib.net.socket.v4." + getArchName() + ".NativeDatagramSocket"; + } + + private static String getArchName() { + return Platform.isWindows() ? "win32" + : Platform.isSolaris() ? "sun" + : (Platform.isMac() || Platform.isFreeBSD() || Platform.isOpenBSD()) ? "bsd" + : "unix"; + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/bsd/NativeDatagramSocket.java b/net-socket/src/main/java/org/xbib/net/socket/v4/bsd/NativeDatagramSocket.java new file mode 100644 index 0000000..5e86753 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/bsd/NativeDatagramSocket.java @@ -0,0 +1,117 @@ +package org.xbib.net.socket.v4.bsd; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; +import org.xbib.net.socket.v4.datagram.DatagramPacket; +import org.xbib.net.socket.v4.datagram.DatagramSocket; +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; + +import static org.xbib.net.socket.v4.Constants.AF_INET; +import static org.xbib.net.socket.v4.Constants.IPPROTO_IP; +import static org.xbib.net.socket.v4.Constants.IP_MTU_DISCOVER; + +public class NativeDatagramSocket implements DatagramSocket, AutoCloseable { + + static { + Native.register((String) null); + } + + private static final int IP_TOS = 3; + + private final int socket; + + private volatile boolean closed; + + public NativeDatagramSocket(int type, int protocol, int port) { + this.socket = socket(AF_INET, type, protocol); + if (socket < 0) { + throw new IllegalStateException("socket < 0"); + } + SocketStructure socketStructure = new SocketStructure(port); + bind(socket, socketStructure, socketStructure.size()); + closed = false; + } + + public native int bind(int socket, SocketStructure address, int address_len) throws LastErrorException; + + public native int socket(int family, int type, int protocol) throws LastErrorException; + + public native int setsockopt(int socket, int level, int option_name, Pointer value, int len); + + public native int sendto(int socket, Buffer buffer, int buflen, int flags, SocketStructure address, int len) throws LastErrorException; + + public native int recvfrom(int socket, Buffer buffer, int buflen, int flags, SocketStructure address, int[] len) throws LastErrorException; + + public native int close(int socket) throws LastErrorException; + + public native String strerror(int errnum); + + @Override + public int setTrafficClass(int tc) throws IOException { + IntByReference ptr = new IntByReference(tc); + try { + return setsockopt(socket, IPPROTO_IP, IP_TOS, ptr.getPointer(), Native.POINTER_SIZE); + } catch (LastErrorException e) { + throw new IOException("setsockopt: " + strerror(e.getErrorCode())); + } + } + + @Override + public int setFragmentation(boolean frag) throws IOException { + return allowFragmentation(IPPROTO_IP, IP_MTU_DISCOVER, frag); + } + + private int allowFragmentation(int level, int option_name, boolean frag) throws IOException { + if (closed) { + return -1; + } + IntByReference dontfragment = new IntByReference(frag ? 0 : 1); + try { + return setsockopt(socket, level, option_name, dontfragment.getPointer(), Native.POINTER_SIZE); + } catch (LastErrorException e) { + throw new IOException("setsockopt: " + strerror(e.getErrorCode())); + } + } + + @Override + public int receive(DatagramPacket datagramPacket) { + if (closed) { + return -1; + } + try { + SocketStructure socketStructure = new SocketStructure(); + int[] szRef = new int[]{socketStructure.size()}; + ByteBuffer buf = datagramPacket.getContent(); + int n = recvfrom(socket, buf, buf.capacity(), 0, socketStructure, szRef); + datagramPacket.setLength(n); + datagramPacket.setAddressable(socketStructure); + return n; + } catch (LastErrorException e) { + if (e.getMessage().contains("[9]")) { + // bad file descriptor + return -1; + } + throw e; + } + } + + @Override + public int send(DatagramPacket datagramPacket) { + if (closed) { + return -1; + } + ByteBuffer buf = datagramPacket.getContent(); + SocketStructure socketStructure = new SocketStructure(datagramPacket.getAddress(), datagramPacket.getPort()); + return sendto(socket, buf, buf.remaining(), 0, socketStructure, socketStructure.size()); + } + + @Override + public void close() { + closed = true; + close(socket); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/bsd/SocketStructure.java b/net-socket/src/main/java/org/xbib/net/socket/v4/bsd/SocketStructure.java new file mode 100644 index 0000000..a89009e --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/bsd/SocketStructure.java @@ -0,0 +1,89 @@ +package org.xbib.net.socket.v4.bsd; + +import static org.xbib.net.socket.v4.Constants.AF_INET; +import com.sun.jna.Structure; +import org.xbib.net.socket.v4.Addressable; +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; + +public class SocketStructure extends Structure implements Addressable { + + public byte sin_len; + + public byte sin_family; + + public byte[] sin_port; + + public byte[] sin_addr; + + public byte[] sin_zero; + + public SocketStructure() { + this(0); + } + + public SocketStructure(int port) { + this(null, port); + } + + public SocketStructure(Inet4Address address, int port) { + this(AF_INET, address, port); + } + + public SocketStructure(int family, Inet4Address address, int port) { + this.sin_family = (byte) (0xff & family); + this.sin_zero = new byte[8]; + this.sin_len = (byte) (0xff & 16); + setAddress(address); + setPort(port); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("sin_len", "sin_family", "sin_port", "sin_addr", "sin_zero"); + } + + @Override + public Inet4Address getAddress() { + try { + return (Inet4Address) Inet4Address.getByAddress(sin_addr); + } catch (UnknownHostException e) { + return null; + } + } + + public void setAddress(Inet4Address address) { + if (address != null) { + byte[] addr = address.getAddress(); + assertLen("address", addr, 4); + sin_addr = addr; + } else { + sin_addr = new byte[] { 0, 0, 0, 0 }; + } + } + + @Override + public int getPort() { + int port = 0; + for (int i = 0; i < 2; i++) { + port = ((port << 8) | (sin_port[i] & 0xff)); + } + return port; + } + + public void setPort(int port) { + if (port >= 0) { + byte[] p = new byte[]{(byte) (0xff & (port >> 8)), (byte) (0xff & port)}; + assertLen("port", p, 2); + sin_port = p; + } + } + + private void assertLen(String field, byte[] addr, int len) { + if (addr.length != len) { + throw new IllegalArgumentException(field + " length must be " + len + " bytes"); + } + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/datagram/DatagramPacket.java b/net-socket/src/main/java/org/xbib/net/socket/v4/datagram/DatagramPacket.java new file mode 100644 index 0000000..96bf72b --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/datagram/DatagramPacket.java @@ -0,0 +1,96 @@ +package org.xbib.net.socket.v4.datagram; + +import org.xbib.net.socket.v4.Addressable; + +import java.net.Inet4Address; +import java.nio.ByteBuffer; + +public class DatagramPacket implements Addressable { + + private final ByteBuffer byteBuffer; + + private Addressable addressable; + + public DatagramPacket(int size) { + this(ByteBuffer.allocate(size)); + } + + public DatagramPacket(byte[] data) { + this(ByteBuffer.wrap(data)); + } + + public DatagramPacket(ByteBuffer byteBuffer) { + this.byteBuffer = byteBuffer; + } + + public void setAddressable(Addressable addressable) { + this.addressable = addressable; + } + + public void setAddress(Inet4Address inetAddress, int port) { + this.addressable = new Addressable() { + @Override + public Inet4Address getAddress() { + return inetAddress; + } + + @Override + public int getPort() { + return port; + } + }; + } + + public Addressable getAddressable() { + return addressable; + } + + public Inet4Address getAddress() { + return addressable != null ?addressable.getAddress() : null; + } + + public int getPort() { + return addressable != null ? addressable.getPort() : -1; + } + + public int getLength() { + return byteBuffer.limit(); + } + + public void setLength(int length) { + byteBuffer.limit(length); + } + + public ByteBuffer getContent() { + return byteBuffer.duplicate(); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("Address: "); + if (addressable != null) { + buf.append(addressable.getAddress()); + } + buf.append(" Port: "); + if (addressable != null) { + buf.append(addressable.getPort()); + } + buf.append("\nData: "); + ByteBuffer data = byteBuffer.duplicate(); + buf.append(data.limit()); + buf.append(" Bytes\n"); + int bytesPerRow = 256; + int limit = data.limit(); + int rows = (limit + bytesPerRow) / bytesPerRow; + int index = 0; + for(int i = 0; i < rows && index < limit; i++) { + for(int j = 0; j < bytesPerRow && index < limit; j++) { + buf.append(String.format("%02X", data.get(index++))); + } + buf.append("\n"); + } + buf.append("\n"); + return buf.toString(); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/datagram/DatagramSocket.java b/net-socket/src/main/java/org/xbib/net/socket/v4/datagram/DatagramSocket.java new file mode 100644 index 0000000..e2690d6 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/datagram/DatagramSocket.java @@ -0,0 +1,18 @@ +package org.xbib.net.socket.v4.datagram; + +import org.xbib.net.socket.NetworkUnreachableException; + +import java.io.Closeable; +import java.io.IOException; +import java.net.UnknownHostException; + +public interface DatagramSocket extends Closeable { + + int setFragmentation(boolean frag) throws IOException; + + int setTrafficClass(int trafficClass) throws IOException; + + int receive(DatagramPacket datagramPacket) throws UnknownHostException; + + int send(DatagramPacket datagramPacket) throws NetworkUnreachableException; +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/icmp/EchoPacket.java b/net-socket/src/main/java/org/xbib/net/socket/v4/icmp/EchoPacket.java new file mode 100644 index 0000000..9c0fe06 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/icmp/EchoPacket.java @@ -0,0 +1,40 @@ +package org.xbib.net.socket.v4.icmp; + +import java.nio.ByteBuffer; + +public class EchoPacket extends Packet { + + public EchoPacket(int size) { + super(size); + } + + public EchoPacket(Packet packet) { + super(packet); + } + + public ByteBuffer getContentBuffer() { + ByteBuffer content = byteBuffer.duplicate(); + content.position(8); + return content.slice(); + } + + public int getPacketLength() { + return byteBuffer.limit(); + } + + public int getIdentifier() { + return getUnsignedShort(4); + } + + public void setIdentifier(int id) { + setUnsignedShort(4, id); + } + + public int getSequenceNumber() { + return getUnsignedShort(6); + } + + public void setSequenceNumber(int sn) { + setUnsignedShort(6, sn); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/icmp/Packet.java b/net-socket/src/main/java/org/xbib/net/socket/v4/icmp/Packet.java new file mode 100644 index 0000000..5ccbead --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/icmp/Packet.java @@ -0,0 +1,133 @@ +package org.xbib.net.socket.v4.icmp; + +import org.xbib.net.socket.v4.datagram.DatagramPacket; + +import java.net.Inet4Address; +import java.nio.ByteBuffer; + +public class Packet { + + public static final int CHECKSUM_INDEX = 2; + + public enum Type { + EchoReply(0), + DestUnreachable(3), + SourceQuench(4), + Redirect(5), + EchoRequest(8), + TimeExceeded(11), + Traceroute(30), + Other(-1); + + private final int code; + + Type(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public static Type toType(int code) { + for (Type p : Type.values()) { + if (code == p.getCode()) { + return p; + } + } + return Other; + } + + } + + protected ByteBuffer byteBuffer; + + public Packet(ByteBuffer byteBuffer) { + this.byteBuffer = byteBuffer; + } + + public Packet(Packet packet) { + this(packet.byteBuffer.duplicate()); + } + + public Packet(int size) { + this(ByteBuffer.allocate(size)); + } + + public Type getType() { + return Type.toType(byteBuffer.get(0)); + } + + public void setType(Type t) { + byteBuffer.put(0, ((byte) (t.getCode()))); + } + + public int getCode() { + return 0xff & byteBuffer.get(1); + } + + public void setCode(int code) { + byteBuffer.put(1, ((byte) code)); + } + + public int getChecksum() { + return getUnsignedShort(2); + } + + public void setChecksum() { + setUnsignedShort(2, computeChecksum()); + } + + public int computeChecksum() { + int sum = 0; + int count = byteBuffer.remaining(); + int index = 0; + while (count > 1) { + if (index != CHECKSUM_INDEX) { + sum += getUnsignedShort(index); + } + index += 2; + count -= 2; + } + if (count > 0) { + sum += makeUnsignedShort(byteBuffer.get((byteBuffer.remaining() - 1)), (byte) 0); + } + int sumLo = sum & 0xffff; + int sumHi = (sum >> 16) & 0xffff; + sum = sumLo + sumHi; + sumLo = sum & 0xffff; + sumHi = (sum >> 16) & 0xffff; + sum = sumLo + sumHi; + return (~sum) & 0xffff; + } + + public void setBytes(int index, byte[] b) { + ByteBuffer payload = byteBuffer; + int oldPos = payload.position(); + try { + payload.position(index); + payload.put(b); + } finally { + payload.position(oldPos); + } + } + + public int makeUnsignedShort(byte b1, byte b0) { + return 0xffff & (((b1 & 0xff) << 8) | ((b0 & 0xff))); + } + + public int getUnsignedShort(int index) { + return byteBuffer.getShort(index) & 0xffff; + } + + public void setUnsignedShort(int index, int us) { + byteBuffer.putShort(index, ((short) (us & 0xffff))); + } + + public DatagramPacket toDatagramPacket(Inet4Address destinationAddress) { + setChecksum(); + DatagramPacket p = new DatagramPacket(byteBuffer.duplicate()); + p.setAddress(destinationAddress, 0); + return p; + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/ip/Packet.java b/net-socket/src/main/java/org/xbib/net/socket/v4/ip/Packet.java new file mode 100644 index 0000000..c3121e5 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/ip/Packet.java @@ -0,0 +1,105 @@ +package org.xbib.net.socket.v4.ip; + +import org.xbib.net.socket.v4.datagram.DatagramPacket; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; + +public class Packet { + + public enum Protocol { + ICMP(1), + TCP(6), + UDP(17), + V6_OVER_V4(41); + private final int m_code; + Protocol(int code) { + m_code = code; + } + + public int getCode() { + return m_code; + } + + public static Protocol toProtocol(int code) { + for(Protocol p : Protocol.values()) { + if (code == p.getCode()) { + return p; + } + } + throw new IllegalArgumentException(String.format("Unabled to find Protocol with code %d", code)); + } + + } + + private final ByteBuffer m_buffer; + + public Packet(Packet p) { + this(p.m_buffer.duplicate()); + } + + public Packet(ByteBuffer buffer) { + m_buffer = buffer; + } + + public Packet(byte[] data, int offset, int length) { + this(ByteBuffer.wrap(data, offset, length).slice()); + } + + public Packet(DatagramPacket datagram) { + this(datagram.getContent()); + } + + public int getVersion() { + return ((m_buffer.get(0) & 0xf0) >> 4); + } + + public int getHeaderLength() { + return (m_buffer.get(0) & 0xf) << 2; // shift effectively does * 4 (4 bytes per 32 bit word) + } + + private InetAddress getAddrAtOffset(int offset) { + byte[] addr = new byte[4]; + int oldPos = m_buffer.position(); + try { + m_buffer.position(offset); + m_buffer.get(addr); + } finally { + m_buffer.position(oldPos); + } + InetAddress result = null; + try { + result = InetAddress.getByAddress(addr); + } catch (UnknownHostException e) { + // this can't happen + } + return result; + } + + public int getTimeToLive() { + return 0xff & m_buffer.get(8); + } + + public Protocol getProtocol() { + return Protocol.toProtocol(m_buffer.get(9)); + } + + public InetAddress getSourceAddress() { + return getAddrAtOffset(12); + } + + public InetAddress getDestinationAddress() { + return getAddrAtOffset(16); + } + + public ByteBuffer getPayload() { + ByteBuffer data = m_buffer.duplicate(); + data.position(getHeaderLength()); + return data.slice(); + } + public int getPayloadLength() { + return getPayload().remaining(); + } + +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/ping/Ping.java b/net-socket/src/main/java/org/xbib/net/socket/v4/ping/Ping.java new file mode 100644 index 0000000..ee0c2ad --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/ping/Ping.java @@ -0,0 +1,143 @@ +package org.xbib.net.socket.v4.ping; + +import org.xbib.net.socket.Metric; +import org.xbib.net.socket.NetworkUnreachableException; +import org.xbib.net.socket.v4.SocketFactory; +import org.xbib.net.socket.v4.datagram.DatagramPacket; +import org.xbib.net.socket.v4.datagram.DatagramSocket; +import org.xbib.net.socket.v4.icmp.Packet; +import java.io.Closeable; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.Inet4Address; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.xbib.net.socket.v4.Constants.IPPROTO_ICMP; + +public class Ping implements Runnable, Closeable { + + private static final Logger logger = Logger.getLogger(Ping.class.getName()); + + public static final long PING_COOKIE = StandardCharsets.US_ASCII.encode("org.xbib").getLong(0); + + private final DatagramSocket datagramSocket; + + private final List listeners; + + private volatile boolean closed; + + private Thread thread; + + private PingMetric metric; + + public Ping(int id) + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + this(SocketFactory.createDatagramSocket(IPPROTO_ICMP, id)); + } + + public Ping(DatagramSocket datagramSocket) { + this.datagramSocket = datagramSocket; + this.listeners = new ArrayList<>(); + this.closed = false; + } + + public Metric getMetric() { + return metric; + } + + public boolean isClosed() { + return closed; + } + + public void start() { + thread = new Thread(this, "PingThread:PingListener"); + thread.setDaemon(true); + thread.start(); + } + + public void stop() { + if (thread != null) { + thread.interrupt(); + } + thread = null; + } + + @Override + public void close() throws IOException { + if (datagramSocket != null) { + closed = true; + datagramSocket.close(); + } + } + + public List getListeners() { + return listeners; + } + + public void addPingReplyListener(PingResponseListener listener) { + listeners.add(listener); + } + + public void execute(int id, Inet4Address addr) + throws InterruptedException, NetworkUnreachableException { + Thread t = new Thread(this); + t.start(); + execute(id, addr, 1, 10, 1000); + } + + public void execute(int id, + Inet4Address inet4Address, + int sequenceNumber, + int count, + long interval) + throws InterruptedException, NetworkUnreachableException { + if (inet4Address == null) { + return; + } + metric = new PingMetric(count, interval); + addPingReplyListener(metric); + for(int i = sequenceNumber; i < sequenceNumber + count; i++) { + PingRequest request = new PingRequest(id, i); + int rc = request.send(datagramSocket, inet4Address); + Thread.sleep(interval); + } + } + + @Override + public void run() { + try { + DatagramPacket datagram = new DatagramPacket(65535); + while (!isClosed()) { + int rc = datagramSocket.receive(datagram); + long received = System.nanoTime(); + Packet packet = new Packet(getPayload(datagram)); + if (packet.getType() == Packet.Type.EchoReply) { + PingResponse pingResponse = new PingResponse(packet, received); + if (pingResponse.isValid()) { + logger.log(Level.INFO, String.format("%d bytes from %s: tid=%d icmp_seq=%d time=%.3f ms%n", + pingResponse.getPacketLength(), + datagram.getAddress().getHostAddress(), + pingResponse.getIdentifier(), + pingResponse.getSequenceNumber(), + pingResponse.elapsedTime(TimeUnit.MILLISECONDS))); + for (PingResponseListener listener : getListeners()) { + listener.onPingResponse(datagram.getAddress(), pingResponse); + } + } + } + } + } catch (Throwable e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } + + private ByteBuffer getPayload(final DatagramPacket datagram) { + return new org.xbib.net.socket.v4.ip.Packet(datagram.getContent()).getPayload(); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingMetric.java b/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingMetric.java new file mode 100644 index 0000000..8dfc4e9 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingMetric.java @@ -0,0 +1,35 @@ +package org.xbib.net.socket.v4.ping; + +import org.xbib.net.socket.Metric; + +import java.net.Inet4Address; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class PingMetric extends Metric implements PingResponseListener { + + private final CountDownLatch countDownLatch; + + private final int count; + + private final long interval; + + public PingMetric(int count, long interval) { + this.countDownLatch = new CountDownLatch(count); + this.count = count; + this.interval = interval; + } + + @Override + public void onPingResponse(Inet4Address address, PingResponse reply) { + try { + update(reply.getElapsedTimeNanos()); + } finally { + countDownLatch.countDown(); + } + } + + public void await() throws InterruptedException { + countDownLatch.await(interval * count + 1000, TimeUnit.MILLISECONDS); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingRequest.java b/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingRequest.java new file mode 100644 index 0000000..a120ade --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingRequest.java @@ -0,0 +1,43 @@ +package org.xbib.net.socket.v4.ping; + +import org.xbib.net.socket.NetworkUnreachableException; +import org.xbib.net.socket.v4.datagram.DatagramPacket; +import org.xbib.net.socket.v4.datagram.DatagramSocket; +import org.xbib.net.socket.v4.icmp.EchoPacket; +import org.xbib.net.socket.v4.icmp.Packet; + +import java.net.Inet4Address; +import java.nio.ByteBuffer; + +public class PingRequest extends EchoPacket { + + public PingRequest() { + super(64); + setType(Packet.Type.EchoRequest); + setCode(0); + } + + public PingRequest(int id, int seqNum) { + super(64); + setType(Type.EchoRequest); + setCode(0); + setIdentifier(id); + setSequenceNumber(seqNum); + ByteBuffer buf = getContentBuffer(); + for(int b = 0; b < 56; b++) { + buf.put((byte)b); + } + } + + @Override + public DatagramPacket toDatagramPacket(Inet4Address destinationAddress) { + ByteBuffer contentBuffer = getContentBuffer(); + contentBuffer.putLong(Ping.PING_COOKIE); + contentBuffer.putLong(System.nanoTime()); + return super.toDatagramPacket(destinationAddress); + } + + public int send(DatagramSocket socket, Inet4Address addr) throws NetworkUnreachableException { + return socket.send(toDatagramPacket(addr)); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingResponse.java b/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingResponse.java new file mode 100644 index 0000000..2253058 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingResponse.java @@ -0,0 +1,38 @@ +package org.xbib.net.socket.v4.ping; + +import org.xbib.net.socket.v4.icmp.EchoPacket; +import org.xbib.net.socket.v4.icmp.Packet; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +public class PingResponse extends EchoPacket { + + private final long receivedTimeNanos; + + public PingResponse(Packet packet, long receivedTimeNanos) { + super(packet); + this.receivedTimeNanos = receivedTimeNanos; + } + + public boolean isValid() { + ByteBuffer content = getContentBuffer(); + return content.limit() >= 16 && Ping.PING_COOKIE == content.getLong(0); + } + + public long getSentTimeNanos() { + return getContentBuffer().getLong(8); + } + + public long getReceivedTimeNanos() { + return receivedTimeNanos; + } + + public double elapsedTime(TimeUnit unit) { + double nanosPerUnit = TimeUnit.NANOSECONDS.convert(1, unit); + return getElapsedTimeNanos() / nanosPerUnit; + } + + public long getElapsedTimeNanos() { + return getReceivedTimeNanos() - getSentTimeNanos(); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingResponseListener.java b/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingResponseListener.java new file mode 100644 index 0000000..373ffbb --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/ping/PingResponseListener.java @@ -0,0 +1,8 @@ +package org.xbib.net.socket.v4.ping; + +import java.net.Inet4Address; + +public interface PingResponseListener { + + void onPingResponse(Inet4Address inetAddress, PingResponse reply); +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/unix/NativeDatagramSocket.java b/net-socket/src/main/java/org/xbib/net/socket/v4/unix/NativeDatagramSocket.java new file mode 100644 index 0000000..d2ce375 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/unix/NativeDatagramSocket.java @@ -0,0 +1,131 @@ +package org.xbib.net.socket.v4.unix; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; +import org.xbib.net.socket.NetworkUnreachableException; +import org.xbib.net.socket.v4.datagram.DatagramPacket; +import org.xbib.net.socket.v4.datagram.DatagramSocket; + +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.xbib.net.socket.v4.Constants.AF_INET; +import static org.xbib.net.socket.v4.Constants.IPPROTO_IP; +import static org.xbib.net.socket.v4.Constants.IP_MTU_DISCOVER; +import static org.xbib.net.socket.v4.Constants.IP_TOS; + +public class NativeDatagramSocket implements DatagramSocket, AutoCloseable { + + static { + Native.register((String) null); + } + + private static final Logger logger = Logger.getLogger(NativeDatagramSocket.class.getName()); + + private final int socket; + + private volatile boolean closed; + + public NativeDatagramSocket(int type, int protocol, int port) { + try { + this.socket = socket(AF_INET, type, protocol); + if (socket < 0) { + throw new IllegalStateException("socket < 0"); + } + SocketStructure socketStructure = new SocketStructure(port); + bind(socket, socketStructure, socketStructure.size()); + closed = false; + } catch (LastErrorException e) { + logger.log(Level.SEVERE, e.getMessage() + ": check if sysctl -w net.ipv4.ping_group_range=\"0 65535\" and check for selinux security:\n" + + "allow unconfined_t node_t:icmp_socket node_bind;\n" + + "allow unconfined_t port_t:icmp_socket name_bind;\n"); + throw e; + } + } + + public native int bind(int socket, SocketStructure address, int address_len) throws LastErrorException; + + public native int socket(int domain, int type, int protocol) throws LastErrorException; + + public native int setsockopt(int socket, int level, int option_name, Pointer value, int option_len); + + public native int sendto(int socket, Buffer buffer, int buflen, int flags, SocketStructure dest_addr, int dest_addr_len) throws LastErrorException; + + public native int recvfrom(int socket, Buffer buffer, int buflen, int flags, SocketStructure in_addr, int[] in_addr_len) throws LastErrorException; + + public native int close(int socket) throws LastErrorException; + + public native String strerror(int errnum); + + @Override + public int setTrafficClass(final int tc) throws IOException { + if (closed) { + return -1; + } + IntByReference ptr = new IntByReference(tc); + try { + return setsockopt(socket, IPPROTO_IP, IP_TOS, ptr.getPointer(), Native.POINTER_SIZE); + } catch (final LastErrorException e) { + throw new IOException("setsockopt: " + strerror(e.getErrorCode())); + } + } + + @Override + public int setFragmentation(boolean frag) throws IOException { + return allowFragmentation(IPPROTO_IP, IP_MTU_DISCOVER, frag); + } + + private int allowFragmentation(int level, int option_name, boolean frag) throws IOException { + if (closed) { + return -1; + } + IntByReference dontfragment = new IntByReference(frag ? 0 : 1); + try { + return setsockopt(socket, level, option_name, dontfragment.getPointer(), Native.POINTER_SIZE); + } catch (final LastErrorException e) { + throw new IOException("setsockopt: " + strerror(e.getErrorCode())); + } + } + + @Override + public int receive(DatagramPacket datagramPacket) { + if (closed) { + return -1; + } + SocketStructure in_addr = new SocketStructure(); + int[] szRef = new int[]{in_addr.size()}; + ByteBuffer buf = datagramPacket.getContent(); + int n = recvfrom(socket, buf, buf.capacity(), 0, in_addr, szRef); + datagramPacket.setLength(n); + datagramPacket.setAddressable(in_addr); + return n; + } + + @Override + public int send(DatagramPacket datagramPacket) throws NetworkUnreachableException { + if (closed) { + return -1; + } + SocketStructure destAddr = new SocketStructure(datagramPacket.getAddress(), datagramPacket.getPort()); + ByteBuffer buf = datagramPacket.getContent(); + try { + return sendto(socket, buf, buf.remaining(), 0, destAddr, destAddr.size()); + } catch (LastErrorException e) { + if (e.getMessage().contains("[101]")) { + throw new NetworkUnreachableException(); + } + throw e; + } + } + + @Override + public void close() { + closed = true; + close(socket); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v4/unix/SocketStructure.java b/net-socket/src/main/java/org/xbib/net/socket/v4/unix/SocketStructure.java new file mode 100644 index 0000000..9fe3386 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v4/unix/SocketStructure.java @@ -0,0 +1,87 @@ +package org.xbib.net.socket.v4.unix; + +import com.sun.jna.Structure; +import org.xbib.net.socket.v4.Addressable; + +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; + +public class SocketStructure extends Structure implements Addressable { + + public static final int AF_INET = 2; + + public short sin_family; + + public byte[] sin_port; + + public byte[] sin_addr; + + public byte[] sin_zero = new byte[8]; + + public SocketStructure() { + this(AF_INET, null, 0); + } + + public SocketStructure(int port) { + this(AF_INET, null, port); + } + + public SocketStructure(Inet4Address address, int port) { + this(AF_INET, address, port); + } + + public SocketStructure(int family, Inet4Address address, int port) { + this.sin_family = (short) (0xffff & family); + setAddress(address); + setPort(port); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("sin_family", "sin_port", "sin_addr", "sin_zero"); + } + + @Override + public Inet4Address getAddress() { + try { + return (Inet4Address) Inet4Address.getByAddress(sin_addr); + } catch (UnknownHostException e) { + return null; + } + } + + public void setAddress(Inet4Address address) { + if (address != null) { + byte[] addr = address.getAddress(); + assertLen("address", addr, 4); + this.sin_addr = addr; + } else { + this.sin_addr = new byte[] { 0,0,0,0 }; + } + } + + @Override + public int getPort() { + int port = 0; + for (int i = 0; i < 2; i++) { + port = ((port << 8) | (sin_port[i] & 0xff)); + } + return port; + } + + public void setPort(int port) { + if (port >= 0) { + byte[] p = new byte[]{(byte) (0xff & (port >> 8)), (byte) (0xff & port)}; + assertLen("port", p, 2); + this.sin_port = p; + } + } + + private void assertLen(String field, byte[] addr, int len) { + if (addr.length != len) { + throw new IllegalArgumentException(field + " length must be " + len + " bytes"); + } + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/Addressable.java b/net-socket/src/main/java/org/xbib/net/socket/v6/Addressable.java new file mode 100644 index 0000000..2f26b16 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/Addressable.java @@ -0,0 +1,10 @@ +package org.xbib.net.socket.v6; + +import java.net.Inet6Address; + +public interface Addressable { + + Inet6Address getAddress(); + + int getPort(); +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/Constants.java b/net-socket/src/main/java/org/xbib/net/socket/v6/Constants.java new file mode 100644 index 0000000..48210ad --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/Constants.java @@ -0,0 +1,23 @@ +package org.xbib.net.socket.v6; + +import com.sun.jna.Platform; + +public interface Constants { + + int AF_INET6 = Platform.isLinux() ? 10 + : Platform.isMac() ? 30 + : Platform.isWindows() ? 23 + : Platform.isFreeBSD() ? 28 + : Platform.isSolaris() ? 26 + : -1; + + int IPPROTO_IPV6 = 41; + + int IPPROTO_ICMPV6 = 58; + + int IPV6_DONTFRAG = 62; + + int IPV6_TCLASS = Platform.isLinux() ? 67 + : Platform.isMac() ? 36 + : -1; +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/SocketFactory.java b/net-socket/src/main/java/org/xbib/net/socket/v6/SocketFactory.java new file mode 100644 index 0000000..5a3c0f5 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/SocketFactory.java @@ -0,0 +1,34 @@ +package org.xbib.net.socket.v6; + +import com.sun.jna.Platform; +import org.xbib.net.socket.v6.datagram.DatagramSocket; +import java.lang.reflect.InvocationTargetException; + +public class SocketFactory { + + public static final int SOCK_DGRAM = Platform.isSolaris() ? 1 : 2; + + private SocketFactory() { + } + + public static DatagramSocket createDatagramSocket(int protocol, int port) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + Class implementationClass = Class.forName(getImplementationClassName()) + .asSubclass(DatagramSocket.class); + return implementationClass + .getDeclaredConstructor(Integer.TYPE, Integer.TYPE, Integer.TYPE) + .newInstance(SOCK_DGRAM, protocol, port); + } + + + private static String getImplementationClassName() { + return "org.xbib.net.socket.v6." + getArch() + ".NativeDatagramSocket"; + } + + private static String getArch() { + return Platform.isWindows() ? "win32" + : Platform.isSolaris() ? "sun" + : (Platform.isMac() || Platform.isFreeBSD() || Platform.isOpenBSD()) ? "bsd" + : "unix"; + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/bsd/NativeDatagramSocket.java b/net-socket/src/main/java/org/xbib/net/socket/v6/bsd/NativeDatagramSocket.java new file mode 100644 index 0000000..e49f8d9 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/bsd/NativeDatagramSocket.java @@ -0,0 +1,115 @@ +package org.xbib.net.socket.v6.bsd; + +import static org.xbib.net.socket.v6.Constants.IPPROTO_IPV6; +import static org.xbib.net.socket.v6.Constants.IPV6_DONTFRAG; +import static org.xbib.net.socket.v6.Constants.IPV6_TCLASS; +import static org.xbib.net.socket.v6.Constants.AF_INET6; +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; +import org.xbib.net.socket.v6.datagram.DatagramPacket; +import org.xbib.net.socket.v6.datagram.DatagramSocket; + +public class NativeDatagramSocket implements DatagramSocket, AutoCloseable { + + static { + Native.register((String) null); + } + + private final int socket; + + private volatile boolean closed; + + public NativeDatagramSocket(int type, int protocol, int port) { + this.socket = socket(AF_INET6, type, protocol); + if (socket < 0) { + throw new IllegalStateException("socket < 0"); + } + SocketStructure socketStructure = new SocketStructure(port); + bind(socket, socketStructure, socketStructure.size()); + closed = false; + } + + public native int bind(int socket, SocketStructure address, int address_len) throws LastErrorException; + + public native int socket(int family, int type, int protocol) throws LastErrorException; + + public native int setsockopt(int socket, int level, int option_name, Pointer value, int option_len); + + public native int sendto(int socket, Buffer buffer, int buflen, int flags, SocketStructure address, int dest_addr_len) throws LastErrorException; + + public native int recvfrom(int socket, Buffer buffer, int buflen, int flags, SocketStructure address, int[] in_addr_len) throws LastErrorException; + + public native int close(int socket) throws LastErrorException; + + public native String strerror(int errnum); + + @Override + public int setTrafficClass(int trafficClass) throws IOException { + IntByReference ptr = new IntByReference(trafficClass); + try { + return setsockopt(socket, IPPROTO_IPV6, IPV6_TCLASS, ptr.getPointer(), Native.POINTER_SIZE); + } catch (LastErrorException e) { + throw new IOException("setsockopt: " + strerror(e.getErrorCode())); + } + } + + @Override + public int setFragmentation(boolean frag) throws IOException { + return allowFragmentation(IPPROTO_IPV6, IPV6_DONTFRAG, frag); + } + + private int allowFragmentation(int level, int optionName, boolean frag) throws IOException { + if (closed) { + return -1; + } + IntByReference ptr = new IntByReference(frag ? 0 : 1); + try { + return setsockopt(socket, level, optionName, ptr.getPointer(), Native.POINTER_SIZE); + } catch (LastErrorException e) { + throw new IOException("setsockopt: " + strerror(e.getErrorCode())); + } + } + + @Override + public int receive(DatagramPacket datagramPacket) { + if (closed) { + return -1; + } + try { + SocketStructure socketStructure = new SocketStructure(); + int[] szRef = new int[]{socketStructure.size()}; + ByteBuffer buf = datagramPacket.getContent(); + int n = recvfrom(socket, buf, buf.capacity(), 0, socketStructure, szRef); + datagramPacket.setLength(n); + datagramPacket.setAddressable(socketStructure); + return n; + } catch (LastErrorException e) { + if (e.getMessage().contains("[9]")) { + // bad file descriptor + return -1; + } + throw e; + } + } + + @Override + public int send(DatagramPacket datagramPacket) { + if (closed) { + return -1; + } + ByteBuffer buf = datagramPacket.getContent(); + SocketStructure socketStructure = new SocketStructure(datagramPacket.getAddress(), datagramPacket.getPort()); + return sendto(socket, buf, buf.remaining(), 0, socketStructure, socketStructure.size()); + } + + @Override + public void close() { + closed = true; + close(socket); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/bsd/SocketStructure.java b/net-socket/src/main/java/org/xbib/net/socket/v6/bsd/SocketStructure.java new file mode 100644 index 0000000..0e526ca --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/bsd/SocketStructure.java @@ -0,0 +1,90 @@ +package org.xbib.net.socket.v6.bsd; + +import static org.xbib.net.socket.v6.Constants.AF_INET6; +import com.sun.jna.Structure; +import org.xbib.net.socket.v6.Addressable; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; + +public class SocketStructure extends Structure implements Addressable { + + public byte sin6_len; + + public byte sin6_family; + + public byte[] sin6_port; + + public byte[] sin6_flowinfo; + + public byte[] sin6_addr; + + public byte[] sin6_scope_id; + + public SocketStructure() { + this(0); + } + + public SocketStructure(int port) { + this(null, port); + } + + public SocketStructure(Inet6Address address, int port) { + this(AF_INET6, address, port); + } + + public SocketStructure(int family, Inet6Address address, int port) { + sin6_family = (byte) (0xff & family); + sin6_scope_id = new byte[4]; + sin6_len = (byte) (0xff & 16); + sin6_flowinfo = new byte[4]; + setAddress(address); + setPort(port); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("sin6_len", "sin6_family", "sin6_port", "sin6_flowinfo", "sin6_addr", "sin6_scope_id"); + } + + public Inet6Address getAddress() { + try { + return (Inet6Address) InetAddress.getByAddress(sin6_addr); + } catch (UnknownHostException ex) { + return null; + } + } + + public void setAddress(Inet6Address address) { + if (address != null) { + byte[] addr = address.getAddress(); + assertLen("address", addr, 16); + sin6_addr = addr; + } else { + sin6_addr = new byte[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }; + } + } + + public int getPort() { + int port = 0; + for (int i = 0; i < 2; i++) { + port = ((port << 8) | (sin6_port[i] & 0xff)); + } + return port; + } + + public void setPort(int port) { + byte[] p = new byte[]{(byte) (0xff & (port >> 8)), (byte) (0xff & port)}; + assertLen("port", p, 2); + sin6_port = p; + } + + private void assertLen(String field, byte[] addr, int len) { + if (addr.length != len) { + throw new IllegalArgumentException(field + " length must be " + len + " bytes but was " + addr.length + " bytes."); + } + } + +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/datagram/DatagramPacket.java b/net-socket/src/main/java/org/xbib/net/socket/v6/datagram/DatagramPacket.java new file mode 100644 index 0000000..d0d0707 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/datagram/DatagramPacket.java @@ -0,0 +1,96 @@ +package org.xbib.net.socket.v6.datagram; + +import org.xbib.net.socket.v6.Addressable; + +import java.net.Inet6Address; +import java.nio.ByteBuffer; + +public class DatagramPacket implements Addressable { + + private final ByteBuffer byteBuffer; + + private Addressable addressable; + + public DatagramPacket(int size) { + this(ByteBuffer.allocate(size)); + } + + public DatagramPacket(byte[] data) { + this(ByteBuffer.wrap(data)); + } + + public DatagramPacket(ByteBuffer byteBuffer) { + this.byteBuffer = byteBuffer; + } + + public void setAddressable(Addressable addressable) { + this.addressable = addressable; + } + + public Addressable getAddressable() { + return addressable; + } + + public void setAddress(Inet6Address inetAddress, int port) { + this.addressable = new Addressable() { + @Override + public Inet6Address getAddress() { + return inetAddress; + } + + @Override + public int getPort() { + return port; + } + }; + } + + public Inet6Address getAddress() { + return addressable != null ? addressable.getAddress() : null; + } + + public int getPort() { + return addressable != null ? addressable.getPort() : -1; + } + + public int getLength() { + return byteBuffer.limit(); + } + + public void setLength(int length) { + byteBuffer.limit(length); + } + + public ByteBuffer getContent() { + return byteBuffer.duplicate(); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("Address: "); + if (addressable != null) { + buf.append(addressable.getAddress()); + } + buf.append(" Port: "); + if (addressable != null) { + buf.append(addressable.getPort()); + } + buf.append("\nData: "); + ByteBuffer data = byteBuffer.duplicate(); + buf.append(data.limit()); + buf.append(" Bytes\n"); + int bytesPerRow = 256; + int limit = data.limit(); + int rows = (limit + bytesPerRow) / bytesPerRow; + int index = 0; + for(int i = 0; i < rows && index < limit; i++) { + for(int j = 0; j < bytesPerRow && index < limit; j++) { + buf.append(String.format("%02X", data.get(index++))); + } + buf.append("\n"); + } + buf.append("\n"); + return buf.toString(); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/datagram/DatagramSocket.java b/net-socket/src/main/java/org/xbib/net/socket/v6/datagram/DatagramSocket.java new file mode 100644 index 0000000..312f894 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/datagram/DatagramSocket.java @@ -0,0 +1,18 @@ +package org.xbib.net.socket.v6.datagram; + +import org.xbib.net.socket.NetworkUnreachableException; + +import java.io.Closeable; +import java.io.IOException; +import java.net.UnknownHostException; + +public interface DatagramSocket extends Closeable { + + int setFragmentation(boolean frag) throws IOException; + + int setTrafficClass(int trafficClass) throws IOException; + + int receive(DatagramPacket datagramPacket) throws UnknownHostException; + + int send(DatagramPacket datagramPacket) throws NetworkUnreachableException; +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/icmp/EchoPacket.java b/net-socket/src/main/java/org/xbib/net/socket/v6/icmp/EchoPacket.java new file mode 100644 index 0000000..8d6f21b --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/icmp/EchoPacket.java @@ -0,0 +1,41 @@ +package org.xbib.net.socket.v6.icmp; + +import java.nio.ByteBuffer; + +public class EchoPacket extends Packet { + + public EchoPacket(int size) { + super(size); + } + + public EchoPacket(Packet icmpPacket) { + super(icmpPacket); + } + + public ByteBuffer getContentBuffer() { + ByteBuffer content = byteBuffer.duplicate(); + content.position(8); + return content.slice(); + } + + public byte[] toBytes() { + return getContentBuffer().array(); + } + + public int getIdentifier() { + return getUnsignedShort(4); + } + + public void setIdentifier(int id) { + setUnsignedShort(4, id); + } + + public int getSequenceNumber() { + return getUnsignedShort(6); + } + + public void setSequenceNumber(int sn) { + setUnsignedShort(6, sn); + } + +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/icmp/Packet.java b/net-socket/src/main/java/org/xbib/net/socket/v6/icmp/Packet.java new file mode 100644 index 0000000..7730cf1 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/icmp/Packet.java @@ -0,0 +1,129 @@ +package org.xbib.net.socket.v6.icmp; + +import org.xbib.net.socket.v6.datagram.DatagramPacket; + +import java.net.Inet6Address; +import java.nio.ByteBuffer; + +public class Packet { + + public static final int CHECKSUM_INDEX = 2; + + public enum Type { + DestinationUnreachable(1), + TimeExceeded(3), + EchoRequest(128), + EchoReply(129), + Other(-1); + + private final int code; + + Type(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public static Type toType(byte typeCode) { + int code = (typeCode & 0xff); + for(Type p : Type.values()) { + if (code == p.getCode()) { + return p; + } + } + return Other; + } + } + + protected ByteBuffer byteBuffer; + + public Packet(ByteBuffer ipPayload) { + byteBuffer = ipPayload; + } + + public Packet(Packet packet) { + this(packet.byteBuffer.duplicate()); + } + + public Packet(int size) { + this(ByteBuffer.allocate(size)); + } + + public int getPacketLength() { + return byteBuffer.limit(); + } + + public Type getType() { + return Type.toType(byteBuffer.get(0)); + } + + public void setType(Type t) { + byteBuffer.put(0, ((byte)(t.getCode()))); + } + + public int getCode() { + return 0xff & byteBuffer.get(1); + } + + public void setCode(int code) { + byteBuffer.put(1, ((byte)code)); + } + + public int getChecksum() { + return getUnsignedShort(2); + } + + public int computeChecksum() { + int sum = 0; + int count = byteBuffer.remaining(); + int index = 0; + while(count > 1) { + if (index != CHECKSUM_INDEX) { + sum += getUnsignedShort(index); + } + index += 2; + count -= 2; + } + if (count > 0) { + sum += makeUnsignedShort(byteBuffer.get((byteBuffer.remaining()-1)), (byte)0); + } + int sumLo = sum & 0xffff; + int sumHi = (sum >> 16) & 0xffff; + sum = sumLo + sumHi; + sumLo = sum & 0xffff; + sumHi = (sum >> 16) & 0xffff; + sum = sumLo + sumHi; + return (~sum) & 0xffff; + } + + public void setBytes(int index, byte[] b) { + ByteBuffer payload = byteBuffer; + int oldPos = payload.position(); + try { + payload.position(index); + payload.put(b); + } finally { + payload.position(oldPos); + } + } + + public int makeUnsignedShort(byte b1, byte b0) { + return 0xffff & (((b1 & 0xff) << 8) | ((b0 & 0xff))); + } + + public int getUnsignedShort(int index) { + return byteBuffer.getShort(index) & 0xffff; + } + + public void setUnsignedShort(int index, int us) { + byteBuffer.putShort(index, ((short)(us & 0xffff))); + } + + public DatagramPacket toDatagramPacket(Inet6Address destinationAddress) { + DatagramPacket p = new DatagramPacket(byteBuffer.duplicate()); + p.setAddress(destinationAddress, 0); + return p; + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/ping/Ping.java b/net-socket/src/main/java/org/xbib/net/socket/v6/ping/Ping.java new file mode 100644 index 0000000..f894e8c --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/ping/Ping.java @@ -0,0 +1,135 @@ +package org.xbib.net.socket.v6.ping; + +import org.xbib.net.socket.Metric; +import org.xbib.net.socket.NetworkUnreachableException; +import org.xbib.net.socket.v6.SocketFactory; +import org.xbib.net.socket.v6.datagram.DatagramPacket; +import org.xbib.net.socket.v6.datagram.DatagramSocket; +import org.xbib.net.socket.v6.icmp.Packet; + +import java.io.Closeable; +import java.io.IOException; +import java.net.Inet6Address; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.xbib.net.socket.v6.Constants.IPPROTO_ICMPV6; + +public class Ping implements Runnable, Closeable { + + private static final Logger logger = Logger.getLogger(Ping.class.getName()); + + public static final long PING_COOKIE = StandardCharsets.US_ASCII.encode("org.xbib").getLong(0); + + private final DatagramSocket datagram; + + private final List listeners; + + private volatile boolean closed; + + private Thread thread; + + private PingMetric metric; + + public Ping(int id) throws Exception { + this(SocketFactory.createDatagramSocket(IPPROTO_ICMPV6, id)); + } + + public Ping(DatagramSocket pingSocket) { + datagram = pingSocket; + this.listeners = new ArrayList<>(); + this.closed = false; + } + + public Metric getMetric() { + return metric; + } + + public boolean isClosed() { + return closed; + } + + public void start() { + thread = new Thread(this, "PingThread:PingListener"); + thread.setDaemon(true); + thread.start(); + } + + public void stop() { + if (thread != null) { + thread.interrupt(); + } + thread = null; + } + + @Override + public void close() throws IOException { + if (datagram != null) { + closed = true; + datagram.close(); + } + } + + public List getListeners() { + return listeners; + } + + public void addPingReplyListener(PingResponseListener listener) { + listeners.add(listener); + } + + public void execute(int id, Inet6Address addr) throws InterruptedException, NetworkUnreachableException { + Thread t = new Thread(this); + t.start(); + execute(id, addr, 1, 10, 1000); + } + + public void execute(int id, + Inet6Address inet6Address, + int sequenceNumber, + int count, + long interval) throws InterruptedException, NetworkUnreachableException { + if (inet6Address == null) { + return; + } + metric = new PingMetric(count, interval); + addPingReplyListener(metric); + for (int i = sequenceNumber; i < sequenceNumber + count; i++) { + PingRequest request = new PingRequest(id, i); + int rc = request.send(datagram, inet6Address); + Thread.sleep(interval); + } + } + + @Override + public void run() { + try { + DatagramPacket datagram = new DatagramPacket(65535); + while (!isClosed()) { + int rc = this.datagram.receive(datagram); + long received = System.nanoTime(); + Packet packet = new Packet(datagram.getContent()); + if ( packet.getType() == Packet.Type.EchoReply) { + PingResponse pingResponse = new PingResponse(packet, received); + if (pingResponse.isValid()) { + logger.log(Level.INFO, String.format("%d bytes from [%s]: tid=%d icmp_seq=%d time=%.3f ms%n", + pingResponse.getPacketLength(), + datagram.getAddress().getHostAddress(), + pingResponse.getIdentifier(), + pingResponse.getSequenceNumber(), + pingResponse.elapsedTime(TimeUnit.MILLISECONDS))); + for (PingResponseListener listener : getListeners()) { + listener.onPingResponse(datagram.getAddress(), pingResponse); + } + } + } + } + } catch (Throwable e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingMetric.java b/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingMetric.java new file mode 100644 index 0000000..f35f954 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingMetric.java @@ -0,0 +1,35 @@ +package org.xbib.net.socket.v6.ping; + +import org.xbib.net.socket.Metric; + +import java.net.Inet6Address; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class PingMetric extends Metric implements PingResponseListener { + + private final CountDownLatch countDownLatch; + + private final int count; + + private final long interval; + + public PingMetric(int count, long interval) { + this.countDownLatch = new CountDownLatch(count); + this.count = count; + this.interval = interval; + } + + @Override + public void onPingResponse(Inet6Address address, PingResponse reply) { + try { + update(reply.getElapsedTimeNanos()); + } finally { + countDownLatch.countDown(); + } + } + + public void await() throws InterruptedException { + countDownLatch.await(interval * count + 1000, TimeUnit.MILLISECONDS); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingRequest.java b/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingRequest.java new file mode 100644 index 0000000..6601b40 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingRequest.java @@ -0,0 +1,43 @@ +package org.xbib.net.socket.v6.ping; + +import org.xbib.net.socket.NetworkUnreachableException; +import org.xbib.net.socket.v6.datagram.DatagramPacket; +import org.xbib.net.socket.v6.datagram.DatagramSocket; +import org.xbib.net.socket.v6.icmp.EchoPacket; +import org.xbib.net.socket.v6.icmp.Packet; + +import java.net.Inet6Address; +import java.nio.ByteBuffer; + +class PingRequest extends EchoPacket { + + public PingRequest() { + super(64); + setType(Packet.Type.EchoRequest); + setCode(0); + } + + public PingRequest(int id, int seqNum) { + super(64); + setType(Type.EchoRequest); + setCode(0); + setIdentifier(id); + setSequenceNumber(seqNum); + ByteBuffer buf = getContentBuffer(); + for(int b = 0; b < 56; b++) { + buf.put((byte)b); + } + } + + @Override + public DatagramPacket toDatagramPacket(Inet6Address destinationAddress) { + ByteBuffer contentBuffer = getContentBuffer(); + contentBuffer.putLong(Ping.PING_COOKIE); + contentBuffer.putLong(System.nanoTime()); + return super.toDatagramPacket(destinationAddress); + } + + public int send(DatagramSocket socket, Inet6Address addr) throws NetworkUnreachableException { + return socket.send(toDatagramPacket(addr)); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingResponse.java b/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingResponse.java new file mode 100644 index 0000000..23e24e2 --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingResponse.java @@ -0,0 +1,39 @@ +package org.xbib.net.socket.v6.ping; + +import org.xbib.net.socket.v6.icmp.EchoPacket; +import org.xbib.net.socket.v6.icmp.Packet; + +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +public class PingResponse extends EchoPacket { + + private final long receivedTimeNanos; + + public PingResponse(Packet icmpPacket, long receivedTimeNanos) { + super(icmpPacket); + this.receivedTimeNanos = receivedTimeNanos; + } + + public boolean isValid() { + ByteBuffer content = getContentBuffer(); + return content.limit() >= 16 && Ping.PING_COOKIE == content.getLong(0); + } + + public long getSentTimeNanos() { + return getContentBuffer().getLong(8); + } + + public long getReceivedTimeNanos() { + return receivedTimeNanos; + } + + public double elapsedTime(TimeUnit unit) { + double nanosPerUnit = TimeUnit.NANOSECONDS.convert(1, unit); + return getElapsedTimeNanos() / nanosPerUnit; + } + + public long getElapsedTimeNanos() { + return getReceivedTimeNanos() - getSentTimeNanos(); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingResponseListener.java b/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingResponseListener.java new file mode 100644 index 0000000..c33f74f --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/ping/PingResponseListener.java @@ -0,0 +1,8 @@ +package org.xbib.net.socket.v6.ping; + +import java.net.Inet6Address; + +public interface PingResponseListener { + + void onPingResponse(Inet6Address inetAddress, PingResponse reply); +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/unix/NativeDatagramSocket.java b/net-socket/src/main/java/org/xbib/net/socket/v6/unix/NativeDatagramSocket.java new file mode 100644 index 0000000..ad20d7b --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/unix/NativeDatagramSocket.java @@ -0,0 +1,132 @@ +package org.xbib.net.socket.v6.unix; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; +import org.xbib.net.socket.NetworkUnreachableException; +import org.xbib.net.socket.v6.datagram.DatagramPacket; +import org.xbib.net.socket.v6.datagram.DatagramSocket; + +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.xbib.net.socket.v6.Constants.AF_INET6; +import static org.xbib.net.socket.v6.Constants.IPPROTO_IPV6; +import static org.xbib.net.socket.v6.Constants.IPV6_DONTFRAG; +import static org.xbib.net.socket.v6.Constants.IPV6_TCLASS; + +public class NativeDatagramSocket implements DatagramSocket, AutoCloseable { + + private static final Logger logger = Logger.getLogger(NativeDatagramSocket.class.getName()); + + static { + Native.register((String) null); + } + + + private final int socket; + + private volatile boolean closed; + + public NativeDatagramSocket(int type, int protocol, int port) { + try { + this.socket = socket(AF_INET6, type, protocol); + if (socket < 0) { + throw new IllegalStateException("socket < 0"); + } + SocketStructure socketStructure = new SocketStructure(port); + bind(socket, socketStructure, socketStructure.size()); + closed = false; + } catch (LastErrorException e) { + logger.log(Level.SEVERE, e.getMessage() + ": check if sysctl -w net.ipv4.ping_group_range=\"0 65535\" and check for selinux security:\n" + + "allow unconfined_t node_t:icmp_socket node_bind;\n" + + "allow unconfined_t port_t:icmp_socket name_bind;\n"); + throw e; + } + } + + public native int bind(int socket, SocketStructure address, int address_len) throws LastErrorException; + + public native int socket(int domain, int type, int protocol) throws LastErrorException; + + public native int setsockopt(int socket, int level, int option_name, Pointer value, int option_len); + + public native int sendto(int socket, Buffer buffer, int buflen, int flags, SocketStructure dest_addr, int dest_addr_len) throws LastErrorException; + + public native int recvfrom(int socket, Buffer buffer, int buflen, int flags, SocketStructure in_addr, int[] in_addr_len) throws LastErrorException; + + public native int close(int socket) throws LastErrorException; + + public native String strerror(int errnum); + + @Override + public int setTrafficClass(int trafficClass) throws LastErrorException { + if (closed) { + return -1; + } + IntByReference tc_ptr = new IntByReference(trafficClass); + try { + return setsockopt(socket, IPPROTO_IPV6, IPV6_TCLASS, tc_ptr.getPointer(), Native.POINTER_SIZE); + } catch (LastErrorException e) { + throw new RuntimeException("setsockopt: " + strerror(e.getErrorCode())); + } + } + + @Override + public int setFragmentation(boolean frag) throws IOException { + return allowFragmentation(IPPROTO_IPV6, IPV6_DONTFRAG, frag); + } + + private int allowFragmentation(int level, int option_name, boolean frag) throws IOException { + if (closed) { + return -1; + } + IntByReference dontfragment = new IntByReference(frag ? 0 : 1); + try { + return setsockopt(socket, level, option_name, dontfragment.getPointer(), Native.POINTER_SIZE); + } catch (LastErrorException e) { + throw new IOException("setsockopt: " + strerror(e.getErrorCode())); + } + } + + @Override + public int receive(DatagramPacket datagramPacket) { + if (closed) { + return -1; + } + SocketStructure in_addr = new SocketStructure(); + int[] szRef = new int[]{in_addr.size()}; + ByteBuffer buf = datagramPacket.getContent(); + int n = recvfrom(socket, buf, buf.capacity(), 0, in_addr, szRef); + datagramPacket.setLength(n); + datagramPacket.setAddressable(in_addr); + return n; + } + + @Override + public int send(DatagramPacket datagramPacket) throws NetworkUnreachableException { + if (closed) { + return -1; + } + try { + ByteBuffer buf = datagramPacket.getContent(); + SocketStructure destAddr = new SocketStructure(datagramPacket.getAddress(), datagramPacket.getPort()); + return sendto(socket, buf, buf.remaining(), 0, destAddr, destAddr.size()); + } catch (LastErrorException e) { + if (e.getMessage().contains("[101]")) { + throw new NetworkUnreachableException(); + } + throw e; + } + } + + @Override + public void close() { + closed = true; + close(socket); + } +} diff --git a/net-socket/src/main/java/org/xbib/net/socket/v6/unix/SocketStructure.java b/net-socket/src/main/java/org/xbib/net/socket/v6/unix/SocketStructure.java new file mode 100644 index 0000000..ed75b2f --- /dev/null +++ b/net-socket/src/main/java/org/xbib/net/socket/v6/unix/SocketStructure.java @@ -0,0 +1,92 @@ +package org.xbib.net.socket.v6.unix; + +import com.sun.jna.Structure; +import org.xbib.net.socket.v6.Addressable; + +import java.net.Inet6Address; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; + +import static org.xbib.net.socket.v6.Constants.AF_INET6; + +public class SocketStructure extends Structure implements Addressable { + + public short sin6_family; + + public byte[] sin6_port; + + public int sin6_flowinfo; + + public byte[] sin6_addr; + + public int sin6_scope_id; + + public SocketStructure() { + this(null, 0); + } + + public SocketStructure(int port) { + this(null, port); + } + + public SocketStructure(Inet6Address address, int port) { + this(AF_INET6, address, port); + } + + public SocketStructure(int family, Inet6Address address, int port) { + this.sin6_family = (short) (0xffff & family); + this.sin6_flowinfo = 0; + this.sin6_scope_id = 0; + setAddress(address); + setPort(port); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("sin6_family", "sin6_port", "sin6_flowinfo", "sin6_addr", "sin6_scope_id"); + } + + @Override + public Inet6Address getAddress() { + try { + return (Inet6Address) Inet6Address.getByAddress(sin6_addr); + } catch (UnknownHostException ex) { + return null; + } + } + + public void setAddress(Inet6Address address) { + if (address != null) { + byte[] addr = address.getAddress(); + assertLen("address", addr, 16); + this.sin6_addr = addr; + } else { + this.sin6_addr = new byte[] { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }; + } + } + + @Override + public int getPort() { + int port = 0; + for (int i = 0; i < 2; i++) { + port = ((port << 8) | (sin6_port[i] & 0xff)); + } + return port; + } + + public void setPort(int port) { + if (port >= 0) { + byte[] p = new byte[]{(byte) (0xff & (port >> 8)), (byte) (0xff & port)}; + assertLen("port", p, 2); + this.sin6_port = p; + } + } + + + private void assertLen(String field, byte[] addr, int len) { + if (addr.length != len) { + throw new IllegalArgumentException(field + " length must be " + len + " bytes but was " + addr.length + " bytes."); + } + } +} diff --git a/net-socket/src/test/java/org/xbib/net/socket/notify/SystemdTest.java b/net-socket/src/test/java/org/xbib/net/socket/notify/SystemdTest.java new file mode 100644 index 0000000..9b3dccd --- /dev/null +++ b/net-socket/src/test/java/org/xbib/net/socket/notify/SystemdTest.java @@ -0,0 +1,13 @@ +package org.xbib.net.socket.notify; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class SystemdTest { + + @Test + public void testSsystemd() throws IOException { + SystemdNotify.sendNotify(); + } +} diff --git a/net-socket/src/test/java/org/xbib/net/socket/v4/PingTest.java b/net-socket/src/test/java/org/xbib/net/socket/v4/PingTest.java new file mode 100644 index 0000000..fe44dea --- /dev/null +++ b/net-socket/src/test/java/org/xbib/net/socket/v4/PingTest.java @@ -0,0 +1,27 @@ +package org.xbib.net.socket.v4; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.net.socket.v4.ping.Ping; +import java.net.Inet4Address; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Disabled("RHEL selinux blocks Java JDK from acessing icmp_socket") +class PingTest { + + private static final Logger logger = Logger.getLogger(PingTest.class.getName()); + + @Test + void ping() throws Exception { + Inet4Address address = (Inet4Address) Inet4Address.getByName("www.google.de"); + if (address != null) { + Ping ping = new Ping(1234); + logger.log(Level.INFO, "address=" + address); + ping.execute(1234, address); + logger.log(Level.INFO, ping.getMetric().getSummary(TimeUnit.MILLISECONDS)); + ping.close(); + } + } +} diff --git a/net-socket/src/test/java/org/xbib/net/socket/v6/PingTest.java b/net-socket/src/test/java/org/xbib/net/socket/v6/PingTest.java new file mode 100644 index 0000000..6778ec2 --- /dev/null +++ b/net-socket/src/test/java/org/xbib/net/socket/v6/PingTest.java @@ -0,0 +1,38 @@ +package org.xbib.net.socket.v6; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.xbib.net.socket.v6.ping.Ping; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Disabled("RHEL selinux blocks Java JDK from acessing icmp_socket") +class PingTest { + + private static final Logger logger = Logger.getLogger(PingTest.class.getName()); + + @Test + void ping() throws Exception { + Inet6Address address = getAddress("www.google.de"); + if (address != null) { + logger.log(Level.INFO, "address=" + address); + Ping ping = new Ping(1234); + ping.execute(1234, address); + logger.log(Level.INFO, ping.getMetric().getSummary(TimeUnit.MILLISECONDS)); + ping.close(); + } + } + + private Inet6Address getAddress(String host) throws UnknownHostException { + for (InetAddress addr : InetAddress.getAllByName(host)) { + if (addr instanceof Inet6Address) { + return (Inet6Address) addr; + } + } + return null; + } +} diff --git a/net/NOTICE.txt b/net/NOTICE.txt new file mode 100644 index 0000000..e792c3b --- /dev/null +++ b/net/NOTICE.txt @@ -0,0 +1,21 @@ +The URI templates subpackages are a revised version of the work of + +https://github.com/fge/uri-template + +(Francis Galiegue , Version 0.9, as of 14 April 2014) + +License: Apache 2.0 + +Ant-style Path Matcher is a work based on AntPathMatcherArrays + +https://github.com/azagniotov/ant-style-path-matcher + +(Alexander Zagniotov, Version as of 14 Feb 2022) + +License: MIT + +The org.xbib.net.buffer "DataBuffer" classes are taken from Spring Framework, Core, branch "main", as of 21-Jun-2022 (v5.3.21) + +https://github.com/spring-projects/spring-framework/tree/main/spring-core/src/main/java/org/springframework/core/io/buffer + +License: Apacche 2.0 diff --git a/net/build.gradle b/net/build.gradle new file mode 100644 index 0000000..ceb7b2f --- /dev/null +++ b/net/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api libs.datastructures.common + testImplementation libs.jackson +} diff --git a/net/src/docs/asciidoc/css/foundation.css b/net/src/docs/asciidoc/css/foundation.css new file mode 100644 index 0000000..27be611 --- /dev/null +++ b/net/src/docs/asciidoc/css/foundation.css @@ -0,0 +1,684 @@ +/*! normalize.css v2.1.2 | MIT License | git.io/normalize */ +/* ========================================================================== HTML5 display definitions ========================================================================== */ +/** Correct `block` display not defined in IE 8/9. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; } + +/** Correct `inline-block` display not defined in IE 8/9. */ +audio, canvas, video { display: inline-block; } + +/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ +audio:not([controls]) { display: none; height: 0; } + +/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ +[hidden], template { display: none; } + +script { display: none !important; } + +/* ========================================================================== Base ========================================================================== */ +/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ +html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } + +/** Remove default margin. */ +body { margin: 0; } + +/* ========================================================================== Links ========================================================================== */ +/** Remove the gray background color from active links in IE 10. */ +a { background: transparent; } + +/** Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { outline: thin dotted; } + +/** Improve readability when focused and also mouse hovered in all browsers. */ +a:active, a:hover { outline: 0; } + +/* ========================================================================== Typography ========================================================================== */ +/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ +h1 { font-size: 2em; margin: 0.67em 0; } + +/** Address styling not present in IE 8/9, Safari 5, and Chrome. */ +abbr[title] { border-bottom: 1px dotted; } + +/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ +b, strong { font-weight: bold; } + +/** Address styling not present in Safari 5 and Chrome. */ +dfn { font-style: italic; } + +/** Address differences between Firefox and other browsers. */ +hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; } + +/** Address styling not present in IE 8/9. */ +mark { background: #ff0; color: #000; } + +/** Correct font family set oddly in Safari 5 and Chrome. */ +code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; } + +/** Improve readability of pre-formatted text in all browsers. */ +pre { white-space: pre-wrap; } + +/** Set consistent quote types. */ +q { quotes: "\201C" "\201D" "\2018" "\2019"; } + +/** Address inconsistent and variable font size in all browsers. */ +small { font-size: 80%; } + +/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } + +sup { top: -0.5em; } + +sub { bottom: -0.25em; } + +/* ========================================================================== Embedded content ========================================================================== */ +/** Remove border when inside `a` element in IE 8/9. */ +img { border: 0; } + +/** Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { overflow: hidden; } + +/* ========================================================================== Figures ========================================================================== */ +/** Address margin not present in IE 8/9 and Safari 5. */ +figure { margin: 0; } + +/* ========================================================================== Forms ========================================================================== */ +/** Define consistent border, margin, and padding. */ +fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } + +/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ +legend { border: 0; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ +button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ } + +/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ +button, input { line-height: normal; } + +/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { text-transform: none; } + +/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ +button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } + +/** Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { cursor: default; } + +/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ +input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ +input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; } + +/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + +/** Remove inner padding and border in Firefox 4+. */ +button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } + +/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ +textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } + +/* ========================================================================== Tables ========================================================================== */ +/** Remove most spacing between table cells. */ +table { border-collapse: collapse; border-spacing: 0; } + +meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; } + +meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; } + +meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; } + +*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } + +html, body { font-size: 100%; } + +body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; } + +a:hover { cursor: pointer; } + +img, object, embed { max-width: 100%; height: auto; } + +object, embed { height: 100%; } + +img { -ms-interpolation-mode: bicubic; } + +#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; } + +.left { float: left !important; } + +.right { float: right !important; } + +.text-left { text-align: left !important; } + +.text-right { text-align: right !important; } + +.text-center { text-align: center !important; } + +.text-justify { text-align: justify !important; } + +.hide { display: none; } + +.antialiased { -webkit-font-smoothing: antialiased; } + +img { display: inline-block; vertical-align: middle; } + +textarea { height: auto; min-height: 50px; } + +select { width: 100%; } + +object, svg { display: inline-block; vertical-align: middle; } + +.center { margin-left: auto; margin-right: auto; } + +.spread { width: 100%; } + +p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; } + +.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; } + +/* Typography resets */ +div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; } + +/* Default Link Styles */ +a { color: #2ba6cb; text-decoration: none; line-height: inherit; } +a:hover, a:focus { color: #2795b6; } +a img { border: none; } + +/* Default paragraph styles */ +p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; } +p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; } + +/* Default header styles */ +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; } +h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; } + +h1 { font-size: 2.125em; } + +h2 { font-size: 1.6875em; } + +h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; } + +h4 { font-size: 1.125em; } + +h5 { font-size: 1.125em; } + +h6 { font-size: 1em; } + +hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; } + +/* Helpful Typography Defaults */ +em, i { font-style: italic; line-height: inherit; } + +strong, b { font-weight: bold; line-height: inherit; } + +small { font-size: 60%; line-height: inherit; } + +code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; } + +/* Lists */ +ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; } + +ul, ol { margin-left: 1.5em; } +ul.no-bullet, ol.no-bullet { margin-left: 1.5em; } + +/* Unordered Lists */ +ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ } +ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; } +ul.square { list-style-type: square; } +ul.circle { list-style-type: circle; } +ul.disc { list-style-type: disc; } +ul.no-bullet { list-style: none; } + +/* Ordered Lists */ +ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; } + +/* Definition Lists */ +dl dt { margin-bottom: 0.3125em; font-weight: bold; } +dl dd { margin-bottom: 1.25em; } + +/* Abbreviations */ +abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; } + +abbr { text-transform: none; } + +/* Blockquotes */ +blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; } +blockquote cite { display: block; font-size: 0.8125em; color: #555555; } +blockquote cite:before { content: "\2014 \0020"; } +blockquote cite a, blockquote cite a:visited { color: #555555; } + +blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; } + +/* Microformats */ +.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; } +.vcard li { margin: 0; display: block; } +.vcard .fn { font-weight: bold; font-size: 0.9375em; } + +.vevent .summary { font-weight: bold; } +.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; } + +@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + h1 { font-size: 2.75em; } + h2 { font-size: 2.3125em; } + h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; } + h4 { font-size: 1.4375em; } } +/* Tables */ +table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; } +table thead, table tfoot { background: whitesmoke; font-weight: bold; } +table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; } +table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; } +table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; } +table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; } + +body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; } + +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + +.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; } +.clearfix:after, .float-group:after { clear: both; } + +*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; } +*:not(pre) > code.nobreak { word-wrap: normal; } +*:not(pre) > code.nowrap { white-space: nowrap; } + +pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; } + +em em { font-style: normal; } + +strong strong { font-weight: normal; } + +.keyseq { color: #555555; } + +kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; } + +.keyseq kbd:first-child { margin-left: 0; } + +.keyseq kbd:last-child { margin-right: 0; } + +.menuseq, .menu { color: #090909; } + +b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } + +b.button:before { content: "["; padding: 0 3px 0 2px; } + +b.button:after { content: "]"; padding: 0 2px 0 3px; } + +#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; } +#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; } +#header:after, #content:after, #footnotes:after, #footer:after { clear: both; } + +#content { margin-top: 1.25em; } + +#content:before { content: none; } + +#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; } +#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; } +#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; } +#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; } +#header .details span:first-child { margin-left: -0.125em; } +#header .details span.email a { color: #6f6f6f; } +#header .details br { display: none; } +#header .details br + span:before { content: "\00a0\2013\00a0"; } +#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; } +#header .details br + span#revremark:before { content: "\00a0|\00a0"; } +#header #revnumber { text-transform: capitalize; } +#header #revnumber:after { content: "\00a0"; } + +#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; } + +#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; } +#toc > ul { margin-left: 0.125em; } +#toc ul.sectlevel0 > li > a { font-style: italic; } +#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; } +#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; } +#toc li { line-height: 1.3334; margin-top: 0.3334em; } +#toc a { text-decoration: none; } +#toc a:active { text-decoration: underline; } + +#toctitle { color: #6f6f6f; font-size: 1.2em; } + +@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; } + body.toc2 { padding-left: 15em; padding-right: 0; } + #toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; } + #toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; } + #toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; } + #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; } + #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } + body.toc2.toc-right { padding-left: 0; padding-right: 15em; } + body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } } +@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } + #toc.toc2 { width: 20em; } + #toc.toc2 #toctitle { font-size: 1.375em; } + #toc.toc2 > ul { font-size: 0.95em; } + #toc.toc2 ul ul { padding-left: 1.25em; } + body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } +#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +#content #toc > :first-child { margin-top: 0; } +#content #toc > :last-child { margin-bottom: 0; } + +#footer { max-width: 100%; background-color: #222222; padding: 1.25em; } + +#footer-text { color: #dddddd; line-height: 1.44; } + +.sect1 { padding-bottom: 0.625em; } + +@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } } +.sect1 + .sect1 { border-top: 1px solid #dddddd; } + +#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; } +#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; } +#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; } +#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; } +#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; } + +.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; } + +.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; } + +table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; } + +.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; } + +table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; } + +.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; } +.admonitionblock > table td.icon { text-align: center; width: 80px; } +.admonitionblock > table td.icon img { max-width: initial; } +.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; } +.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; } +.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; } + +.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; } +.exampleblock > .content > :first-child { margin-top: 0; } +.exampleblock > .content > :last-child { margin-bottom: 0; } + +.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +.sidebarblock > :first-child { margin-top: 0; } +.sidebarblock > :last-child { margin-bottom: 0; } +.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; } + +.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; } + +.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; } +.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; } + +.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; } +.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; } +@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } } +@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } } + +.literalblock.output pre { color: #eeeeee; background-color: black; } + +.listingblock pre.highlightjs { padding: 0; } +.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; } + +.listingblock > .content { position: relative; } + +.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; } + +.listingblock:hover code[data-lang]:before { display: block; } + +.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; } + +.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; } + +table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; } + +table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; } + +table.pyhltable td.code { padding-left: .75em; padding-right: 0; } + +pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; } + +pre.pygments .lineno { display: inline-block; margin-right: .25em; } + +table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; } + +.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; } +.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; } +.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; } +.quoteblock blockquote { margin: 0; padding: 0; border: 0; } +.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } +.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; } +.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; } +.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; } +.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; } +.quoteblock .quoteblock blockquote:before { display: none; } + +.verseblock { margin: 0 1em 1.25em 1em; } +.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; } +.verseblock pre strong { font-weight: 400; } +.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; } + +.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; } +.quoteblock .attribution br, .verseblock .attribution br { display: none; } +.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; } + +.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; } +.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; } +.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; } + +table.tableblock { max-width: 100%; border-collapse: separate; } +table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; } + +table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; } + +table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; } + +table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; } + +table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; } + +table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; } + +table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; } + +table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; } + +table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; } + +table.frame-all { border-width: 1px; } + +table.frame-sides { border-width: 0 1px; } + +table.frame-topbot { border-width: 1px 0; } + +th.halign-left, td.halign-left { text-align: left; } + +th.halign-right, td.halign-right { text-align: right; } + +th.halign-center, td.halign-center { text-align: center; } + +th.valign-top, td.valign-top { vertical-align: top; } + +th.valign-bottom, td.valign-bottom { vertical-align: bottom; } + +th.valign-middle, td.valign-middle { vertical-align: middle; } + +table thead th, table tfoot th { font-weight: bold; } + +tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; } + +tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; } + +p.tableblock > code:only-child { background: none; padding: 0; } + +p.tableblock { font-size: 1em; } + +td > div.verse { white-space: pre; } + +ol { margin-left: 1.75em; } + +ul li ol { margin-left: 1.5em; } + +dl dd { margin-left: 1.125em; } + +dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; } + +ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; } + +ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } + +ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; } + +ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; } + +ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; } + +ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; } +ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; } +ul.inline > li > * { display: block; } + +.unstyled dl dt { font-weight: normal; font-style: normal; } + +ol.arabic { list-style-type: decimal; } + +ol.decimal { list-style-type: decimal-leading-zero; } + +ol.loweralpha { list-style-type: lower-alpha; } + +ol.upperalpha { list-style-type: upper-alpha; } + +ol.lowerroman { list-style-type: lower-roman; } + +ol.upperroman { list-style-type: upper-roman; } + +ol.lowergreek { list-style-type: lower-greek; } + +.hdlist > table, .colist > table { border: 0; background: none; } +.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } + +td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; } + +td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; } + +.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } + +.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; } +.colist > table tr > td:first-of-type img { max-width: initial; } +.colist > table tr > td:last-of-type { padding: 0.25em 0; } + +.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; } + +.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; } +.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; } +.imageblock > .title { margin-bottom: 0; } +.imageblock.thumb, .imageblock.th { border-width: 6px; } +.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; } + +.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; } +.image.left { margin-right: 0.625em; } +.image.right { margin-left: 0.625em; } + +a.image { text-decoration: none; display: inline-block; } +a.image object { pointer-events: none; } + +sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; } +sup.footnote a, sup.footnoteref a { text-decoration: none; } +sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; } + +#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; } +#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; } +#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; } +#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; } +#footnotes .footnote:last-of-type { margin-bottom: 0; } +#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; } + +.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; } +.gist .file-data > table td.line-data { width: 99%; } + +div.unbreakable { page-break-inside: avoid; } + +.big { font-size: larger; } + +.small { font-size: smaller; } + +.underline { text-decoration: underline; } + +.overline { text-decoration: overline; } + +.line-through { text-decoration: line-through; } + +.aqua { color: #00bfbf; } + +.aqua-background { background-color: #00fafa; } + +.black { color: black; } + +.black-background { background-color: black; } + +.blue { color: #0000bf; } + +.blue-background { background-color: #0000fa; } + +.fuchsia { color: #bf00bf; } + +.fuchsia-background { background-color: #fa00fa; } + +.gray { color: #606060; } + +.gray-background { background-color: #7d7d7d; } + +.green { color: #006000; } + +.green-background { background-color: #007d00; } + +.lime { color: #00bf00; } + +.lime-background { background-color: #00fa00; } + +.maroon { color: #600000; } + +.maroon-background { background-color: #7d0000; } + +.navy { color: #000060; } + +.navy-background { background-color: #00007d; } + +.olive { color: #606000; } + +.olive-background { background-color: #7d7d00; } + +.purple { color: #600060; } + +.purple-background { background-color: #7d007d; } + +.red { color: #bf0000; } + +.red-background { background-color: #fa0000; } + +.silver { color: #909090; } + +.silver-background { background-color: #bcbcbc; } + +.teal { color: #006060; } + +.teal-background { background-color: #007d7d; } + +.white { color: #bfbfbf; } + +.white-background { background-color: #fafafa; } + +.yellow { color: #bfbf00; } + +.yellow-background { background-color: #fafa00; } + +span.icon > .fa { cursor: default; } + +.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; } +.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; } +.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; } +.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; } +.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; } +.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; } + +.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; } +.conum[data-value] * { color: #fff !important; } +.conum[data-value] + b { display: none; } +.conum[data-value]:after { content: attr(data-value); } +pre .conum[data-value] { position: relative; top: -0.125em; } + +b.conum * { color: inherit !important; } + +.conum:not([data-value]):empty { display: none; } + +.literalblock pre, .listingblock pre { background: #eeeeee; } diff --git a/net/src/docs/asciidoc/index.adoc b/net/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..1f11ab8 --- /dev/null +++ b/net/src/docs/asciidoc/index.adoc @@ -0,0 +1,11 @@ += Net classes for Java +Jörg Prante +Version 1.0 +:sectnums: +:toc: preamble +:toclevels: 4 +:!toc-title: Content +:experimental: +:description: Net classes for URL +:keywords: Java, Net, URL, URI, IRI +:icons: font diff --git a/net/src/docs/asciidoclet/overview.adoc b/net/src/docs/asciidoclet/overview.adoc new file mode 100644 index 0000000..8398dd7 --- /dev/null +++ b/net/src/docs/asciidoclet/overview.adoc @@ -0,0 +1,4 @@ += Net classes for Java +Jörg Prante +Version 1.0 + diff --git a/net/src/main/java/module-info.java b/net/src/main/java/module-info.java new file mode 100644 index 0000000..1562019 --- /dev/null +++ b/net/src/main/java/module-info.java @@ -0,0 +1,21 @@ +import org.xbib.net.buffer.DataBufferFactory; +import org.xbib.net.buffer.DefaultDataBufferFactory; + +module org.xbib.net { + exports org.xbib.net; + exports org.xbib.net.buffer; + exports org.xbib.net.scheme; + exports org.xbib.net.template; + exports org.xbib.net.template.expression; + exports org.xbib.net.template.parse; + exports org.xbib.net.template.render; + exports org.xbib.net.template.vars; + exports org.xbib.net.template.vars.specs; + exports org.xbib.net.template.vars.values; + exports org.xbib.net.util; + requires transitive org.xbib.datastructures.common; + requires java.management; + requires java.logging; + uses DataBufferFactory; + provides DataBufferFactory with DefaultDataBufferFactory; +} diff --git a/net/src/main/java/org/xbib/net/Address.java b/net/src/main/java/org/xbib/net/Address.java new file mode 100644 index 0000000..1c58aeb --- /dev/null +++ b/net/src/main/java/org/xbib/net/Address.java @@ -0,0 +1,22 @@ +package org.xbib.net; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +public interface Address { + + String getHost(); + + Integer getPort(); + + InetAddress getInetAddress() throws IOException; + + InetSocketAddress getInetSocketAddress() throws IOException; + + URL base(); + + boolean isSecure(); + + SocketConfig getSocketConfig(); +} diff --git a/net/src/main/java/org/xbib/net/Attributes.java b/net/src/main/java/org/xbib/net/Attributes.java new file mode 100644 index 0000000..f3dd8e0 --- /dev/null +++ b/net/src/main/java/org/xbib/net/Attributes.java @@ -0,0 +1,10 @@ +package org.xbib.net; + +import java.util.Map; + +public interface Attributes extends Map { + + T get(Class cl, String key); + + T get(Class cl, String key, T defaultValue); +} diff --git a/net/src/main/java/org/xbib/net/Authenticator.java b/net/src/main/java/org/xbib/net/Authenticator.java new file mode 100644 index 0000000..3b1fb1b --- /dev/null +++ b/net/src/main/java/org/xbib/net/Authenticator.java @@ -0,0 +1,41 @@ +package org.xbib.net; + +public abstract class Authenticator { + + public Authenticator() { + } + + /** + * Authenticate. + * @return true if user was successfully authenticated with specified credentials, false otherwise + * @throws RuntimeException in case of unexpected error such as connection failure + */ + public abstract boolean authenticate(Context context); + + public static final class Context { + + private final String username; + + private final String password; + + private final Request request; + + public Context(String username, String password, Request request) { + this.request = request; + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public Request getRequest() { + return request; + } + } +} diff --git a/net/src/main/java/org/xbib/net/Context.java b/net/src/main/java/org/xbib/net/Context.java new file mode 100644 index 0000000..1ed84a9 --- /dev/null +++ b/net/src/main/java/org/xbib/net/Context.java @@ -0,0 +1,8 @@ +package org.xbib.net; + +public interface Context { + + Req request(); + + Resp response(); +} diff --git a/net/src/main/java/org/xbib/net/GroupsProvider.java b/net/src/main/java/org/xbib/net/GroupsProvider.java new file mode 100644 index 0000000..07380f6 --- /dev/null +++ b/net/src/main/java/org/xbib/net/GroupsProvider.java @@ -0,0 +1,47 @@ +package org.xbib.net; + +import java.util.Collection; + +public abstract class GroupsProvider { + + public GroupsProvider() { + } + + /** + * @return list of groups associated with specified user, or null if such user doesn't exist + * @throws RuntimeException in case of unexpected error such as connection failure + */ + public Collection getGroups(String username) { + return null; + } + + /** + * Override this method in order to load user group information. + * + * @return list of groups associated with specified user, or null if such user doesn't exist + * @throws RuntimeException in case of unexpected error such as connection failure + */ + public Collection getGroups(Context context) { + return getGroups(context.getUsername()); + } + + public static final class Context { + + private final String username; + + private final Request request; + + public Context(String username, Request request) { + this.username = username; + this.request = request; + } + + public String getUsername() { + return username; + } + + public Request getRequest() { + return request; + } + } +} \ No newline at end of file diff --git a/net/src/main/java/org/xbib/net/Handler.java b/net/src/main/java/org/xbib/net/Handler.java new file mode 100644 index 0000000..cfef94f --- /dev/null +++ b/net/src/main/java/org/xbib/net/Handler.java @@ -0,0 +1,10 @@ +package org.xbib.net; + +import java.io.IOException; + +@SuppressWarnings("rawtypes") +@FunctionalInterface +public interface Handler { + + void handle(C context) throws IOException; +} diff --git a/net/src/main/java/org/xbib/net/HandlerException.java b/net/src/main/java/org/xbib/net/HandlerException.java new file mode 100644 index 0000000..8483843 --- /dev/null +++ b/net/src/main/java/org/xbib/net/HandlerException.java @@ -0,0 +1,21 @@ +package org.xbib.net; + +@SuppressWarnings("serial") +public class HandlerException extends RuntimeException { + + public HandlerException() { + super(); + } + + public HandlerException(String message) { + super(message); + } + + public HandlerException(Exception e) { + super(e); + } + + public HandlerException(String message, Exception e) { + super(message, e); + } +} diff --git a/net/src/main/java/org/xbib/net/Headers.java b/net/src/main/java/org/xbib/net/Headers.java new file mode 100644 index 0000000..62f8e96 --- /dev/null +++ b/net/src/main/java/org/xbib/net/Headers.java @@ -0,0 +1,16 @@ +package org.xbib.net; + +import java.util.List; +import org.xbib.datastructures.common.Pair; + +/** + * Headers should never be a hash map. Never. + */ +public interface Headers { + + String get(CharSequence name); + + List getAll(CharSequence name); + + List> entries(); +} diff --git a/net/src/main/java/org/xbib/net/NetworkClass.java b/net/src/main/java/org/xbib/net/NetworkClass.java new file mode 100644 index 0000000..ff53586 --- /dev/null +++ b/net/src/main/java/org/xbib/net/NetworkClass.java @@ -0,0 +1,9 @@ +package org.xbib.net; + +/** + * The network classes. + */ +public enum NetworkClass { + + ANY, LOOPBACK, LOCAL, SITE, PUBLIC +} diff --git a/net/src/main/java/org/xbib/net/NetworkProtocolVersion.java b/net/src/main/java/org/xbib/net/NetworkProtocolVersion.java new file mode 100644 index 0000000..d4b9930 --- /dev/null +++ b/net/src/main/java/org/xbib/net/NetworkProtocolVersion.java @@ -0,0 +1,9 @@ +package org.xbib.net; + +/** + * The TCP/IP network protocol versions. + */ +public enum NetworkProtocolVersion { + + IPV4, IPV6, IPV46, NONE +} diff --git a/net/src/main/java/org/xbib/net/NetworkUtils.java b/net/src/main/java/org/xbib/net/NetworkUtils.java new file mode 100644 index 0000000..3b2392d --- /dev/null +++ b/net/src/main/java/org/xbib/net/NetworkUtils.java @@ -0,0 +1,671 @@ +package org.xbib.net; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Helper class for Java networking. + */ +public class NetworkUtils { + + private static final String lf = System.lineSeparator(); + + private static final char[] hexDigit = new char[]{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + private static final String IPV4_SETTING = "java.net.preferIPv4Stack"; + + private static final String IPV6_SETTING = "java.net.preferIPv6Addresses"; + + private static final CountDownLatch latch = new CountDownLatch(1); + + private static final InterfaceWaiter interfaceWaiter = new InterfaceWaiter(); + + private static InetAddress localAddress; + + private NetworkUtils() { + throw new UnsupportedOperationException(); + } + + public static Properties createProperties() { + Properties properties = new Properties(); + try { + InetAddress address = getLocalAddress(); + properties.put("net.localhost", address.getCanonicalHostName()); + String hostname = address.getHostName(); + properties.put("net.hostname", hostname); + InetAddress[] hostnameAddresses = InetAddress.getAllByName(hostname); + int i = 0; + for (InetAddress hostnameAddress : hostnameAddresses) { + properties.put("net.host." + (i++) + "." + hostnameAddress.getCanonicalHostName(), hostnameAddress.getHostAddress()); + } + for (InetAddress inetAddress : getAddresses(getActiveInterfaces(), NetworkUtils::isNonLoopBack, NetworkProtocolVersion.IPV4)) { + properties.put("net." + inetAddress.getCanonicalHostName(), inetAddress.getHostAddress()); + } + } catch (Throwable e) { + Logger.getLogger("network").log(Level.WARNING, e.getMessage(), e); + } + return properties; + } + + public static boolean isPreferIPv4() { + return Boolean.getBoolean(System.getProperty(IPV4_SETTING)); + } + + public static boolean isPreferIPv6() { + return Boolean.getBoolean(System.getProperty(IPV6_SETTING)); + } + + public static InetAddress getIPv4Localhost() throws UnknownHostException { + return getLocalhost(NetworkProtocolVersion.IPV4); + } + + public static InetAddress getIPv6Localhost() throws UnknownHostException { + return getLocalhost(NetworkProtocolVersion.IPV6); + } + + public static InetAddress getLocalhost(NetworkProtocolVersion ipversion) throws UnknownHostException { + return ipversion == NetworkProtocolVersion.IPV4 ? + InetAddress.getByName("127.0.0.1") : InetAddress.getByName("::1"); + } + + public static String getLocalHostName(String defaultHostName) { + String hostName = getLocalAddress().getHostName(); + if (hostName == null) { + return defaultHostName; + } + return hostName; + } + + public static String getLocalHostAddress(String defaultHostAddress) { + String hostAddress = getLocalAddress().getHostAddress(); + if (hostAddress == null) { + return defaultHostAddress; + } + return hostAddress; + } + + public static NetworkClass getNetworkClass(InetAddress address) { + if (address == null) { + return NetworkClass.ANY; + } + if (address.isLoopbackAddress()) { + return NetworkClass.LOOPBACK; + } + // link local unicast address + if (address.isLinkLocalAddress()) { + return NetworkClass.LOCAL; + } + // site local unicast address + if (address.isSiteLocalAddress()) { + return NetworkClass.SITE; + } + // wildcard address + if (address.isAnyLocalAddress()) { + return NetworkClass.ANY; + } + // other, must be public + return NetworkClass.PUBLIC; + } + + public static boolean matchesNetwork(NetworkClass given, NetworkClass expected) { + switch (expected) { + case ANY: + return EnumSet.of(NetworkClass.LOOPBACK, NetworkClass.LOCAL, NetworkClass.SITE, NetworkClass.PUBLIC, NetworkClass.ANY) + .contains(given); + case PUBLIC: + return EnumSet.of(NetworkClass.LOOPBACK, NetworkClass.LOCAL, NetworkClass.SITE, NetworkClass.PUBLIC) + .contains(given); + case SITE: + return EnumSet.of(NetworkClass.LOOPBACK, NetworkClass.LOCAL, NetworkClass.SITE) + .contains(given); + case LOCAL: + return EnumSet.of(NetworkClass.LOOPBACK, NetworkClass.LOCAL) + .contains(given); + case LOOPBACK: + return NetworkClass.LOOPBACK == given; + } + return false; + } + + public static List getAllAvailableAddresses() { + List allAddresses = new ArrayList<>(); + for (NetworkInterface networkInterface : getAllNetworkInterfaces()) { + Enumeration addrs = networkInterface.getInetAddresses(); + while (addrs.hasMoreElements()) { + allAddresses.add(addrs.nextElement()); + } + } + sortAddresses(allAddresses); + return allAddresses; + } + + public static boolean isIpv4Available() { + return getAllAvailableAddresses().stream().anyMatch(addr -> addr instanceof Inet4Address); + } + + public static boolean isIpv6Available() { + return getAllAvailableAddresses().stream().anyMatch(addr -> addr instanceof Inet6Address); + } + + public static List getAllActiveAddresses() { + List allAddresses = new ArrayList<>(); + for (NetworkInterface networkInterface : getActiveInterfaces()) { + Enumeration addrs = networkInterface.getInetAddresses(); + while (addrs.hasMoreElements()) { + allAddresses.add(addrs.nextElement()); + } + } + sortAddresses(allAddresses); + return allAddresses; + } + + public static boolean isIpv4Active() { + return getAllActiveAddresses().stream().anyMatch(addr -> addr instanceof Inet4Address); + } + + public static boolean isIpv6Active() { + return getAllActiveAddresses().stream().anyMatch(addr -> addr instanceof Inet6Address); + } + + public static List getAllNetworkInterfaces() { + return getInterfaces(n -> true); + } + + public static List getNonLoopbackNetworkInterfaces() { + return getInterfaces(NetworkUtils::isNonLoopBack); + } + + public static List getActiveInterfaces() { + return getInterfaces(NetworkUtils::isUp); + } + + public static List getAddresses(List networkInterfaces, + Predicate predicate, + NetworkProtocolVersion networkProtocolVersion) { + List addresses = new ArrayList<>(); + for (NetworkInterface networkInterface : networkInterfaces) { + if (predicate.test(networkInterface)) { + Enumeration addrs = networkInterface.getInetAddresses(); + while (addrs.hasMoreElements()) { + InetAddress address = addrs.nextElement(); + if ((address instanceof Inet4Address && networkProtocolVersion == NetworkProtocolVersion.IPV4) || + (address instanceof Inet6Address && networkProtocolVersion == NetworkProtocolVersion.IPV6)) { + addresses.add(address); + } + } + } + } + return addresses; + } + + public static InetAddress getFirstAddress(List networkInterfaces, NetworkProtocolVersion networkProtocolVersion) { + List addresses = getAddresses(networkInterfaces, n-> true, networkProtocolVersion); + return addresses.isEmpty() ? null : addresses.get(0); + } + + public static InetAddress getFirstNonLoopbackAddress(List networkInterfaces, NetworkProtocolVersion networkProtocolVersion) { + List addresses = getAddresses(networkInterfaces, NetworkUtils::isNonLoopBack, networkProtocolVersion); + return addresses.isEmpty() ? null : addresses.get(0); + } + + public static boolean interfaceSupports(NetworkInterface networkInterface, NetworkProtocolVersion ipVersion) { + boolean supportsVersion = false; + if (networkInterface != null) { + Enumeration addresses = networkInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if ((address instanceof Inet4Address && (ipVersion == NetworkProtocolVersion.IPV4)) || + (address instanceof Inet6Address && (ipVersion == NetworkProtocolVersion.IPV6))) { + supportsVersion = true; + break; + } + } + } + return supportsVersion; + } + + public static NetworkProtocolVersion getProtocolVersion() { + switch (findAvailableProtocols()) { + case IPV4: + return NetworkProtocolVersion.IPV4; + case IPV6: + return NetworkProtocolVersion.IPV6; + case IPV46: + if (Boolean.getBoolean(System.getProperty(IPV4_SETTING))) { + return NetworkProtocolVersion.IPV4; + } + if (Boolean.getBoolean(System.getProperty(IPV6_SETTING))) { + return NetworkProtocolVersion.IPV6; + } + return NetworkProtocolVersion.IPV6; + default: + break; + } + return NetworkProtocolVersion.NONE; + } + + public static NetworkProtocolVersion findAvailableProtocols() { + boolean hasIPv4 = false; + boolean hasIPv6 = false; + for (InetAddress addr : getAllAvailableAddresses()) { + if (addr instanceof Inet4Address) { + hasIPv4 = true; + } + if (addr instanceof Inet6Address) { + hasIPv6 = true; + } + } + if (hasIPv4 && hasIPv6) { + return NetworkProtocolVersion.IPV46; + } + if (hasIPv4) { + return NetworkProtocolVersion.IPV4; + } + if (hasIPv6) { + return NetworkProtocolVersion.IPV6; + } + return NetworkProtocolVersion.NONE; + } + + public static InetAddress resolveInetAddress(String hostname, String defaultValue) throws IOException { + String host = hostname; + if (host == null) { + host = defaultValue; + } + String origHost = host; + // strip port + int pos = host.indexOf(':'); + if (pos > 0) { + host = host.substring(0, pos - 1); + } + if ((host.startsWith("#") && host.endsWith("#")) || (host.startsWith("_") && host.endsWith("_"))) { + host = host.substring(1, host.length() - 1); + if ("loopback".equals(host)) { + return InetAddress.getLoopbackAddress(); + } else if ("local".equals(host)) { + return getLocalAddress(); + } else if (host.startsWith("non_loopback")) { + if (host.toLowerCase(Locale.ROOT).endsWith(":ipv4")) { + return getFirstNonLoopbackAddress(getActiveInterfaces(), NetworkProtocolVersion.IPV4); + } else if (host.toLowerCase(Locale.ROOT).endsWith(":ipv6")) { + return getFirstNonLoopbackAddress(getActiveInterfaces(), NetworkProtocolVersion.IPV6); + } else { + return getFirstNonLoopbackAddress(getActiveInterfaces(), getProtocolVersion()); + } + } else { + NetworkProtocolVersion networkProtocolVersion = getProtocolVersion(); + String reducedHost = host.substring(0, host.length() - 5); + if (host.toLowerCase(Locale.ROOT).endsWith(":ipv4")) { + networkProtocolVersion = NetworkProtocolVersion.IPV4; + host = reducedHost; + } else if (host.toLowerCase(Locale.ROOT).endsWith(":ipv6")) { + networkProtocolVersion = NetworkProtocolVersion.IPV6; + host = reducedHost; + } + for (NetworkInterface networkInterface : getActiveInterfaces()) { + if (host.equals(networkInterface.getName()) || host.equals(networkInterface.getDisplayName())) { + if (networkInterface.isLoopback()) { + return getFirstAddress(List.of(networkInterface), networkProtocolVersion); + } else { + return getFirstNonLoopbackAddress(List.of(networkInterface), networkProtocolVersion); + } + } + } + } + throw new IOException("failed to find network interface for [" + origHost + "]"); + } + if ("0.0.0.0".equals(host) || "::".equals(host)) { + return new InetSocketAddress(0).getAddress(); + } + return InetAddress.getByName(host); + } + + public static InetAddress resolvePublicHostAddress(String host) throws IOException { + InetAddress address = resolveInetAddress(host, null); + if (address == null || address.isAnyLocalAddress()) { + address = getFirstNonLoopbackAddress(getActiveInterfaces(), NetworkProtocolVersion.IPV4); + if (address == null) { + address = getFirstNonLoopbackAddress(getActiveInterfaces(), getProtocolVersion()); + if (address == null) { + address = getLocalAddress(); + if (address == null) { + return getLocalhost(NetworkProtocolVersion.IPV4); + } + } + } + } + return address; + } + + public static List getInterfaces(Predicate predicate) { + List networkInterfaces = new ArrayList<>(); + for (NetworkInterface networkInterface : waitForNetworkInterfaces()) { + if (predicate.test(networkInterface)) { + networkInterfaces.add(networkInterface); + Enumeration subInterfaces = networkInterface.getSubInterfaces(); + while (subInterfaces.hasMoreElements()) { + networkInterfaces.add(subInterfaces.nextElement()); + } + } + } + sortInterfaces(networkInterfaces); + return networkInterfaces; + } + + public static List waitForNetworkInterfaces() { + return waitForNetworkInterfaces(30L, TimeUnit.SECONDS); + } + + public static List waitForNetworkInterfaces(long period, TimeUnit timeUnit) { + if (latch.getCount() == 0L) { + return interfaceWaiter.interfaces; + } + ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); + ScheduledFuture future = service.scheduleAtFixedRate(interfaceWaiter, 0L, period, timeUnit); + try { + latch.await(); + future.cancel(true); + service.shutdownNow(); + return interfaceWaiter.interfaces; + } catch (InterruptedException e) { + throw new IllegalStateException("timeout while waiting for network interfaces"); + } + } + + private static class InterfaceWaiter implements Runnable { + + private final List interfaces = new ArrayList<>(); + + private final Logger logger = Logger.getLogger("network"); + + @Override + public void run() { + try { + interfaces.clear(); + logger.log(Level.INFO, "waiting for network interfaces"); + Enumeration networkInterfaceEnumeration = NetworkInterface.getNetworkInterfaces(); + if (networkInterfaceEnumeration.hasMoreElements()) { + do { + NetworkInterface networkInterface = networkInterfaceEnumeration.nextElement(); + interfaces.add(networkInterface); + } while (networkInterfaceEnumeration.hasMoreElements()); + logger.log(Level.INFO, "network interfaces present: " + interfaces); + if (!interfaces.isEmpty()) { + latch.countDown(); + } + } + } catch (Exception e) { + // getNetworkInterfaces() throws socket exception if no network is configured + logger.log(Level.WARNING, e.getMessage()); + } + } + } + + public static String getNetworkInterfacesAsString() { + StringBuilder sb = new StringBuilder(); + for (NetworkInterface nic : getAllNetworkInterfaces()) { + sb.append(getNetworkInterfaceAsString(nic)); + } + return sb.toString(); + } + + public static String getNetworkInterfaceAsString(NetworkInterface nic) { + StringBuilder sb = new StringBuilder(); + sb.append(lf).append(nic.getName()).append(lf); + if (!nic.getName().equals(nic.getDisplayName())) { + sb.append("\t").append(nic.getDisplayName()).append(lf); + } + sb.append("\t").append("flags "); + List flags = new ArrayList<>(); + try { + if (nic.isUp()) { + flags.add("UP"); + } + if (nic.supportsMulticast()) { + flags.add("MULTICAST"); + } + if (nic.isLoopback()) { + flags.add("LOOPBACK"); + } + if (nic.isPointToPoint()) { + flags.add("POINTTOPOINT"); + } + if (nic.isVirtual()) { + flags.add("VIRTUAL"); + } + } catch (Exception e) { + Logger.getLogger("network").log(Level.WARNING, e.getMessage(), e); + } + sb.append(String.join(",", flags)); + try { + sb.append(" mtu ").append(nic.getMTU()).append(lf); + } catch (SocketException e) { + Logger.getLogger("network").log(Level.WARNING, e.getMessage(), e); + } + List addresses = nic.getInterfaceAddresses(); + for (InterfaceAddress address : addresses) { + sb.append("\t").append(formatAddress(address)).append(lf); + } + try { + byte[] b = nic.getHardwareAddress(); + if (b != null) { + sb.append("\t").append("ether "); + for (int i = 0; i < b.length; i++) { + if (i > 0) { + sb.append(":"); + } + sb.append(hexDigit[(b[i] >> 4) & 0x0f]).append(hexDigit[b[i] & 0x0f]); + } + sb.append(lf); + } + } catch (SocketException e) { + Logger.getLogger("network").log(Level.WARNING, e.getMessage(), e); + } + return sb.toString(); + } + + public static InetAddress getLocalAddress() { + if (localAddress != null) { + return localAddress; + } + InetAddress address; + try { + address = InetAddress.getLocalHost(); + } catch (Exception e) { + Logger.getLogger("network").log(Level.WARNING, e.getMessage(), e); + address = InetAddress.getLoopbackAddress(); + } + localAddress = address; + return localAddress; + } + + + public static String format(InetAddress address) { + return format(address, -1); + } + + public static String format(InetSocketAddress address) { + return format(address.getAddress(), address.getPort()); + } + + public static String format(InetAddress address, int port) { + Objects.requireNonNull(address); + StringBuilder sb = new StringBuilder(); + if (port != -1 && address instanceof Inet6Address) { + sb.append(toUriString(address)); + } else { + sb.append(toAddrString(address)); + } + if (port != -1) { + sb.append(':').append(port); + } + return sb.toString(); + } + + public static String toUriString(InetAddress ip) { + if (ip instanceof Inet6Address) { + return "[" + toAddrString(ip) + "]"; + } + return toAddrString(ip); + } + + public static String toAddrString(InetAddress ip) { + if (ip == null) { + throw new NullPointerException("ip"); + } + if (ip instanceof Inet4Address) { + byte[] bytes = ip.getAddress(); + return (bytes[0] & 0xff) + "." + (bytes[1] & 0xff) + "." + (bytes[2] & 0xff) + "." + (bytes[3] & 0xff); + } + if (!(ip instanceof Inet6Address)) { + throw new IllegalArgumentException("ip"); + } + byte[] bytes = ip.getAddress(); + int[] hextets = new int[8]; + for (int i = 0; i < hextets.length; i++) { + hextets[i] = (bytes[2 * i] & 255) << 8 | bytes[2 * i + 1] & 255; + } + compressLongestRunOfZeroes(hextets); + return hextetsToIPv6String(hextets); + } + + public static String formatAddress(InterfaceAddress interfaceAddress) { + StringBuilder sb = new StringBuilder(); + InetAddress address = interfaceAddress.getAddress(); + if (address instanceof Inet6Address) { + sb.append("inet6 ").append(format(address)) + .append(" prefixlen:").append(interfaceAddress.getNetworkPrefixLength()); + } else { + int netmask = 0xFFFFFFFF << (32 - interfaceAddress.getNetworkPrefixLength()); + byte[] b = new byte[] { + (byte) (netmask >>> 24), + (byte) (netmask >>> 16 & 0xFF), + (byte) (netmask >>> 8 & 0xFF), + (byte) (netmask & 0xFF) + }; + sb.append("inet ").append(format(address)); + try { + sb.append(" netmask:").append(format(InetAddress.getByAddress(b))); + } catch (UnknownHostException e) { + Logger.getLogger("network").log(Level.WARNING, e.getMessage(), e); + } + InetAddress broadcast = interfaceAddress.getBroadcast(); + if (broadcast != null) { + sb.append(" broadcast:").append(format(broadcast)); + } + } + if (address.isLoopbackAddress()) { + sb.append(" scope:host"); + } else if (address.isLinkLocalAddress()) { + sb.append(" scope:link"); + } else if (address.isSiteLocalAddress()) { + sb.append(" scope:site"); + } + return sb.toString(); + } + + private static boolean isUp(NetworkInterface networkInterface) { + try { + return networkInterface.isUp(); + } catch (SocketException e) { + return false; + } + } + + private static boolean isNonLoopBack(NetworkInterface networkInterface) { + try { + return !networkInterface.isLoopback(); + } catch (SocketException e) { + return false; + } + } + + private static int compareBytes(byte[] left, byte[] right) { + for (int i = 0, j = 0; i < left.length && j < right.length; i++, j++) { + int a = left[i] & 0xff; + int b = right[j] & 0xff; + if (a != b) { + return a - b; + } + } + return left.length - right.length; + } + + private static void compressLongestRunOfZeroes(int[] hextets) { + int bestRunStart = -1; + int bestRunLength = -1; + int runStart = -1; + for (int i = 0; i < hextets.length + 1; i++) { + if (i < hextets.length && hextets[i] == 0) { + if (runStart < 0) { + runStart = i; + } + } else if (runStart >= 0) { + int runLength = i - runStart; + if (runLength > bestRunLength) { + bestRunStart = runStart; + bestRunLength = runLength; + } + runStart = -1; + } + } + if (bestRunLength >= 2) { + Arrays.fill(hextets, bestRunStart, bestRunStart + bestRunLength, -1); + } + } + + private static String hextetsToIPv6String(int[] hextets) { + StringBuilder sb = new StringBuilder(39); + boolean lastWasNumber = false; + for (int i = 0; i < hextets.length; i++) { + boolean b = hextets[i] >= 0; + if (b) { + if (lastWasNumber) { + sb.append(':'); + } + sb.append(Integer.toHexString(hextets[i])); + } else { + if (i == 0 || lastWasNumber) { + sb.append("::"); + } + } + lastWasNumber = b; + } + return sb.toString(); + } + + private static void sortInterfaces(List interfaces) { + interfaces.sort(Comparator.comparingInt(NetworkInterface::getIndex)); + } + + private static void sortAddresses(List addressList) { + addressList.sort((o1, o2) -> compareBytes(o1.getAddress(), o2.getAddress())); + } +} diff --git a/net/src/main/java/org/xbib/net/Parameter.java b/net/src/main/java/org/xbib/net/Parameter.java new file mode 100644 index 0000000..104fc00 --- /dev/null +++ b/net/src/main/java/org/xbib/net/Parameter.java @@ -0,0 +1,221 @@ +package org.xbib.net; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.xbib.datastructures.common.ImmutableList; +import org.xbib.datastructures.common.LinkedHashSetMultiMap; +import org.xbib.datastructures.common.MultiMap; +import org.xbib.datastructures.common.Pair; + +public class Parameter implements Iterable>, Comparable { + + private static final Parameter EMPTY = Parameter.builder().build(); + + private final ParameterBuilder builder; + + private final ImmutableList> list; + + private final String queryString; + + Parameter(ParameterBuilder builder, + ImmutableList> list, + String queryString) { + this.builder = builder; + this.list = list; + this.queryString = queryString; + } + + public static ParameterBuilder builder() { + return new ParameterBuilder(); + } + + public static ParameterBuilder builder(ParameterBuilder parameterBuilder) { + return new ParameterBuilder(parameterBuilder); + } + + public static Parameter of() { + return EMPTY; + } + + public static Parameter of(String domain) { + return Parameter.builder().domain(domain).build(); + } + + public static Parameter of(Map map) { + return Parameter.builder().enableSort().add(map).build(); + } + + public static Parameter of(String domain, Map map) { + return Parameter.builder().domain(domain).enableSort().add(map).build(); + } + + @Override + public Iterator> iterator() { + return list.iterator(); + } + + @Override + public boolean equals(Object o) { + return o instanceof Parameter && + builder.domain.equals(((Parameter) o).builder.domain) && + list.equals(((Parameter) o).list); + } + + @Override + public int hashCode() { + return Objects.hash(builder.domain, list); + } + + @Override + public int compareTo(Parameter o) { + return toString().compareTo(o.toString()); + } + + @Override + public String toString() { + return list.toString(); + } + + @SuppressWarnings("unchecked") + public String getAsString(String domain, String key) { + Object object = get(domain, key); + if (object instanceof Collection) { + Collection collection = (Collection) object; + object = collection.iterator().next(); + return object != null ? object.toString() : null; + } + return object != null ? object instanceof String ? (String) object : object.toString() : null; + } + + @SuppressWarnings("unchecked") + public Integer getAsInteger(String domain, String key) { + Object object = get(domain, key); + if (object instanceof Collection) { + Collection collection = (Collection) object; + object = collection.iterator().next(); + return object != null ? Integer.parseInt(object.toString()) : null; + } + try { + return object != null ? object instanceof Integer ? (Integer) object : Integer.parseInt(object.toString()) : null; + } catch (Exception e) { + return null; + } + } + + @SuppressWarnings("unchecked") + public Boolean getAsBoolean(String domain, String key) { + Object object = get(domain, key); + if (object instanceof Collection) { + Collection collection = (Collection) object; + object = collection.iterator().next(); + return object != null ? Boolean.parseBoolean(object.toString()) : null; + } + try { + return object != null ? object instanceof Boolean ? (Boolean) object : Boolean.parseBoolean(object.toString()) : null; + } catch (Exception e) { + return null; + } + } + + public String allToString() { + StringBuilder sb = new StringBuilder(); + sb.append(list.toString()); + if (!builder.parameterMap.isEmpty()) { + builder.parameterMap.forEach((key, value) -> sb.append(" ").append(key).append(" -> ").append(value)); + } + return sb.toString(); + } + + public boolean hasElements() { + return !list.isEmpty(); + } + + public MultiMap asMultiMap() { + MultiMap multiMap = new LinkedHashSetMultiMap<>(); + this.forEach(p -> multiMap.put(p.getKey(), p.getValue())); + return multiMap; + } + + public Map asSingleValuedMap() { + Map map = new LinkedHashMap<>(); + this.forEach(p -> map.put(p.getKey(), createValue(p.getValue()))); + return map; + } + + @SuppressWarnings("unchecked") + private static Object createValue(Object object) { + if (object instanceof Collection) { + Collection collection = (Collection) object; + if (collection.size() == 1) { + return collection.iterator().next(); + } else { + return collection; + } + } + return object; + } + + public String getDomain() { + return builder.domain; + } + + public Stream> stream(String domain) { + if (!builder.domain.equals(domain)) { + throw new IllegalArgumentException("domain mismatch"); + } + return list.stream(); + } + + public List getAll(String domain, String key) { + Optional optional = builder.parameterMap.values().stream().filter(p -> domain.equals(p.getDomain())).findFirst(); + if (optional.isPresent()) { + return optional.get().getAll(domain, key); + } else { + if (!builder.domain.equals(domain)) { + throw new IllegalArgumentException("domain mismatch"); + } + return list.stream() + .filter(p -> key.equals(p.getKey())) + .map(Pair::getValue) + .collect(Collectors.toList()); + } + } + + public boolean containsKey(String domain, String key) { + Optional optional = builder.parameterMap.values().stream().filter(p -> domain.equals(p.getDomain())).findFirst(); + if (optional.isPresent()) { + return optional.get().containsKey(domain, key); + } else { + if (!builder.domain.equals(domain)) { + throw new IllegalArgumentException("domain mismatch"); + } + return list.stream().anyMatch(p -> key.equals(p.getKey())); + } + } + + public Object get(String domain, String key) { + Optional optional = builder.parameterMap.values().stream().filter(p -> domain.equals(p.getDomain())).findFirst(); + if (optional.isPresent()) { + return optional.get().getAll(domain, key); + } else { + if (!builder.domain.equals(domain)) { + throw new IllegalArgumentException("domain mismatch"); + } + return list.stream() + .filter(p -> key.equals(p.getKey())) + .map(Pair::getValue) + .findFirst().orElse(null); + } + } + + public String getAsQueryString() { + return queryString; + } +} diff --git a/net/src/main/java/org/xbib/net/ParameterBuilder.java b/net/src/main/java/org/xbib/net/ParameterBuilder.java new file mode 100644 index 0000000..f480f58 --- /dev/null +++ b/net/src/main/java/org/xbib/net/ParameterBuilder.java @@ -0,0 +1,378 @@ +package org.xbib.net; + +import java.io.UncheckedIOException; +import java.lang.reflect.Array; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.xbib.datastructures.common.ImmutableList; +import org.xbib.datastructures.common.MultiMap; +import org.xbib.datastructures.common.Pair; +import org.xbib.datastructures.common.PairValidator; + +public class ParameterBuilder implements PairValidator { + + @SuppressWarnings("unchecked") + private static final Pair[] EMPTY_PAIR = (Pair[]) Array.newInstance(Pair.class, 0); + + private static final char AMPERSAND_CHAR = '&'; + + private static final char EQUAL_CHAR = '='; + + private static final Integer MAX_PARAMS_IN_QUERY_STRING = 1024; + + private final List> list; + + String domain; + + final Map parameterMap; + + private Charset charset; + + private PercentEncoder percentEncoder; + + private PercentDecoder percentDecoder; + + private int limit; + + private PairValidator pairValidator; + + private ParameterValidator parameterValidator; + + private boolean enableLowerCaseNames; + + private boolean enablePercentEncoding; + + private boolean enableQueryStringPercentEncoding; + + private boolean enablePercentDecoding; + + private boolean enableSort; + + private boolean enableDuplicates; + + private boolean enableQueryString; + + ParameterBuilder() { + this.list = new ArrayList<>(); + this.parameterMap = new HashMap<>(); + this.domain = "DEFAULT"; + this.limit = 0; + } + + ParameterBuilder(ParameterBuilder builder) { + this.list = builder.list; + this.parameterMap = builder.parameterMap; + this.domain = builder.domain; + this.limit = builder.limit; + this.charset = builder.charset; + this.percentDecoder = builder.percentDecoder; + this.percentEncoder = builder.percentEncoder; + this.enableLowerCaseNames = builder.enableLowerCaseNames; + this.enablePercentDecoding = builder.enablePercentDecoding; + this.enablePercentEncoding = builder.enablePercentEncoding; + this.enableQueryString = builder.enableQueryString; + this.enableQueryStringPercentEncoding = builder.enableQueryStringPercentEncoding; + this.enableSort = builder.enableSort; + this.enableDuplicates = builder.enableDuplicates; + this.pairValidator = builder.pairValidator; + this.parameterValidator = builder.parameterValidator; + } + + public ParameterBuilder lowercase() { + this.enableLowerCaseNames = true; + return this; + } + + public ParameterBuilder charset(Charset charset) { + this.charset = charset; + this.percentEncoder = PercentEncoders.getQueryParamEncoder(charset); + this.percentDecoder = new PercentDecoder(charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE)); + return this; + } + + public ParameterBuilder percentEncoder(PercentEncoder percentEncoder) { + this.percentEncoder = percentEncoder; + return this; + } + + public ParameterBuilder percentDecode(PercentDecoder percentDecoder) { + this.percentDecoder = percentDecoder; + return this; + } + + public ParameterBuilder enablePercentEncoding() { + this.enablePercentEncoding = true; + charset(StandardCharsets.UTF_8); + return this; + } + + public ParameterBuilder enablePercentDeccoding() { + this.enablePercentDecoding = true; + charset(StandardCharsets.UTF_8); + return this; + } + + public ParameterBuilder limit(int limit) { + this.limit = limit; + return this; + } + + public ParameterBuilder pairValidator(PairValidator validator) { + this.pairValidator = validator; + return this; + } + + public ParameterBuilder parameterValidator(ParameterValidator validator) { + this.parameterValidator = validator; + return this; + } + + public ParameterBuilder domain(String domain) { + this.domain = domain; + return this; + } + + public ParameterBuilder enableSort() { + this.enableSort = true; + return this; + } + + public ParameterBuilder enableDuplicates() { + this.enableDuplicates = true; + return this; + } + + public ParameterBuilder enableQueryString(boolean enableQueryStringPercentEncoding) { + this.enableQueryString = true; + this.enableQueryStringPercentEncoding = enableQueryStringPercentEncoding; + charset(StandardCharsets.UTF_8); + return this; + } + + public ParameterBuilder add(Map map) { + map.forEach(this::add); + return this; + } + + public ParameterBuilder add(MultiMap map) { + map.asMap().forEach(this::add); + return this; + } + + public ParameterBuilder add(Parameter parameter) { + if (domain.equals(parameter.getDomain())) { + parameter.stream(domain).forEach(this::add); + } else { + parameterMap.putIfAbsent(parameter.getDomain(), parameter); + } + return this; + } + + public ParameterBuilder addPercentEncodedBody(String body) { + if (body == null) { + return this; + } + if (percentDecoder == null) { + charset(StandardCharsets.UTF_8); + } + // watch out for "plus" encoding, replace it with a space character + String s = body.replace('+', ' '); + while (s != null) { + Pair pairs = indexOf(AMPERSAND_CHAR, s); + Pair pair = indexOf(EQUAL_CHAR, pairs.getKey()); + if (pair.getKey() != null && !pair.getKey().isEmpty()) { + try { + add(percentDecoder.decode(pair.getKey()), + pair.getValue() instanceof CharSequence ? percentDecoder.decode((CharSequence) pair.getValue()) : pair.getValue()); + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new UncheckedIOException(e); + } + } + s = pairs.getValue() !=null ? pairs.getValue().toString() : null; + } + return this; + } + + public ParameterBuilder add(Pair pair) { + add(pair.getKey(), pair.getValue()); + return this; + } + + public ParameterBuilder add(String name, Object value) { + if (limit > 0 && list.size() >= limit) { + throw new IllegalArgumentException("parameter limit " + limit + " exceeded"); + } + Pair pair = apply(Pair.of(name, value)); + if (pair != null) { + if (enableDuplicates || !list.contains(pair)) { + list.add(pair); + } + } + return this; + } + + public ParameterBuilder add(String percentEncodedQueryString) { + try { + decodeQueryString(percentEncodedQueryString); + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new UncheckedIOException(e); + } + return this; + } + + public Parameter build() { + if (enableSort) { + list.sort(Comparator.comparing(Pair::getKey)); + } + String queryString = null; + if (enableQueryString) { + try { + queryString = encodeQueryString(); + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new UncheckedIOException(e); + } + } + Parameter parameter = new Parameter(this, ImmutableList.of(list, EMPTY_PAIR), queryString); + if (parameterValidator != null) { + return parameterValidator.apply(parameter); + } else { + return parameter; + } + } + + @Override + public String toString() { + return list.toString(); + } + + @Override + public Pair apply(Pair pair) { + Object theValue = pair.getValue(); + if (pairValidator != null) { + return pairValidator.apply(pair); + } + String theName = pair.getKey(); + if (enableLowerCaseNames) { + theName = theName.toLowerCase(Locale.ROOT); + } + if (charset != null && !charset.equals(Charset.defaultCharset())) { + theName = new String(theName.getBytes(charset)); + } + if (enablePercentEncoding) { + try { + theName = percentEncoder.encode(theName); + if (theValue instanceof CharSequence) { + theValue = percentEncoder.encode((CharSequence) theValue); + } + } catch (MalformedInputException | UnmappableCharacterException e) { + // never thrown + throw new UncheckedIOException(e); + } + } + if (enablePercentDecoding) { + try { + theName = percentDecoder.decode(theName); + if (theValue instanceof CharSequence) { + theValue = percentDecoder.decode((CharSequence) theValue); + } + } catch (MalformedInputException | UnmappableCharacterException e) { + // never thrown + throw new UncheckedIOException(e); + } + } + return Pair.of(theName, theValue); + } + + public boolean isEmpty() { + return list.isEmpty(); + } + + public Iterator> iterator() { + return list.iterator(); + } + + private String encodeQueryString() + throws MalformedInputException, UnmappableCharacterException { + Iterator> it = list.iterator(); + StringBuilder sb = new StringBuilder(); + while (it.hasNext()) { + Pair p = it.next(); + String k = (enableQueryStringPercentEncoding ? percentEncoder.encode(p.getKey()) : p.getKey()); + String v = p.getValue() != null ? + (enableQueryStringPercentEncoding && p.getValue() instanceof CharSequence ? + percentEncoder.encode((CharSequence) p.getValue()) : (p.getValue() != null ? p.getValue().toString() : "")) : ""; + sb.append(k).append(EQUAL_CHAR).append(v); + if (it.hasNext()) { + sb.append(AMPERSAND_CHAR); + } + } + return sb.toString(); + } + + private void decodeQueryString(String query) + throws MalformedInputException, UnmappableCharacterException { + if (query == null || query.isEmpty()) { + return; + } + String name = null; + int count = 0; + int pos = 0; + int i; + char c; + for (i = 0; i < query.length(); i++) { + c = query.charAt(i); + if (c == '=' && name == null) { + if (pos != i) { + name = query.substring(pos, i).replaceAll("\\+", "%20"); + name = percentDecoder.decode(name); + } + pos = i + 1; + } else if (c == '&' || c == ';') { + if (name == null && pos != i) { + if (++count > MAX_PARAMS_IN_QUERY_STRING) { + return; + } + String s = query.substring(pos, i).replaceAll("\\+", "%20"); + add(percentDecoder.decode(s), ""); + } else if (name != null) { + if (++count > MAX_PARAMS_IN_QUERY_STRING) { + return; + } + String value = query.substring(pos, i).replaceAll("\\+", "%20"); + add(name, percentDecoder.decode(value)); + name = null; + } + pos = i + 1; + } + } + if (pos != i) { + if (name == null) { + add(percentDecoder.decode(query.substring(pos, i)), ""); + } else { + String value = query.substring(pos, i).replaceAll("\\+", "%20"); + add(name, percentDecoder.decode(value)); + } + } else if (name != null) { + add(name, ""); + } + } + + private static Pair indexOf(char ch, String input) { + int i = input.indexOf(ch); + String k = i >= 0 ? input.substring(0, i) : input; + Object v = i >= 0 ? input.substring(i + 1) : null; + return Pair.of(k, v); + } +} diff --git a/net/src/main/java/org/xbib/net/ParameterDefinition.java b/net/src/main/java/org/xbib/net/ParameterDefinition.java new file mode 100644 index 0000000..eb3a28a --- /dev/null +++ b/net/src/main/java/org/xbib/net/ParameterDefinition.java @@ -0,0 +1,8 @@ +package org.xbib.net; + +public interface ParameterDefinition { + + String getName(); + + String getType(); +} diff --git a/net/src/main/java/org/xbib/net/ParameterValidator.java b/net/src/main/java/org/xbib/net/ParameterValidator.java new file mode 100644 index 0000000..142348b --- /dev/null +++ b/net/src/main/java/org/xbib/net/ParameterValidator.java @@ -0,0 +1,6 @@ +package org.xbib.net; + +import java.util.function.Function; + +public interface ParameterValidator extends Function { +} diff --git a/net/src/main/java/org/xbib/net/PathNormalizer.java b/net/src/main/java/org/xbib/net/PathNormalizer.java new file mode 100644 index 0000000..c99b58d --- /dev/null +++ b/net/src/main/java/org/xbib/net/PathNormalizer.java @@ -0,0 +1,60 @@ +package org.xbib.net; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.StringTokenizer; + +/** + * Path normalizer. + */ +public class PathNormalizer { + + private static final char SEPARATOR_CHAR = '/'; + + private static final String SEPARATOR_STRING = "/"; + + private PathNormalizer() { + } + + public static String normalize(String p) { + String path = p; + if (path == null || "".equals(path) || SEPARATOR_STRING.equals(path)) { + return SEPARATOR_STRING; + } + path = path.replaceAll("/+", SEPARATOR_STRING); + int leadingSlashes = 0; + while (leadingSlashes < path.length() && path.charAt(leadingSlashes) == SEPARATOR_CHAR) { + ++leadingSlashes; + } + boolean isDir = (path.charAt(path.length() - 1) == SEPARATOR_CHAR); + StringTokenizer st = new StringTokenizer(path, SEPARATOR_STRING); + LinkedList list = new LinkedList<>(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if ("..".equals(token)) { + if (!list.isEmpty() && !"..".equals(list.getLast())) { + list.removeLast(); + if (!st.hasMoreTokens()) { + isDir = true; + } + } + } else if (!".".equals(token) && !"".equals(token)) { + list.add(token); + } + } + StringBuilder sb = new StringBuilder(); + while (leadingSlashes-- > 0) { + sb.append(SEPARATOR_CHAR); + } + for (Iterator it = list.iterator(); it.hasNext();) { + sb.append(it.next()); + if (it.hasNext()) { + sb.append(SEPARATOR_CHAR); + } + } + if (isDir && sb.length() > 0 && sb.charAt(sb.length() - 1) != SEPARATOR_CHAR) { + sb.append(SEPARATOR_CHAR); + } + return sb.toString(); + } +} diff --git a/net/src/main/java/org/xbib/net/PercentDecoder.java b/net/src/main/java/org/xbib/net/PercentDecoder.java new file mode 100644 index 0000000..fd6de0b --- /dev/null +++ b/net/src/main/java/org/xbib/net/PercentDecoder.java @@ -0,0 +1,193 @@ +package org.xbib.net; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; + +/** + * Decodes percent-encoded strings. + * + * This class is not thread-safe. + */ +public class PercentDecoder { + + /** + * Written to with decoded chars by decoder. + */ + private final CharBuffer decodedCharBuf; + + private final CharsetDecoder decoder; + + /** + * The decoded string for the current input. + */ + private final StringBuilder outputBuf; + + /** + * The bytes represented by the current sequence of %-triples. Resized as needed. + */ + private ByteBuffer encodedBuf; + + public PercentDecoder() { + this(StandardCharsets.UTF_8.newDecoder() + .onUnmappableCharacter(CodingErrorAction.REPORT) + .onMalformedInput(CodingErrorAction.REPORT)); + } + + /** + * Construct a new PercentDecoder with default buffer sizes. + * + * @param charsetDecoder Charset to decode bytes into chars with + * @see PercentDecoder#PercentDecoder(CharsetDecoder, int, int) + */ + public PercentDecoder(CharsetDecoder charsetDecoder) { + this(charsetDecoder, 16, 16); + } + + /** + * @param charsetDecoder Charset to decode bytes into chars with + * @param initialEncodedByteBufSize Initial size of buffer that holds encoded bytes + * @param decodedCharBufSize Size of buffer that encoded bytes are decoded into + */ + public PercentDecoder(CharsetDecoder charsetDecoder, int initialEncodedByteBufSize, int decodedCharBufSize) { + this.outputBuf = new StringBuilder(); + this.encodedBuf = ByteBuffer.allocate(initialEncodedByteBufSize); + this.decodedCharBuf = CharBuffer.allocate(decodedCharBufSize); + this.decoder = charsetDecoder; + } + + /** + * Decode a percent-encoded character sequuence to a string. + * + * @param input Input with %-encoded representation of characters in this instance's configured character set, e.g. + * "%20" for a space character + * @return Corresponding string with %-encoded data decoded and converted to their corresponding characters + * @throws MalformedInputException if decoder is configured to report errors and malformed input is detected + * @throws UnmappableCharacterException if decoder is configured to report errors and an unmappable character is + * detected + */ + public String decode(CharSequence input) throws MalformedInputException, UnmappableCharacterException { + if (input == null) { + return null; + } + outputBuf.setLength(0); + outputBuf.ensureCapacity((input.length() / 8)); + encodedBuf.clear(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c != '%') { + handleEncodedBytes(); + outputBuf.append(c); + continue; + } + if (i + 2 >= input.length()) { + CodingErrorAction codingErrorAction = decoder.malformedInputAction(); + if (codingErrorAction == CodingErrorAction.REPORT) { + throw new MalformedInputException(i); + } + if (codingErrorAction == CodingErrorAction.REPLACE) { + continue; + } + } + if (encodedBuf.remaining() == 0) { + ByteBuffer largerBuf = ByteBuffer.allocate(encodedBuf.capacity() * 2); + encodedBuf.flip(); + largerBuf.put(encodedBuf); + encodedBuf = largerBuf; + } + int c1 = input.charAt(++i); + int c2 = input.charAt(++i); + byte b1 = (byte) decode((char) c1); + byte b2 = (byte) decode((char) c2); + if (b1 == -1 || b2 == -1) { + CodingErrorAction codingErrorAction = decoder.malformedInputAction(); + if (codingErrorAction == CodingErrorAction.REPORT) { + throw new MalformedInputException(i); + } + if (codingErrorAction == CodingErrorAction.REPLACE) { + encodedBuf.put((byte) 0xff); + } + } else { + byte b = (byte) ((b1 & 0xf) << 4 | (b2 & 0xf)); + encodedBuf.put(b); + } + } + handleEncodedBytes(); + return outputBuf.toString(); + } + + private static int decode(char c) { + return (c >= '0' && c <= '9') ? c - '0' : + (c >= 'A' && c <= 'F') ? c - 'A' + 10 : + (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1; + } + + /** + * Decode any buffered encoded bytes and write them to the output buf. + */ + private void handleEncodedBytes() throws MalformedInputException, UnmappableCharacterException { + if (encodedBuf.position() == 0) { + return; + } + decoder.reset(); + CoderResult coderResult = CoderResult.OVERFLOW; + encodedBuf.flip(); + while (coderResult == CoderResult.OVERFLOW && encodedBuf.hasRemaining()) { + decodedCharBuf.clear(); + coderResult = decoder.decode(encodedBuf, decodedCharBuf, false); + throwIfError(coderResult); + decodedCharBuf.flip(); + outputBuf.append(decodedCharBuf); + } + decodedCharBuf.clear(); + coderResult = decoder.decode(encodedBuf, decodedCharBuf, true); + throwIfError(coderResult); + if (encodedBuf.hasRemaining()) { + throw new IllegalStateException("final decode didn't error, but didn't consume remaining input bytes"); + } + if (coderResult != CoderResult.UNDERFLOW) { + throw new IllegalStateException("expected underflow, but instead final decode returned " + coderResult); + } + decodedCharBuf.flip(); + outputBuf.append(decodedCharBuf); + encodedBuf.clear(); + flush(); + } + + /** + * Must only be called when the input encoded bytes buffer is empty. + */ + private void flush() throws MalformedInputException, UnmappableCharacterException { + CoderResult coderResult; + decodedCharBuf.clear(); + coderResult = decoder.flush(decodedCharBuf); + decodedCharBuf.flip(); + outputBuf.append(decodedCharBuf); + throwIfError(coderResult); + if (coderResult != CoderResult.UNDERFLOW) { + throw new IllegalStateException("decoder flush resulted in " + coderResult); + } + } + + /** + * If the coder result is considered an error (i.e. not overflow or underflow), throw the corresponding + * CharacterCodingException. + * + * @param coderResult result to check + * @throws MalformedInputException if result represents malformed input + * @throws UnmappableCharacterException if result represents an unmappable character + */ + private static void throwIfError(CoderResult coderResult) throws MalformedInputException, UnmappableCharacterException { + if (coderResult.isMalformed()) { + throw new MalformedInputException(coderResult.length()); + } + if (coderResult.isUnmappable()) { + throw new UnmappableCharacterException(coderResult.length()); + } + } +} diff --git a/net/src/main/java/org/xbib/net/PercentEncoder.java b/net/src/main/java/org/xbib/net/PercentEncoder.java new file mode 100644 index 0000000..41a625b --- /dev/null +++ b/net/src/main/java/org/xbib/net/PercentEncoder.java @@ -0,0 +1,178 @@ +package org.xbib.net; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import java.util.BitSet; + +/** + * Encodes unsafe characters as a sequence of %XX hex-encoded bytes. + * + * This is typically done when encoding components of URLs. See {@link PercentEncoders} for pre-configured + * PercentEncoder instances. + * + * This class is not thread-safe. + */ +public class PercentEncoder { + + private static final char[] HEX_CODE = "0123456789ABCDEF".toCharArray(); + + private final BitSet safeChars; + + private final CharsetEncoder encoder; + + private final StringBuilderPercentEncoderOutputHandler stringHandler; + + private final ByteBuffer encodedBytes; + + private final CharBuffer unsafeCharsToEncode; + + /** + * @param safeChars the set of chars to NOT encode, stored as a bitset with the int positions corresponding to + * those chars set to true. Treated as read only. + * @param charsetEncoder charset encoder to encode characters with. Make sure to not re-use CharsetEncoder instances + * across threads. + */ + PercentEncoder(BitSet safeChars, CharsetEncoder charsetEncoder) { + this.safeChars = safeChars; + this.encoder = charsetEncoder; + this.stringHandler = new StringBuilderPercentEncoderOutputHandler(); + int maxBytesPerChar = 1 + (int) encoder.maxBytesPerChar(); + encodedBytes = ByteBuffer.allocate(maxBytesPerChar * 2); + unsafeCharsToEncode = CharBuffer.allocate(2); + } + + /** + * Encode the input and pass output chars to a handler. + * + * @param input input string + * @param handler handler to call on each output character + * @throws MalformedInputException if encoder is configured to report errors and malformed input is detected + * @throws UnmappableCharacterException if encoder is configured to report errors and an unmappable character is + * detected + */ + private void encode(CharSequence input, StringBuilderPercentEncoderOutputHandler handler) + throws MalformedInputException, UnmappableCharacterException { + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + int cp = Character.codePointAt(String.valueOf(c), 0); + if (safeChars.get(cp)) { + handler.onOutputChar(c); + continue; + } + unsafeCharsToEncode.clear(); + unsafeCharsToEncode.append(c); + if (Character.isHighSurrogate(c)) { + if (input.length() > i + 1) { + char lowSurrogate = input.charAt(i + 1); + if (Character.isLowSurrogate(lowSurrogate)) { + unsafeCharsToEncode.append(lowSurrogate); + i++; + } else { + throw new IllegalArgumentException("invalid UTF-16: character " + + i + " is a high surrogate (\\u" + + Integer.toHexString(cp) + "), but char " + (i + 1) + + " is not a low surrogate (\\u" + + Integer.toHexString(Character.codePointAt(String.valueOf(lowSurrogate), 0)) + ")"); + } + } else { + throw new IllegalArgumentException("invalid UTF-16: the last character in the input string " + + "was a high surrogate (\\u" + Integer.toHexString(cp) + ")"); + } + } + flushUnsafeCharBuffer(handler); + } + } + + /** + * Encode the input and return the resulting text as a String. + * + * @param input input string + * @return the input string with every character that's not in safeChars turned into its byte representation via the + * instance's encoder and then percent-encoded + * @throws MalformedInputException if encoder is configured to report errors and malformed input is detected + * @throws UnmappableCharacterException if encoder is configured to report errors and an unmappable character is + * detected + */ + public String encode(CharSequence input) throws MalformedInputException, UnmappableCharacterException { + if (input == null) { + return null; + } + stringHandler.reset(); + stringHandler.ensureCapacity(input.length()); + encode(input, stringHandler); + return stringHandler.getContents(); + } + + /** + * Encode unsafeCharsToEncode to bytes as per charsetEncoder, then percent-encode those bytes into output. + * + * Side effects: unsafeCharsToEncode will be read from and cleared. encodedBytes will be cleared and written to. + * + * @param handler where the encoded versions of the contents of unsafeCharsToEncode will be written + */ + private void flushUnsafeCharBuffer(StringBuilderPercentEncoderOutputHandler handler) + throws MalformedInputException, UnmappableCharacterException { + // need to read from the char buffer, which was most recently written to + unsafeCharsToEncode.flip(); + encodedBytes.clear(); + encoder.reset(); + CoderResult result = encoder.encode(unsafeCharsToEncode, encodedBytes, true); + throwIfError(result); + result = encoder.flush(encodedBytes); + throwIfError(result); + encodedBytes.flip(); + while (encodedBytes.hasRemaining()) { + byte b = encodedBytes.get(); + handler.onOutputChar('%'); + handler.onOutputChar(HEX_CODE[b >> 4 & 0xF]); + handler.onOutputChar(HEX_CODE[b & 0xF]); + } + } + + /** + * @param result result to check + * @throws IllegalStateException if result is overflow + * @throws MalformedInputException if result represents malformed input + * @throws UnmappableCharacterException if result represents an unmappable character + */ + private static void throwIfError(CoderResult result) throws MalformedInputException, UnmappableCharacterException { + if (result.isOverflow()) { + throw new IllegalStateException("Byte buffer overflow, this should not happen"); + } + if (result.isMalformed()) { + throw new MalformedInputException(result.length()); + } + if (result.isUnmappable()) { + throw new UnmappableCharacterException(result.length()); + } + } + + static class StringBuilderPercentEncoderOutputHandler { + + private final StringBuilder stringBuilder; + + StringBuilderPercentEncoderOutputHandler() { + stringBuilder = new StringBuilder(); + } + + String getContents() { + return stringBuilder.toString(); + } + + void reset() { + stringBuilder.setLength(0); + } + + void ensureCapacity(int length) { + stringBuilder.ensureCapacity(length); + } + + void onOutputChar(char c) { + stringBuilder.append(c); + } + } +} diff --git a/net/src/main/java/org/xbib/net/PercentEncoders.java b/net/src/main/java/org/xbib/net/PercentEncoders.java new file mode 100644 index 0000000..2e53b6f --- /dev/null +++ b/net/src/main/java/org/xbib/net/PercentEncoders.java @@ -0,0 +1,172 @@ +package org.xbib.net; + +import java.nio.charset.Charset; +import java.util.BitSet; + +import static java.nio.charset.CodingErrorAction.REPORT; + +/** + * See RFC 3986, RFC 1738 and http://www.lunatech-research.com/archives/2009/02/03/what-every-web-developer-must-know-about-url-encoding. + */ +public class PercentEncoders { + + private static final BitSet UNRESERVED_BIT_SET = new BitSet(); + /** + * an encoder for RFC 3986 reg-names. + */ + private static final BitSet REG_NAME_BIT_SET = new BitSet(); + private static final BitSet PATH_BIT_SET = new BitSet(); + private static final BitSet MATRIX_BIT_SET = new BitSet(); + private static final BitSet QUERY_BIT_SET = new BitSet(); + private static final BitSet QUERY_PARAM_BIT_SET = new BitSet(); + private static final BitSet FRAGMENT_BIT_SET = new BitSet(); + + static { + // minimal encoding, for URI templates RFC 6570 + addUnreserved(UNRESERVED_BIT_SET); + // RFC 3986 'reg-name'. This is not very aggressive. + // It's quite possible to have DNS-illegal names out of this. + // Regardless, it will at least be URI-compliant even if it's not HTTP URL-compliant. + addUnreserved(REG_NAME_BIT_SET); + addSubdelims(REG_NAME_BIT_SET); + // Represents RFC 3986 'pchar'. Remove delimiter that starts matrix section. + addPChar(PATH_BIT_SET); + PATH_BIT_SET.clear((int) ';'); + // Remove delims for HTTP matrix params as per RFC 1738 S3.3. + // The other reserved chars ('/' and '?') are already excluded. + addPChar(MATRIX_BIT_SET); + MATRIX_BIT_SET.clear((int) ';'); + MATRIX_BIT_SET.clear((int) '='); + /* + * At this point it represents RFC 3986 'query'. http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 also + * specifies that "+" can mean space in a query, so we will make sure to say that '+' is not safe to leave as-is + */ + addQuery(QUERY_BIT_SET); + QUERY_BIT_SET.clear((int) '+'); + /* + * Create more stringent requirements for HTML4 queries: remove delimiters for HTML query params so that key=value + * pairs can be used. + */ + QUERY_PARAM_BIT_SET.or(QUERY_BIT_SET); + QUERY_PARAM_BIT_SET.clear((int) '='); + QUERY_PARAM_BIT_SET.clear((int) '&'); + addFragment(FRAGMENT_BIT_SET); + } + + public static PercentEncoder getUnreservedEncoder(Charset charset) { + return new PercentEncoder(UNRESERVED_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getCookieEncoder(Charset charset) { + return new PercentEncoder(UNRESERVED_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getRegNameEncoder(Charset charset) { + return new PercentEncoder(REG_NAME_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getPathEncoder(Charset charset) { + return new PercentEncoder(PATH_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getMatrixEncoder(Charset charset) { + return new PercentEncoder(MATRIX_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getQueryEncoder(Charset charset) { + return new PercentEncoder(QUERY_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getQueryParamEncoder(Charset charset) { + return new PercentEncoder(QUERY_PARAM_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + public static PercentEncoder getFragmentEncoder(Charset charset) { + return new PercentEncoder(FRAGMENT_BIT_SET, + charset.newEncoder().onMalformedInput(REPORT).onUnmappableCharacter(REPORT)); + } + + private PercentEncoders() { + } + + /** + * Add code points for 'fragment' chars. + * + * @param fragmentBitSet bit set + */ + private static void addFragment(BitSet fragmentBitSet) { + addPChar(fragmentBitSet); + fragmentBitSet.set((int) '/'); + fragmentBitSet.set((int) '?'); + } + + /** + * Add code points for 'query' chars. + * + * @param queryBitSet bit set + */ + private static void addQuery(BitSet queryBitSet) { + addPChar(queryBitSet); + queryBitSet.set((int) '/'); + queryBitSet.set((int) '?'); + } + + /** + * Add code points for 'pchar' chars. + * + * @param bs bitset + */ + private static void addPChar(BitSet bs) { + addUnreserved(bs); + addSubdelims(bs); + bs.set((int) ':'); + bs.set((int) '@'); + } + + /** + * Add codepoints for 'unreserved' chars. + * + * @param bs bitset to add codepoints to + */ + private static void addUnreserved(BitSet bs) { + for (int i = 'a'; i <= 'z'; i++) { + bs.set(i); + } + for (int i = 'A'; i <= 'Z'; i++) { + bs.set(i); + } + for (int i = '0'; i <= '9'; i++) { + bs.set(i); + } + bs.set((int) '-'); + bs.set((int) '.'); + bs.set((int) '_'); + bs.set((int) '~'); + } + + /** + * Add codepoints for 'sub-delims' chars. + * + * @param bs bitset to add codepoints to + */ + private static void addSubdelims(BitSet bs) { + bs.set((int) '!'); + bs.set((int) '$'); + bs.set((int) '&'); + bs.set((int) '\''); + bs.set((int) '('); + bs.set((int) ')'); + bs.set((int) '*'); + bs.set((int) '+'); + bs.set((int) ','); + bs.set((int) ';'); + bs.set((int) '='); + } +} diff --git a/net/src/main/java/org/xbib/net/ProtocolVersion.java b/net/src/main/java/org/xbib/net/ProtocolVersion.java new file mode 100644 index 0000000..2762664 --- /dev/null +++ b/net/src/main/java/org/xbib/net/ProtocolVersion.java @@ -0,0 +1,9 @@ +package org.xbib.net; + +/** + * The TCP/IP network protocol versions. + */ +public enum ProtocolVersion { + + IPV4, IPV6, NONE +} diff --git a/net/src/main/java/org/xbib/net/Request.java b/net/src/main/java/org/xbib/net/Request.java new file mode 100644 index 0000000..af0e232 --- /dev/null +++ b/net/src/main/java/org/xbib/net/Request.java @@ -0,0 +1,21 @@ +package org.xbib.net; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +public interface Request { + + InetSocketAddress getLocalAddress(); + + InetSocketAddress getRemoteAddress(); + + URL getBaseURL(); + + ByteBuffer getBody(); + + CharBuffer getBodyAsChars(Charset charset); + + R as(Class cl); +} diff --git a/net/src/main/java/org/xbib/net/Resource.java b/net/src/main/java/org/xbib/net/Resource.java new file mode 100644 index 0000000..ed629d4 --- /dev/null +++ b/net/src/main/java/org/xbib/net/Resource.java @@ -0,0 +1,33 @@ +package org.xbib.net; + +import java.nio.file.Path; +import java.time.Instant; + +public interface Resource { + + Path getPath(); + + String getName(); + + String getBaseName(); + + String getSuffix(); + + String getResourcePath(); + + URL getURL(); + + Instant getLastModified(); + + long getLength(); + + boolean isExists(); + + boolean isDirectory(); + + String getMimeType(); + + String getIndexFileName(); + + boolean isExistsIndexFile(); +} diff --git a/net/src/main/java/org/xbib/net/Response.java b/net/src/main/java/org/xbib/net/Response.java new file mode 100644 index 0000000..66f1f91 --- /dev/null +++ b/net/src/main/java/org/xbib/net/Response.java @@ -0,0 +1,6 @@ +package org.xbib.net; + +import java.io.Flushable; + +public interface Response extends Flushable { +} diff --git a/net/src/main/java/org/xbib/net/SecurityDomain.java b/net/src/main/java/org/xbib/net/SecurityDomain.java new file mode 100644 index 0000000..7e5d5a0 --- /dev/null +++ b/net/src/main/java/org/xbib/net/SecurityDomain.java @@ -0,0 +1,6 @@ +package org.xbib.net; + +public interface SecurityDomain { + + SecurityRealm getRealm(); +} diff --git a/net/src/main/java/org/xbib/net/SecurityRealm.java b/net/src/main/java/org/xbib/net/SecurityRealm.java new file mode 100644 index 0000000..82c85da --- /dev/null +++ b/net/src/main/java/org/xbib/net/SecurityRealm.java @@ -0,0 +1,34 @@ +package org.xbib.net; + +public abstract class SecurityRealm { + + public SecurityRealm() { + } + + /** + * @return unique name of this realm, e.g. "ldap" + */ + public abstract String getName(); + + /** + * Invoked during server startup and can be used to initialize internal state. + */ + public void init() { + } + + public abstract Authenticator getAuthenticator(); + + /** + * @return {@link UsersProvider} associated with this realm, null if not supported + */ + public UsersProvider getUsersProvider() { + return null; + } + + /** + * @return {@link GroupsProvider} associated with this realm, null if not supported + */ + public GroupsProvider getGroupsProvider() { + return null; + } +} diff --git a/net/src/main/java/org/xbib/net/SocketConfig.java b/net/src/main/java/org/xbib/net/SocketConfig.java new file mode 100644 index 0000000..8737396 --- /dev/null +++ b/net/src/main/java/org/xbib/net/SocketConfig.java @@ -0,0 +1,162 @@ +package org.xbib.net; + +public class SocketConfig { + + /** + * Keep alive. + */ + private boolean keepAlive = true; + + /** + * Default for SO_REUSEADDR. + */ + private boolean reuseAddr = true; + + /** + * Default for TCP_NODELAY. + */ + private boolean tcpNodelay = true; + + /** + * Set TCP send buffer to 64k per socket. + */ + private int tcpSendBufferSize = 64 * 1024; + + /** + * Set TCP receive buffer to 64k per socket. + */ + private int tcpReceiveBufferSize = 64 * 1024; + + /** + * Default for socket back log. + */ + private int backLogSize = 10 * 1024; + + /** + * The default socket timeout for SO_TIMEOUT in seconds. + */ + private int socketTimeoutMillis = 60000; + + /** + * Default connect timeout in milliseconds when socket is created. + */ + private int connectTimeoutMillis = 5000; + + /** + * Default read timeout in milliseconds per request. + */ + private int readTimeoutMillis = 15000; + + /** + * For SSL handshakes. + */ + private int sslHandshakeTimeoutMillis = 15000; + + /** + * Disable linger. + */ + private int linger = -1; + + public SocketConfig() { + } + + public SocketConfig setKeepAlive(boolean keepAlive) { + this.keepAlive = keepAlive; + return this; + } + + public boolean isKeepAlive() { + return keepAlive; + } + + public SocketConfig setReuseAddr(boolean reuseAddr) { + this.reuseAddr = reuseAddr; + return this; + } + + public boolean isReuseAddr() { + return reuseAddr; + } + + public SocketConfig setTcpNodelay(boolean tcpNodelay) { + this.tcpNodelay = tcpNodelay; + return this; + } + + public boolean isTcpNodelay() { + return tcpNodelay; + } + + public SocketConfig setTcpSendBufferSize(int tcpSendBufferSize) { + this.tcpSendBufferSize = tcpSendBufferSize; + return this; + } + + public int getTcpSendBufferSize() { + return tcpSendBufferSize; + } + + public SocketConfig setTcpReceiveBufferSize(int tcpReceiveBufferSize) { + this.tcpReceiveBufferSize = tcpReceiveBufferSize; + return this; + } + + public int getTcpReceiveBufferSize() { + return tcpReceiveBufferSize; + } + + public SocketConfig setBackLogSize(int backLogSize) { + this.backLogSize = backLogSize; + return this; + } + + public int getBackLogSize() { + return backLogSize; + } + + public SocketConfig setSocketTimeoutMillis(int socketTimeoutMillis) { + this.socketTimeoutMillis = socketTimeoutMillis; + return this; + } + + public int getSocketTimeoutMillis() { + return socketTimeoutMillis; + } + + public SocketConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + this.connectTimeoutMillis = connectTimeoutMillis; + return this; + } + + public int getConnectTimeoutMillis() { + return connectTimeoutMillis; + } + + public SocketConfig setReadTimeoutMillis(int readTimeoutMillis) { + this.readTimeoutMillis = readTimeoutMillis; + return this; + } + + public int getReadTimeoutMillis() { + return readTimeoutMillis; + } + + + public SocketConfig setSslHandshakeTimeoutMillis(int sslHandshakeTimeoutMillis) { + this.sslHandshakeTimeoutMillis = sslHandshakeTimeoutMillis; + return this; + } + + public int getSslHandshakeTimeoutMillis() { + return sslHandshakeTimeoutMillis; + } + + public SocketConfig setLinger(int seconds) { + this.linger = seconds; + return this; + } + + public int getLinger() { + return linger; + } +} diff --git a/net/src/main/java/org/xbib/net/URL.java b/net/src/main/java/org/xbib/net/URL.java new file mode 100755 index 0000000..a919cf4 --- /dev/null +++ b/net/src/main/java/org/xbib/net/URL.java @@ -0,0 +1,724 @@ +package org.xbib.net; + +import org.xbib.datastructures.common.Pair; +import org.xbib.net.scheme.Scheme; +import org.xbib.net.scheme.SchemeRegistry; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URI; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * + * A Uniform Resource Locator (URL) is a compact representation of the + * location and access method for a resource available via the Internet. + * + * Historically, there are many different forms of internet resource representations, for example, + * the URL (RFC 1738 as of 1994), the URI (RFC 2396 as of 1998), and IRI (RFC 3987 as of 2005), + * and most of them have updated specifications. + * + * {@link URL} is a Java implementation that serves as a universal point of handling all + * different forms. It follows the syntax of the Uniform Resource Identifier ({@code RFC 3986}) + * in accordance with the link: [{@code WHATWG} URL standard]. + * + * The reason for the name {@code URL} is merely because of the popularity of the name, which + * overweighs the URI or IRI popularity. + * + * [source,java] + * -- + * URL url = URL.http().resolveFromHost("google.com").build(); + * -- + * + */ +public class URL implements Comparable { + + static final URL NULL_URL = URL.builder().build(); + + static final char SEPARATOR_CHAR = '/'; + + static final char QUESTION_CHAR = '?'; + + static final char COLON_CHAR = ':'; + + static final char SEMICOLON_CHAR = ';'; + + static final char EQUAL_CHAR = '='; + + static final char AMPERSAND_CHAR = '&'; + + static final char NUMBER_SIGN_CHAR = '#'; + + static final char AT_CHAR = '@'; + + static final char LEFT_BRACKET_CHAR = '['; + + static final char RIGHT_BRACKET_CHAR = ']'; + + static final String DOUBLE_SLASH = "//"; + + private final transient URLBuilder builder; + + private final transient Scheme scheme; + + private final transient PercentEncoder queryParamEncoder; + + private final transient PercentEncoder regNameEncoder; + + private final transient PercentEncoder pathEncoder; + + private final transient PercentEncoder matrixEncoder; + + private final transient PercentEncoder fragmentEncoder; + + private final String hostinfo; + + private final String path; + + private final String query; + + private final String fragment; + + private String internalStringRepresentation; + + private String externalStringRepresentation; + + URL(URLBuilder builder) { + this.builder = builder; + this.scheme = SchemeRegistry.getInstance().getScheme(builder.scheme); + this.queryParamEncoder = PercentEncoders.getQueryParamEncoder(builder.charset); + this.regNameEncoder = PercentEncoders.getRegNameEncoder(builder.charset); + this.pathEncoder = PercentEncoders.getPathEncoder(builder.charset); + this.matrixEncoder = PercentEncoders.getMatrixEncoder(builder.charset); + this.fragmentEncoder = PercentEncoders.getFragmentEncoder(builder.charset); + this.hostinfo = encodeHostInfo(); + this.path = encodePath(); + this.query = encodeQuery(); + this.fragment = encodeFragment(); + } + + public static URLBuilder builder() { + return new URLBuilder(); + } + + public static URLBuilder http() { + return new URLBuilder().scheme(Scheme.HTTP); + } + + public static URLBuilder https() { + return new URLBuilder().scheme(Scheme.HTTPS); + } + + public static URLParser parser() { + return new URLParser(StandardCharsets.UTF_8, CodingErrorAction.REPORT); + } + + public static URLParser parser(Charset charset, CodingErrorAction codingErrorAction) { + return new URLParser(charset, codingErrorAction); + } + + public static URLResolver base(String base) { + return base(URL.create(base)); + } + + public static URLResolver base(URL base) { + return new URLResolver(base); + } + + /** + * Return a special URL denoting the fact that this URL should be considered as invalid. + * The URL has no scheme. + * @return url + */ + public static URL nullUrl() { + return NULL_URL; + } + + public static URL from(String input) { + return from(input, StandardCharsets.UTF_8, CodingErrorAction.REPORT, true, false); + } + + public static URL create(String input) { + return from(input, StandardCharsets.UTF_8, CodingErrorAction.REPORT, false, false); + } + + public static URL create(String input, boolean disableException) { + return from(input, StandardCharsets.UTF_8, CodingErrorAction.REPORT, false, disableException); + } + + public static URL from(String input, + Charset charset, CodingErrorAction codingErrorAction, + boolean resolve, boolean disableException) { + try { + return parser(charset, codingErrorAction).parse(input, resolve); + } catch (URLSyntaxException | MalformedInputException | UnmappableCharacterException e) { + if (disableException) { + return null; + } else { + throw new IllegalArgumentException(e); + } + } + } + + public URL resolve(String spec) { + return from(this, spec, false); + } + + public URL resolve(String spec, boolean disableException) { + return from(this, spec, disableException); + } + + public static URL from(URL base, String spec, boolean disableException) { + try { + return new URLResolver(base).resolve(spec); + } catch (URLSyntaxException | MalformedInputException | UnmappableCharacterException e) { + if (disableException) { + return null; + } else { + throw new IllegalArgumentException(e); + } + } + } + + public URL resolve(URL spec) { + return from(this, spec, false); + } + + public URL resolve(URL spec, boolean disableException) { + return from(this, spec, disableException); + } + + public static URL from(URL base, URL spec, boolean disableException) { + try { + return new URLResolver(base).resolve(spec); + } catch (URLSyntaxException e) { + if (disableException) { + return null; + } else { + throw new IllegalArgumentException(e); + } + } + } + + public String relativeReference() { + StringBuilder sb = new StringBuilder(); + if (path != null) { + sb.append(path); + } + if (query != null) { + sb.append(QUESTION_CHAR).append(query); + } + if (fragment != null) { + sb.append(NUMBER_SIGN_CHAR).append(fragment); + } + if (sb.length() == 0) { + sb.append(SEPARATOR_CHAR); + } + return sb.toString(); + } + + public static Parameter parseQueryString(String query) { + return parseQueryString(query, false); + } + + public static Parameter parseQueryString(String query, boolean disableException) { + Objects.requireNonNull(query); + try { + return URL.parser().parse(query.charAt(0) == QUESTION_CHAR ? query : QUESTION_CHAR + query).getQueryParams(); + } catch (URLSyntaxException | MalformedInputException | UnmappableCharacterException e) { + if (disableException) { + return null; + } else { + throw new IllegalArgumentException(e); + } + } + } + + public URLBuilder mutator() { + return builder; + } + + public URLBuilder newBuilder() { + return new URLBuilder(); + } + + private String decode(String input) { + try { + return builder.percentDecoder.decode(input); + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new IllegalArgumentException(e); + } + } + + private String toString(boolean withFragment) { + if (internalStringRepresentation != null) { + return internalStringRepresentation; + } + internalStringRepresentation = toInternalForm(withFragment); + return internalStringRepresentation; + } + + /** + * Gets the scheme of this {@code URL}. + * @return the scheme ('http' or 'file' or 'ftp' etc...) of the URL if it exists, or null. + */ + public String getScheme() { + return builder.scheme; + } + + /** + * Get the user info of this {@code URL}. + * @return the user info part if it exists. + */ + public String getUserInfo() { + return builder.userInfo; + } + + /** + * Get the user of the user info. + * @return the user + */ + public String getUser() { + if (builder.userInfo == null) { + return null; + } + Pair p = indexOf(COLON_CHAR, builder.userInfo); + return decode(p.getKey()); + } + + public String getPassword() { + if (builder.userInfo == null) { + return null; + } + Pair p = indexOf(COLON_CHAR, builder.userInfo); + return decode(p.getValue()); + } + + /** + * Get the host name ('www.example.com' or '192.168.0.1:8080' or '[fde2:d7de:302::]') of the {@code URL}. + * @return the host name + */ + public String getHost() { + return builder.host; + } + + /** + * Get the decoded host name. + * @return the decoded host name + */ + public String getDecodedHost() { + return decode(builder.host); + } + + public String getHostInfo() { + return hostinfo; + } + + public ProtocolVersion getProtocolVersion() { + return builder.protocolVersion; + } + + public Integer getPort() { + return builder.port; + } + + /** + * Get the path ('/path/to/my/file.html') of the {@code URL} if it exists. + * @return the path + */ + public String getPath() { + return path; + } + + /** + * Get the percent-decoded path of the {@code URL} if it exists. + * @return decoded path + */ + public String getDecodedPath() { + return decode(path); + } + + public List getPathSegments() { + return builder.pathSegments; + } + + /** + * Get the query ('?q=foo{@literal &}bar') of the {@code URL} if it exists. + * @return the query + */ + public String getQuery() { + return query; + } + + public String getDecodedQuery() { + return decode(query); + } + + public Parameter getQueryParams() { + return builder.queryParams.build(); + } + + /** + * @return the fragment ('#foo{@literal &}bar') of the URL if it exists. + */ + public String getFragment() { + return fragment; + } + + public String getDecodedFragment() { + return decode(fragment); + } + + /** + * @return the opaque part of the URL if it exists. + */ + public String getSchemeSpecificPart() { + return builder.schemeSpecificPart; + } + + /** + * @return true if URL is opaque. + */ + public boolean isOpaque() { + return !isNullOrEmpty(builder.scheme) && !isNullOrEmpty(builder.schemeSpecificPart) && builder.host == null; + } + + /** + * Whether this is a hierarchical URL or not. That is, a URL that allows multiple path segments. + * + * The term hierarchical comes form the URI standard + * (RFC 3986). + * Other libraries might refer to it as relative or cannot-be-a-base-URL. + * The later is the current WHATWG URL standard + * (see whatwg/url#89 for the rationale). + * @return true if URL is hierarchical + */ + public boolean isHierarchical() { + return !isOpaque(); + } + + /** + * @return true if URL is absolute. + */ + public boolean isAbsolute() { + return !isNullOrEmpty(builder.scheme); + } + + public boolean isRelative() { + return isNullOrEmpty(builder.scheme); + } + + public Comparator withFragmentComparator() { + return new URLWithFragmentComparator(); + } + + public Comparator withoutFragmentComparator() { + return new URLWithoutFragmentComparator(); + } + + public URL normalize() { + return scheme != null ? scheme.normalize(this) : this; + } + + public String toExternalForm() { + if (externalStringRepresentation != null) { + return externalStringRepresentation; + } + externalStringRepresentation = writeExternalForm(); + return externalStringRepresentation; + } + + public java.net.URL toURL() throws MalformedURLException { + return new java.net.URL(toString()); + } + + public URI toURI() { + return URI.create(toString()); + } + + public Path toPath() { + return Paths.get(toURI()); + } + + public InputStream openStream() throws IOException { + return toURL().openStream(); + } + + private String toInternalForm(boolean withFragment) { + StringBuilder sb = new StringBuilder(); + if (!isNullOrEmpty(builder.scheme)) { + sb.append(builder.scheme).append(COLON_CHAR); + } + if (isOpaque()) { + sb.append(builder.schemeSpecificPart); + } else { + appendHostInfo(sb, false, true); + appendPath(sb, false); + appendQuery(sb, false, true); + if (withFragment) { + appendFragment(sb, false, true); + } + } + return sb.toString(); + } + + private String writeExternalForm() { + StringBuilder sb = new StringBuilder(); + if (!isNullOrEmpty(builder.scheme)) { + sb.append(builder.scheme).append(COLON_CHAR); + } + if (isOpaque()) { + sb.append(builder.schemeSpecificPart); + } else { + appendHostInfo(sb, true, true); + appendPath(sb, true); + appendQuery(sb, true, true); + appendFragment(sb, true, true); + } + return sb.toString(); + } + + private String encodeHostInfo() { + StringBuilder sb = new StringBuilder(); + appendHostInfo(sb, true, false); + return sb.toString(); + } + + private void appendHostInfo(StringBuilder sb, boolean encoded, boolean withSlash) { + if (builder.host == null) { + return; + } + if (withSlash) { + if (scheme != null) { + sb.append(DOUBLE_SLASH); + } else { + sb.append(SEPARATOR_CHAR); + } + } + if (!builder.host.isEmpty()) { + if (!isNullOrEmpty(builder.userInfo)) { + sb.append(builder.userInfo).append(AT_CHAR); + } + if (builder.protocolVersion != null) { + switch (builder.protocolVersion) { + case IPV6: + String s = "localhost".equals(builder.host) ? + InetAddress.getLoopbackAddress().getHostAddress() : builder.host; + // prefer host name over numeric address + if (s != null && !s.equals(builder.hostAddress)) { + sb.append(s); + } else if (builder.hostAddress != null) { + sb.append(LEFT_BRACKET_CHAR).append(builder.hostAddress).append(RIGHT_BRACKET_CHAR); + } + break; + case IPV4: + sb.append(builder.host); + break; + default: + if (encoded) { + try { + String encodedHostName = regNameEncoder.encode(builder.host); + validateHostnameCharacters(encodedHostName); + sb.append(encodedHostName); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } else { + sb.append(builder.host); + } + break; + } + } else { + if (encoded) { + try { + String encodedHostName = regNameEncoder.encode(builder.host); + validateHostnameCharacters(encodedHostName); + sb.append(encodedHostName); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } else { + sb.append(builder.host); + } + } + if (scheme != null && builder.port != null && builder.port != scheme.getDefaultPort()) { + sb.append(COLON_CHAR); + if (builder.port != -1) { + sb.append(builder.port); + } + } + } + } + + private void validateHostnameCharacters(String hostname) { + boolean valid; + for (int i = 0; i < hostname.length(); i++) { + char c = hostname.charAt(i); + valid = ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || + ('0' <= c && c <= '9') || + c == '-' || c == '.' || c == '_' || c == '~' || + c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || + c == '*' || c == '+' || c == ',' || c == ';' || c == '=' || c == '%'; + if (!valid) { + throw new IllegalArgumentException("invalid host name character in: " + hostname); + } + } + } + + private String encodePath() { + StringBuilder sb = new StringBuilder(); + appendPath(sb, true); + return sb.toString(); + } + + private void appendPath(StringBuilder sb, boolean encoded) { + Iterator it = builder.pathSegments.iterator(); + while (it.hasNext()) { + URLBuilder.PathSegment pathSegment = it.next(); + try { + sb.append(encoded ? pathEncoder.encode(pathSegment.getSegment()) : pathSegment.getSegment()); + for (Pair matrixParam : pathSegment.getMatrixParams()) { + sb.append(SEMICOLON_CHAR).append(encoded ? + matrixEncoder.encode(matrixParam.getKey()) : matrixParam.getKey()); + if (matrixParam.getValue() != null) { + sb.append(EQUAL_CHAR).append(encoded ? + matrixEncoder.encode(matrixParam.getValue()) : matrixParam.getValue()); + } + } + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + if (it.hasNext()) { + sb.append(SEPARATOR_CHAR); + } + } + } + + private String encodeQuery() { + StringBuilder sb = new StringBuilder(); + appendQuery(sb, true, false); + return sb.length() == 0 ? null : sb.toString(); + } + + private void appendQuery(StringBuilder sb, boolean withEncoding, boolean withQuestionMark) { + // a given query has priority + if (!isNullOrEmpty(builder.query)) { + if (withQuestionMark) { + sb.append(QUESTION_CHAR); + } + // ignore encoding, the query string must already be encoded + sb.append(builder.query); + } else if (builder.queryParams != null && !builder.queryParams.isEmpty()) { + if (withQuestionMark) { + sb.append(QUESTION_CHAR); + } + Iterator> it = builder.queryParams.iterator(); + while (it.hasNext()) { + Pair queryParam = it.next(); + try { + String k = withEncoding ? queryParamEncoder.encode(queryParam.getKey()) : queryParam.getKey(); + sb.append(k); + if (queryParam.getValue() != null) { + Object v = withEncoding && queryParam.getValue() instanceof CharSequence ? + queryParamEncoder.encode((CharSequence) queryParam.getValue()) : queryParam.getValue(); + sb.append(EQUAL_CHAR).append(v); + } + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + if (it.hasNext()) { + sb.append(AMPERSAND_CHAR); + } + } + } + } + + private String encodeFragment() { + StringBuilder sb = new StringBuilder(); + appendFragment(sb, true, false); + return sb.length() == 0 ? null : sb.toString(); + } + + private void appendFragment(StringBuilder sb, boolean encoded, boolean withHashSymbol) { + if (!isNullOrEmpty(builder.fragment)) { + if (withHashSymbol) { + sb.append(NUMBER_SIGN_CHAR); + } + if (encoded) { + try { + sb.append(fragmentEncoder.encode(builder.fragment)); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + } else { + sb.append(builder.fragment); + } + } + } + + /** + * Returns true if the parameter string is neither null nor empty. + */ + static boolean isNullOrEmpty(String str) { + return str == null || str.isEmpty(); + } + + static Pair indexOf(char ch, String input) { + int i = input.indexOf(ch); + String k = i >= 0 ? input.substring(0, i) : input; + String v = i >= 0 ? input.substring(i + 1) : null; + return Pair.of(k, v); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public boolean equals(Object other) { + return other instanceof URL && toString().equals(other.toString()); + } + + @Override + public String toString() { + return toString(true); + } + + @Override + public int compareTo(URL o) { + return toString().compareTo(o.toString()); + } + + @SuppressWarnings("serial") + private static class URLWithFragmentComparator implements Comparator { + + @Override + public int compare(URL o1, URL o2) { + return o1.toString(true).compareTo(o2.toString(true)); + } + } + + @SuppressWarnings("serial") + private static class URLWithoutFragmentComparator implements Comparator { + + @Override + public int compare(URL o1, URL o2) { + return o1.toString(false).compareTo(o2.toString(false)); + } + } +} diff --git a/net/src/main/java/org/xbib/net/URLBuilder.java b/net/src/main/java/org/xbib/net/URLBuilder.java new file mode 100644 index 0000000..bf499fd --- /dev/null +++ b/net/src/main/java/org/xbib/net/URLBuilder.java @@ -0,0 +1,332 @@ +package org.xbib.net; + +import java.net.IDN; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import org.xbib.datastructures.common.Pair; + +/** + * The URL builder class is required for building an URL. It uses fluent API methods + * and pre-processes parameter accordingly. + */ +public class URLBuilder { + + private static final String EMPTY = ""; + + static final PathSegment EMPTY_SEGMENT = new PathSegment(EMPTY); + + PercentEncoder regNameEncoder; + + PercentEncoder percentEncoder; + + PercentDecoder percentDecoder; + + ParameterBuilder queryParams; + + final List pathSegments; + + Charset charset; + + CodingErrorAction codingErrorAction; + + String scheme; + + String schemeSpecificPart; + + String userInfo; + + String host; + + String hostAddress; + + ProtocolVersion protocolVersion; + + Integer port; + + String query; + + String fragment; + + boolean fatalResolveErrorsEnabled; + + URLBuilder() { + this.pathSegments = new ArrayList<>(); + charset(StandardCharsets.UTF_8, CodingErrorAction.REPLACE); + } + + /** + * Set the character set of the URL. Default is UTF-8. + * + * @param charset the character set + * @param codingErrorAction the coding error action + * @return this builder + */ + public URLBuilder charset(Charset charset, CodingErrorAction codingErrorAction) { + this.charset = charset; + this.codingErrorAction = codingErrorAction; + this.percentEncoder = PercentEncoders.getQueryEncoder(charset); + this.regNameEncoder = PercentEncoders.getRegNameEncoder(charset); + CharsetDecoder charsetDecoder = charset.newDecoder() + .onMalformedInput(codingErrorAction) + .onUnmappableCharacter(codingErrorAction); + this.percentDecoder = new PercentDecoder(charsetDecoder); + this.queryParams = Parameter.builder(); + return this; + } + + public URLBuilder scheme(String scheme) { + if (!URL.isNullOrEmpty(scheme)) { + validateSchemeCharacters(scheme.toLowerCase(Locale.ROOT)); + this.scheme = scheme; + } + return this; + } + + public String scheme() { + return scheme; + } + + public URLBuilder schemeSpecificPart(String schemeSpecificPart) { + this.schemeSpecificPart = schemeSpecificPart; + return this; + } + + public URLBuilder userInfo(String userInfo) { + this.userInfo = userInfo; + return this; + } + + public URLBuilder userInfo(String user, String pass) { + try { + // allow colons in usernames and passwords by percent-encoding them here + this.userInfo = regNameEncoder.encode(user) + URL.COLON_CHAR + regNameEncoder.encode(pass); + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new IllegalArgumentException(e); + } + return this; + } + + public URLBuilder host(String host) { + if (host != null) { + this.host = host.toLowerCase(Locale.ROOT); + } + this.protocolVersion = ProtocolVersion.NONE; + return this; + } + + public URLBuilder host(String host, ProtocolVersion protocolVersion) { + if (host != null) { + this.host = host.toLowerCase(Locale.ROOT); + } + this.protocolVersion = protocolVersion; + return this; + } + + public String host() { + return host; + } + + public URLBuilder fatalResolveErrors(boolean fatalResolveErrorsEnabled) { + this.fatalResolveErrorsEnabled = fatalResolveErrorsEnabled; + return this; + } + + public URLBuilder resolveFromHost(String hostname) { + if (hostname == null) { + return this; + } + if (hostname.isEmpty()) { + host(EMPTY); + return this; + } + try { + InetAddress inetAddress = InetAddress.getByName(hostname); + hostAddress = inetAddress.getHostAddress(); + host(inetAddress.getHostName(), inetAddress instanceof Inet6Address ? + ProtocolVersion.IPV6 : inetAddress instanceof Inet4Address ? + ProtocolVersion.IPV4 : ProtocolVersion.NONE); + return this; + } catch (UnknownHostException e) { + if (fatalResolveErrorsEnabled) { + throw new IllegalStateException(e); + } + if (e.getMessage() != null && !e.getMessage().endsWith("invalid IPv6 address") && + hostname.charAt(0) != URL.LEFT_BRACKET_CHAR && + hostname.charAt(hostname.length() - 1) != URL.RIGHT_BRACKET_CHAR) { + try { + String idna = IDN.toASCII(percentDecoder.decode(hostname)); + host(idna, ProtocolVersion.NONE); + } catch (CharacterCodingException e2) { + throw new IllegalArgumentException(e2); + } + } + } + return this; + } + + public URLBuilder port(Integer port) { + this.port = port; + return this; + } + + public Integer port() { + return port; + } + + public URLBuilder path(String path) { + try { + URL.parser(charset, codingErrorAction).parsePathWithQueryAndFragment(this, path); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException(e); + } + return this; + } + + public URLBuilder pathSegments(String... segments) { + for (String segment : segments) { + pathSegment(segment); + } + return this; + } + + public URLBuilder pathSegment(String segment) { + if (pathSegments.isEmpty() && !URL.isNullOrEmpty(host) && !URL.isNullOrEmpty(segment)) { + pathSegments.add(EMPTY_SEGMENT); + } + pathSegments.add(new PathSegment(segment)); + return this; + } + + /** + * Add a query parameter. Query parameters will be encoded in the order added. + *

    + * Using query strings to encode key=value pairs is not part of the URI/URL specification. + * It is specified by http://www.w3.org/TR/html401/interact/forms.html#form-content-type. + *

    + * If you use this method to build a query string, or created this builder from an URL with a query string that can + * successfully be parsed into query param pairs, you cannot subsequently use + * {@link URLBuilder#query(String)}. + * + * @param name param name + * @param value param value + * @return this + */ + public URLBuilder queryParam(String name, Object value) { + queryParams.add(name, value); + return this; + } + + /** + * Set the complete and encoded query string of arbitrary structure. This is useful when you want to specify a query string that + * is not of key=value format. If the query has previously been set via this method, subsequent calls will overwrite + * that query. + * If you use this method, or create a builder from a URL whose query is not parseable into query param pairs, you + * cannot subsequently use {@link URLBuilder#queryParam(String, Object)}. + * + * @param query complete and encoded URI query, as specified by https://tools.ietf.org/html/rfc3986#section-3.4 + * @return this + */ + public URLBuilder query(String query) { + this.query = query; + return this; + } + + /** + * Add a matrix param to the last added path segment. If no segments have been added, the param will be added to the + * root. Matrix params will be encoded in the order added. + * + * @param name param name + * @param value param value + * @return this + */ + public URLBuilder matrixParam(String name, String value) { + if (pathSegments.isEmpty()) { + pathSegment(EMPTY); + } + pathSegments.get(pathSegments.size() - 1).getMatrixParams().add(Pair.of(name, value)); + return this; + } + + /** + * Set the fragment. + * + * @param fragment fragment string + * @return this + */ + public URLBuilder fragment(String fragment) { + if (!URL.isNullOrEmpty(fragment)) { + this.fragment = fragment; + } + return this; + } + + public URL build() { + return new URL(this); + } + + /** + * Encode the current builder state into a string. + * + * @return a string + */ + String toUrlString() { + return build().toExternalForm(); + } + + void validateSchemeCharacters(String scheme) { + boolean valid; + for (int i = 0; i < scheme.length(); i++) { + char c = scheme.charAt(i); + if (i == 0) { + valid = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'); + } else { + valid = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || + ('0' <= c && c <= '9') || c == '+' || c == '-' || c == '.'; + } + if (!valid) { + throw new IllegalArgumentException("invalid scheme character in: " + scheme); + } + } + } + + + /** + * A path segment with associated matrix params, if any. + */ + public static class PathSegment { + + private final String segment; + + private final List> params; + + PathSegment(String segment) { + this.segment = segment; + this.params = new ArrayList<>(); + } + + public String getSegment() { + return segment; + } + + public List> getMatrixParams() { + return params; + } + + @Override + public String toString() { + return segment + ";" + params; + } + } +} diff --git a/net/src/main/java/org/xbib/net/URLParser.java b/net/src/main/java/org/xbib/net/URLParser.java new file mode 100644 index 0000000..5786284 --- /dev/null +++ b/net/src/main/java/org/xbib/net/URLParser.java @@ -0,0 +1,224 @@ +package org.xbib.net; + +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import org.xbib.datastructures.common.Pair; +import org.xbib.net.scheme.Scheme; +import org.xbib.net.scheme.SchemeRegistry; + +/** + * A URL parser class. + */ +public class URLParser { + + private static final String EMPTY = ""; + + private final URLBuilder builder; + + URLParser(Charset charset, CodingErrorAction codingErrorAction) { + builder = new URLBuilder(); + builder.charset(charset, codingErrorAction); + } + + public URL parse(String input) + throws URLSyntaxException, MalformedInputException, UnmappableCharacterException { + return parse(input, true); + } + + public URL parse(String input, boolean resolve) + throws URLSyntaxException, MalformedInputException, UnmappableCharacterException { + if (URL.isNullOrEmpty(input)) { + return URL.NULL_URL; + } + if (input.indexOf('\n') >= 0) { + return URL.NULL_URL; + } + if (input.indexOf('\t') >= 0) { + return URL.NULL_URL; + } + String remaining = parseScheme(builder, input); + if (remaining != null) { + remaining = remaining.replace('\\', URL.SEPARATOR_CHAR); + builder.schemeSpecificPart(remaining); + if (remaining.startsWith(URL.DOUBLE_SLASH)) { + Scheme scheme = SchemeRegistry.getInstance().getScheme(builder.scheme); + if (builder.scheme == null || scheme.getDefaultPort() == -1) { + builder.host(EMPTY); + } else { + remaining = remaining.substring(2); + int i = remaining.indexOf(URL.SEPARATOR_CHAR); + int j = remaining.indexOf(URL.QUESTION_CHAR); + int pos = i >= 0 && j >= 0 ? Math.min(i, j) : i >= 0 ? i : j >= 0 ? j : -1; + String host = (pos >= 0 ? remaining.substring(0, pos) : remaining); + parseHostAndPort(builder, parseUserInfo(builder, host), resolve); + if (builder.host == null) { + return URL.NULL_URL; + } + remaining = pos >= 0 ? remaining.substring(pos) : EMPTY; + } + } + if (!URL.isNullOrEmpty(remaining)) { + try { + parsePathWithQueryAndFragment(builder, remaining); + } catch (CharacterCodingException e) { + throw new URLSyntaxException(e); + } + } + } + return builder.build(); + } + + String parseScheme(URLBuilder builder, String input) { + Pair p = URL.indexOf(URL.COLON_CHAR, input); + if (p.getValue() == null) { + return input; + } + if (!URL.isNullOrEmpty(p.getKey())) { + builder.scheme(p.getKey()); + } + return p.getValue(); + } + + String parseUserInfo(URLBuilder builder, String input) + throws MalformedInputException, UnmappableCharacterException { + String remaining = input; + int i = input.lastIndexOf(URL.AT_CHAR); + if (i > 0) { + remaining = input.substring(i + 1); + String userInfo = input.substring(0, i); + builder.userInfo(builder.percentDecoder.decode(userInfo)); + } + return remaining; + } + + void parseHostAndPort(URLBuilder builder, String rawHost, boolean resolve) + throws URLSyntaxException { + String host = rawHost; + if (host.indexOf(URL.LEFT_BRACKET_CHAR) == 0) { + int i = host.lastIndexOf(URL.RIGHT_BRACKET_CHAR); + if (i >= 0) { + builder.port(parsePort(host.substring(i + 1))); + host = host.substring(1, i); + } + } else { + int i = host.indexOf(URL.COLON_CHAR); + if (i >= 0) { + builder.port(parsePort(host.substring(i))); + host = host.substring(0, i); + } + } + if (resolve) { + builder.resolveFromHost(host); + } else { + builder.host(host); + } + } + + Integer parsePort(String portStr) throws URLSyntaxException { + if (portStr == null || portStr.isEmpty()) { + return null; + } + int i = portStr.indexOf(URL.COLON_CHAR); + if (i >= 0) { + portStr = portStr.substring(i + 1); + if (portStr.isEmpty()) { + return -1; + } + } + try { + int port = Integer.parseInt(portStr); + if (port > 0 && port < 65536) { + return port; + } else { + throw new URLSyntaxException("invalid port"); + } + } catch (NumberFormatException e) { + throw new URLSyntaxException("no numeric port: " + portStr); + } + } + + void parsePathWithQueryAndFragment(URLBuilder builder, String inputStr) + throws MalformedInputException, UnmappableCharacterException { + String input = inputStr; + if (input == null) { + return; + } + int i = input.lastIndexOf(URL.NUMBER_SIGN_CHAR); + if (i >= 0) { + builder.fragment(builder.percentDecoder.decode(input.substring(i + 1))); + input = input.substring(0, i); + } + i = input.indexOf(URL.QUESTION_CHAR); + if (i >= 0) { + parseQuery(builder, input.substring(i + 1)); + input = input.substring(0, i); + } + if (input.length() > 0 && input.charAt(0) == URL.SEPARATOR_CHAR) { + builder.pathSegment(EMPTY); + } + String s = input; + while (s != null) { + Pair pair = URL.indexOf(URL.SEPARATOR_CHAR, s); + String elem = pair.getKey(); + if (!elem.isEmpty()) { + if (elem.charAt(0) == URL.SEMICOLON_CHAR) { + builder.pathSegment(EMPTY); + String t = elem.substring(1); + while (t != null) { + Pair pathWithMatrixElem = URL.indexOf(URL.SEMICOLON_CHAR, t); + String matrixElem = pathWithMatrixElem.getKey(); + Pair p = URL.indexOf(URL.EQUAL_CHAR, matrixElem); + builder.matrixParam(builder.percentDecoder.decode(p.getKey()), + builder.percentDecoder.decode(p.getValue())); + t = pathWithMatrixElem.getValue(); + } + } else { + String t = elem; + i = 0; + while (t != null) { + Pair pathWithMatrixElem = URL.indexOf(URL.SEMICOLON_CHAR, t); + String segment = pathWithMatrixElem.getKey(); + if (i == 0) { + builder.pathSegment(builder.percentDecoder.decode(segment)); + } else { + Pair p = URL.indexOf(URL.EQUAL_CHAR, segment); + builder.matrixParam(builder.percentDecoder.decode(p.getKey()), + builder.percentDecoder.decode(p.getValue())); + } + t = pathWithMatrixElem.getValue(); + i++; + } + } + } + s = pair.getValue(); + } + if (input.endsWith("/")) { + builder.pathSegment(EMPTY); + } + } + + void parseQuery(URLBuilder builder, String query) + throws MalformedInputException, UnmappableCharacterException { + if (query == null) { + return; + } + String s = query; + while (s != null) { + Pair p = URL.indexOf(URL.AMPERSAND_CHAR, s); + Pair param = URL.indexOf(URL.EQUAL_CHAR, p.getKey()); + if (!URL.isNullOrEmpty(param.getKey())) { + builder.queryParam(builder.percentDecoder.decode(param.getKey()), + builder.percentDecoder.decode(param.getValue())); + } + s = p.getValue(); + } + if (builder.queryParams.isEmpty()) { + builder.query(builder.percentDecoder.decode(query)); + } else { + builder.query(query); + } + } +} diff --git a/net/src/main/java/org/xbib/net/URLResolver.java b/net/src/main/java/org/xbib/net/URLResolver.java new file mode 100644 index 0000000..53fb971 --- /dev/null +++ b/net/src/main/java/org/xbib/net/URLResolver.java @@ -0,0 +1,136 @@ +package org.xbib.net; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import java.util.ArrayList; +import java.util.List; + +/** + * The URL resolver class is a class for resolving a relative URL specification to a base URL. + */ +public class URLResolver { + + private static final String EMPTY = ""; + + private final URL base; + + URLResolver(URL base) { + this.base = base; + } + + public URL resolve(String relative) + throws URLSyntaxException, MalformedInputException, UnmappableCharacterException { + if (relative == null) { + return null; + } + if (relative.isEmpty()) { + return base; + } + // TODO(jprante) parser(charset, codingErrorAction) + URL url = URL.parser().parse(relative); + return resolve(url); + } + + public URL resolve(URL relative) + throws URLSyntaxException { + if (relative == null || relative.equals(URL.NULL_URL)) { + throw new URLSyntaxException("relative URL is invalid"); + } + if (!base.isAbsolute()) { + throw new URLSyntaxException("base URL is not absolute"); + } + URLBuilder builder = new URLBuilder(); + if (relative.isOpaque()) { + builder.scheme(relative.getScheme()); + builder.schemeSpecificPart(relative.getSchemeSpecificPart()); + return builder.build(); + } + if (relative.isAbsolute()) { + builder.scheme(relative.getScheme()); + } else { + builder.scheme(base.getScheme()); + } + if (!URL.isNullOrEmpty(relative.getScheme()) || !URL.isNullOrEmpty(relative.getHost())) { + builder.host(relative.getDecodedHost(), relative.getProtocolVersion()).port(relative.getPort()); + builder.path(relative.getPath()); + return builder.build(); + } + if (base.isOpaque()) { + builder.schemeSpecificPart(base.getSchemeSpecificPart()); + return builder.build(); + } + if (relative.getHost() != null) { + builder.host(relative.getDecodedHost(), relative.getProtocolVersion()).port(relative.getPort()); + } else { + builder.host(base.getDecodedHost(), base.getProtocolVersion()).port(base.getPort()); + } + builder.path(resolvePath(base, relative)); + return builder.build(); + } + + private String resolvePath(URL base, URL relative) { + String basePath = base.getPath(); + String baseQuery = base.getQuery(); + String baseFragment = base.getFragment(); + String relPath = relative.getPath(); + String relQuery = relative.getQuery(); + String relFragment = relative.getFragment(); + boolean isBase = false; + String merged; + List result = new ArrayList<>(); + if (URL.isNullOrEmpty(relPath)) { + merged = basePath; + isBase = true; + } else if (relPath.charAt(0) != URL.SEPARATOR_CHAR && !URL.isNullOrEmpty(basePath)) { + merged = basePath.substring(0, basePath.lastIndexOf(URL.SEPARATOR_CHAR) + 1) + relPath; + } else { + merged = relPath; + } + if (URL.isNullOrEmpty(merged)) { + return EMPTY; + } + String[] parts = merged.split("/", -1); + for (String part : parts) { + switch (part) { + case EMPTY: + case ".": + break; + case "..": + if (result.size() > 0) { + result.remove(result.size() - 1); + } + break; + default: + result.add(part); + break; + } + } + if (parts.length > 0) { + switch (parts[parts.length - 1]) { + case EMPTY: + case ".": + case "..": + result.add(EMPTY); + break; + default: + break; + } + } + StringBuilder sb = new StringBuilder(); + sb.append(String.join(Character.toString(URL.SEPARATOR_CHAR), result)); + if (sb.length() == 0 && result.size() == 1) { + sb.append(URL.SEPARATOR_CHAR); + } + if (!URL.isNullOrEmpty(relQuery)) { + sb.append(URL.QUESTION_CHAR).append(relQuery); + } else if (isBase && !URL.isNullOrEmpty(baseQuery)) { + sb.append(URL.QUESTION_CHAR).append(baseQuery); + } + if (!URL.isNullOrEmpty(relFragment)) { + sb.append(URL.NUMBER_SIGN_CHAR).append(relFragment); + } else if (isBase && !URL.isNullOrEmpty(baseFragment)) { + sb.append(URL.NUMBER_SIGN_CHAR).append(baseFragment); + } + return sb.toString(); + } +} diff --git a/net/src/main/java/org/xbib/net/URLSyntaxException.java b/net/src/main/java/org/xbib/net/URLSyntaxException.java new file mode 100644 index 0000000..2f0a91f --- /dev/null +++ b/net/src/main/java/org/xbib/net/URLSyntaxException.java @@ -0,0 +1,17 @@ +package org.xbib.net; + +/** + * URL syntax exception. + */ +@SuppressWarnings("serial") +public class URLSyntaxException extends Exception { + + URLSyntaxException(String message) { + super(message); + } + + URLSyntaxException(Throwable cause) { + super(cause); + } + +} diff --git a/net/src/main/java/org/xbib/net/UserDetails.java b/net/src/main/java/org/xbib/net/UserDetails.java new file mode 100644 index 0000000..b477f90 --- /dev/null +++ b/net/src/main/java/org/xbib/net/UserDetails.java @@ -0,0 +1,45 @@ +package org.xbib.net; + +public final class UserDetails { + + private String name; + + private String userId; + + private String effectiveUserId; + + public UserDetails() { + this.name = ""; + this.userId = ""; + this.effectiveUserId = ""; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getUserId() { + return userId; + } + + public void setEffectiveUserId(String effectiveUserId) { + this.effectiveUserId = effectiveUserId; + } + + public String getEffectiveUserId() { + return effectiveUserId; + } + + @Override + public String toString() { + return "UserDetails{" + "name='" + name + ',' + ",userId='" + userId + ",effectiveUserId=" + effectiveUserId + '}'; + } +} diff --git a/net/src/main/java/org/xbib/net/UserProfile.java b/net/src/main/java/org/xbib/net/UserProfile.java new file mode 100644 index 0000000..1bf7561 --- /dev/null +++ b/net/src/main/java/org/xbib/net/UserProfile.java @@ -0,0 +1,59 @@ +package org.xbib.net; + +import java.util.List; + +public interface UserProfile { + + boolean isLoggedIn(); + + void setUserId(String uid); + + String getUserId(); + + void setEffectiveUserId(String eid); + + String getEffectiveUserId(); + + void setName(String name); + + String getName(); + + void addRole(String role); + + void addEffectiveRole(String role); + + List getRoles(); + + List getEffectiveRoles(); + + boolean hasRole(String role); + + boolean hasEffectiveRole(String role); + + boolean hasAnyRole(String[] expectedRoles); + + boolean hasAnyEffectiveRole(String[] expectedRoles); + + boolean hasAllRoles(String[] expectedRoles); + + boolean hasAllEffectiveRoles(String[] expectedRoles); + + void addPermission(String permission); + + void removePermission(String permission); + + List getPermissions(); + + List getEffectivePermissions(); + + Attributes attributes(); + + Attributes effectiveAttributes(); + + void setRemembered(boolean remembered); + + boolean isRemembered(); + + boolean hasAccess(String requireAnyRole, String requireAllRoles); + +} diff --git a/net/src/main/java/org/xbib/net/UsersProvider.java b/net/src/main/java/org/xbib/net/UsersProvider.java new file mode 100644 index 0000000..362f94e --- /dev/null +++ b/net/src/main/java/org/xbib/net/UsersProvider.java @@ -0,0 +1,35 @@ +package org.xbib.net; + +public abstract class UsersProvider { + + public UsersProvider() { + } + + /** + * Override this method in order to load user details. + * + * @return the user, or null if user doesn't exist + * @throws RuntimeException in case of unexpected error such as connection failure + */ + public abstract UserDetails getUserDetails(Context context); + + public static final class Context { + + private final String username; + + private final Request request; + + public Context(String username, Request request) { + this.username = username; + this.request = request; + } + + public String getUsername() { + return username; + } + + public Request getRequest() { + return request; + } + } +} diff --git a/net/src/main/java/org/xbib/net/buffer/DataBuffer.java b/net/src/main/java/org/xbib/net/buffer/DataBuffer.java new file mode 100644 index 0000000..a3b23fb --- /dev/null +++ b/net/src/main/java/org/xbib/net/buffer/DataBuffer.java @@ -0,0 +1,340 @@ +package org.xbib.net.buffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.util.Objects; +import java.util.function.IntPredicate; + +/** + * Basic abstraction over byte buffers. + * + *

    {@code DataBuffer}s has a separate {@linkplain #readPosition() read} and + * {@linkplain #writePosition() write} position, as opposed to {@code ByteBuffer}'s + * single {@linkplain ByteBuffer#position() position}. As such, the {@code DataBuffer} + * does not require a {@linkplain ByteBuffer#flip() flip} to read after writing. In general, + * the following invariant holds for the read and write positions, and the capacity: + * + *

    + * 0 <= + * readPosition <= + * writePosition <= + * capacity + *
    + * + *

    The {@linkplain #capacity() capacity} of a {@code DataBuffer} is expanded on demand, + * similar to {@code StringBuilder}. + */ +public interface DataBuffer { + + /** + * Return the {@link DataBufferFactory} that created this buffer. + * @return the creating buffer factory + */ + DataBufferFactory factory(); + + /** + * Return the index of the first byte in this buffer that matches + * the given predicate. + * @param predicate the predicate to match + * @param fromIndex the index to start the search from + * @return the index of the first byte that matches {@code predicate}; + * or {@code -1} if none match + */ + int indexOf(IntPredicate predicate, int fromIndex); + + /** + * Return the index of the last byte in this buffer that matches + * the given predicate. + * @param predicate the predicate to match + * @param fromIndex the index to start the search from + * @return the index of the last byte that matches {@code predicate}; + * or {@code -1} if none match + */ + int lastIndexOf(IntPredicate predicate, int fromIndex); + + /** + * Return the number of bytes that can be read from this data buffer. + * @return the readable byte count + */ + int readableByteCount(); + + /** + * Return the number of bytes that can be written to this data buffer. + * @return the writable byte count + */ + int writableByteCount(); + + /** + * Return the number of bytes that this buffer can contain. + * @return the capacity + */ + int capacity(); + + /** + * Set the number of bytes that this buffer can contain. + *

    If the new capacity is lower than the current capacity, the contents + * of this buffer will be truncated. If the new capacity is higher than + * the current capacity, it will be expanded. + * @param capacity the new capacity + * @return this buffer + */ + DataBuffer capacity(int capacity); + + /** + * Ensure that the current buffer has enough {@link #writableByteCount()} + * to write the amount of data given as an argument. If not, the missing + * capacity will be added to the buffer. + * @param capacity the writable capacity to check for + * @return this buffer + */ + default DataBuffer ensureCapacity(int capacity) { + return this; + } + + /** + * Return the position from which this buffer will read. + * @return the read position + */ + int readPosition(); + + /** + * Set the position from which this buffer will read. + * @param readPosition the new read position + * @return this buffer + * @throws IndexOutOfBoundsException if {@code readPosition} is smaller than 0 + * or greater than {@link #writePosition()} + */ + DataBuffer readPosition(int readPosition); + + /** + * Return the position to which this buffer will write. + * @return the write position + */ + int writePosition(); + + /** + * Set the position to which this buffer will write. + * @param writePosition the new write position + * @return this buffer + * @throws IndexOutOfBoundsException if {@code writePosition} is smaller than + * {@link #readPosition()} or greater than {@link #capacity()} + */ + DataBuffer writePosition(int writePosition); + + /** + * Read a single byte at the given index from this data buffer. + * @param index the index at which the byte will be read + * @return the byte at the given index + * @throws IndexOutOfBoundsException when {@code index} is out of bounds + */ + byte getByte(int index); + + /** + * Read a single byte from the current reading position from this data buffer. + * @return the byte at this buffer's current reading position + */ + byte read(); + + /** + * Read this buffer's data into the specified destination, starting at the current + * reading position of this buffer. + * @param destination the array into which the bytes are to be written + * @return this buffer + */ + DataBuffer read(byte[] destination); + + /** + * Read at most {@code length} bytes of this buffer into the specified destination, + * starting at the current reading position of this buffer. + * @param destination the array into which the bytes are to be written + * @param offset the index within {@code destination} of the first byte to be written + * @param length the maximum number of bytes to be written in {@code destination} + * @return this buffer + */ + DataBuffer read(byte[] destination, int offset, int length); + + /** + * Write a single byte into this buffer at the current writing position. + * @param b the byte to be written + * @return this buffer + */ + DataBuffer write(byte b); + + /** + * Write the given source into this buffer, starting at the current writing position + * of this buffer. + * @param source the bytes to be written into this buffer + * @return this buffer + */ + DataBuffer write(byte[] source); + + /** + * Write at most {@code length} bytes of the given source into this buffer, starting + * at the current writing position of this buffer. + * @param source the bytes to be written into this buffer + * @param offset the index within {@code source} to start writing from + * @param length the maximum number of bytes to be written from {@code source} + * @return this buffer + */ + DataBuffer write(byte[] source, int offset, int length); + + /** + * Write one or more {@code DataBuffer}s to this buffer, starting at the current + * writing position. It is the responsibility of the caller to + * {@linkplain DataBufferUtil#release(DataBuffer) release} the given data buffers. + * @param buffers the byte buffers to write into this buffer + * @return this buffer + */ + DataBuffer write(DataBuffer... buffers); + + /** + * Write one or more {@link ByteBuffer} to this buffer, starting at the current + * writing position. + * @param buffers the byte buffers to write into this buffer + * @return this buffer + */ + DataBuffer write(ByteBuffer... buffers); + + /** + * Write the given {@code CharSequence} using the given {@code Charset}, + * starting at the current writing position. + * @param charSequence the char sequence to write into this buffer + * @param charset the charset to encode the char sequence with + * @return this buffer + */ + default DataBuffer write(CharSequence charSequence, Charset charset) { + Objects.requireNonNull(charSequence, "CharSequence must not be null"); + Objects.requireNonNull(charset, "Charset must not be null"); + if (charSequence.length() != 0) { + CharsetEncoder charsetEncoder = charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + CharBuffer inBuffer = CharBuffer.wrap(charSequence); + int estimatedSize = (int) (inBuffer.remaining() * charsetEncoder.averageBytesPerChar()); + ByteBuffer outBuffer = ensureCapacity(estimatedSize) + .asByteBuffer(writePosition(), writableByteCount()); + while (true) { + CoderResult cr = (inBuffer.hasRemaining() ? + charsetEncoder.encode(inBuffer, outBuffer, true) : CoderResult.UNDERFLOW); + if (cr.isUnderflow()) { + cr = charsetEncoder.flush(outBuffer); + } + if (cr.isUnderflow()) { + break; + } + if (cr.isOverflow()) { + writePosition(writePosition() + outBuffer.position()); + int maximumSize = (int) (inBuffer.remaining() * charsetEncoder.maxBytesPerChar()); + ensureCapacity(maximumSize); + outBuffer = asByteBuffer(writePosition(), writableByteCount()); + } + } + writePosition(writePosition() + outBuffer.position()); + } + return this; + } + + /** + * Create a new {@code DataBuffer} whose contents is a shared subsequence of this + * data buffer's content. Data between this data buffer and the returned buffer is + * shared; though changes in the returned buffer's position will not be reflected + * in the reading nor writing position of this data buffer. + *

    Note that this method will not call + * {@link DataBufferUtil#retain(DataBuffer)} on the resulting slice: the reference + * count will not be increased. + * @param index the index at which to start the slice + * @param length the length of the slice + * @return the specified slice of this data buffer + */ + DataBuffer slice(int index, int length); + + /** + * Create a new {@code DataBuffer} whose contents is a shared, retained subsequence of this + * data buffer's content. Data between this data buffer and the returned buffer is + * shared; though changes in the returned buffer's position will not be reflected + * in the reading nor writing position of this data buffer. + *

    Note that unlike {@link #slice(int, int)}, this method + * will call {@link DataBufferUtil#retain(DataBuffer)} (or equivalent) on the + * resulting slice. + * @param index the index at which to start the slice + * @param length the length of the slice + * @return the specified, retained slice of this data buffer + */ + default DataBuffer retainedSlice(int index, int length) { + return DataBufferUtil.retain(slice(index, length)); + } + + /** + * Expose this buffer's bytes as a {@link ByteBuffer}. Data between this + * {@code DataBuffer} and the returned {@code ByteBuffer} is shared; though + * changes in the returned buffer's {@linkplain ByteBuffer#position() position} + * will not be reflected in the reading nor writing position of this data buffer. + * @return this data buffer as a byte buffer + */ + ByteBuffer asByteBuffer(); + + /** + * Expose a subsequence of this buffer's bytes as a {@link ByteBuffer}. Data between + * this {@code DataBuffer} and the returned {@code ByteBuffer} is shared; though + * changes in the returned buffer's {@linkplain ByteBuffer#position() position} + * will not be reflected in the reading nor writing position of this data buffer. + * @param index the index at which to start the byte buffer + * @param length the length of the returned byte buffer + * @return this data buffer as a byte buffer + */ + ByteBuffer asByteBuffer(int index, int length); + + /** + * Expose this buffer's data as an {@link InputStream}. Both data and read position are + * shared between the returned stream and this data buffer. The underlying buffer will + * not be {@linkplain DataBufferUtil#release(DataBuffer) released} + * when the input stream is {@linkplain InputStream#close() closed}. + * @return this data buffer as an input stream + * @see #asInputStream(boolean) + */ + InputStream asInputStream(); + + /** + * Expose this buffer's data as an {@link InputStream}. Both data and read position are + * shared between the returned stream and this data buffer. + * @param releaseOnClose whether the underlying buffer will be + * {@linkplain DataBufferUtil#release(DataBuffer) released} when the input stream is + * {@linkplain InputStream#close() closed}. + * @return this data buffer as an input stream + */ + InputStream asInputStream(boolean releaseOnClose); + + /** + * Expose this buffer's data as an {@link OutputStream}. Both data and write position are + * shared between the returned stream and this data buffer. + * @return this data buffer as an output stream + */ + OutputStream asOutputStream(); + + /** + * Return this buffer's data a String using the specified charset. Default implementation + * delegates to {@code toString(readPosition(), readableByteCount(), charset)}. + * @param charset the character set to use + * @return a string representation of all this buffers data + */ + default String toString(Charset charset) { + Objects.requireNonNull(charset, "Charset must not be null"); + return toString(readPosition(), readableByteCount(), charset); + } + + /** + * Return a part of this buffer's data as a String using the specified charset. + * @param index the index at which to start the string + * @param length the number of bytes to use for the string + * @param charset the charset to use + * @return a string representation of a part of this buffers data + */ + String toString(int index, int length, Charset charset); + +} diff --git a/net/src/main/java/org/xbib/net/buffer/DataBufferFactory.java b/net/src/main/java/org/xbib/net/buffer/DataBufferFactory.java new file mode 100644 index 0000000..17a52ab --- /dev/null +++ b/net/src/main/java/org/xbib/net/buffer/DataBufferFactory.java @@ -0,0 +1,57 @@ +package org.xbib.net.buffer; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * A factory for {@link DataBuffer DataBuffers}, allowing for allocation and + * wrapping of data buffers. + */ +public interface DataBufferFactory { + + /** + * Allocate a data buffer of a default initial capacity. Depending on the + * underlying implementation and its configuration, this will be heap-based + * or direct buffer. + * @return the allocated buffer + */ + DataBuffer allocateBuffer(); + + /** + * Allocate a data buffer of the given initial capacity. Depending on the + * underlying implementation and its configuration, this will be heap-based + * or direct buffer. + * @param initialCapacity the initial capacity of the buffer to allocate + * @return the allocated buffer + */ + DataBuffer allocateBuffer(int initialCapacity); + + /** + * Wrap the given {@link ByteBuffer} in a {@code DataBuffer}. Unlike + * {@linkplain #allocateBuffer(int) allocating}, wrapping does not use new memory. + * @param byteBuffer the NIO byte buffer to wrap + * @return the wrapped buffer + */ + DataBuffer wrap(ByteBuffer byteBuffer); + + /** + * Wrap the given {@code byte} array in a {@code DataBuffer}. Unlike + * {@linkplain #allocateBuffer(int) allocating}, wrapping does not use new memory. + * @param bytes the byte array to wrap + * @return the wrapped buffer + */ + DataBuffer wrap(byte[] bytes); + + /** + * Return a new {@code DataBuffer} composed of the {@code dataBuffers} elements joined together. + * Depending on the implementation, the returned buffer may be a single buffer containing all + * data of the provided buffers, or it may be a true composite that contains references to the + * buffers. + *

    Note that the given data buffers do not have to be released, as they are + * released as part of the returned composite. + * @param dataBuffers the data buffers to be composed + * @return a buffer that is composed from the {@code dataBuffers} argument + */ + DataBuffer join(List dataBuffers); + +} diff --git a/net/src/main/java/org/xbib/net/buffer/DataBufferUtil.java b/net/src/main/java/org/xbib/net/buffer/DataBufferUtil.java new file mode 100644 index 0000000..8b2f659 --- /dev/null +++ b/net/src/main/java/org/xbib/net/buffer/DataBufferUtil.java @@ -0,0 +1,72 @@ +package org.xbib.net.buffer; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DataBufferUtil { + + private static final Logger logger = Logger.getLogger(DataBufferUtil.class.getName()); + + private DataBufferUtil() { + } + + public static boolean release(DataBuffer dataBuffer) { + if (dataBuffer instanceof PooledDataBuffer) { + PooledDataBuffer pooledDataBuffer = (PooledDataBuffer) dataBuffer; + if (pooledDataBuffer.isAllocated()) { + try { + return pooledDataBuffer.release(); + } + catch (IllegalStateException ex) { + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "failed to release PooledDataBuffer " + dataBuffer, ex); + } + return false; + } + } + } + return false; + } + + /** + * Retain the given data buffer, if it is a {@link PooledDataBuffer}. + * @param dataBuffer the data buffer to retain + * @return the retained buffer + */ + @SuppressWarnings("unchecked") + public static T retain(T dataBuffer) { + if (dataBuffer instanceof PooledDataBuffer) { + PooledDataBuffer pooledDataBuffer = (PooledDataBuffer) dataBuffer; + return (T) pooledDataBuffer.retain(); + } + else { + return dataBuffer; + } + } + + public static DataBuffer readBuffer(DataBufferFactory factory, + ReadableByteChannel channel, + long size) throws IOException { + boolean release = true; + DataBuffer dataBuffer = factory.allocateBuffer((int) size); + try { + int read; + ByteBuffer byteBuffer = dataBuffer.asByteBuffer(0, dataBuffer.capacity()); + if ((read = channel.read(byteBuffer)) >= 0) { + dataBuffer.writePosition(read); + release = false; + return dataBuffer; + } + else { + return null; + } + } finally { + if (release) { + release(dataBuffer); + } + } + } +} diff --git a/net/src/main/java/org/xbib/net/buffer/DefaultDataBuffer.java b/net/src/main/java/org/xbib/net/buffer/DefaultDataBuffer.java new file mode 100644 index 0000000..df092ba --- /dev/null +++ b/net/src/main/java/org/xbib/net/buffer/DefaultDataBuffer.java @@ -0,0 +1,486 @@ +package org.xbib.net.buffer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.IntPredicate; + +/** + * Default implementation of the {@link DataBuffer} interface that uses a + * {@link ByteBuffer} internally. with separate read and write positions. + * Constructed using the {@link DefaultDataBufferFactory}. + */ +public class DefaultDataBuffer implements DataBuffer { + + private static final int MAX_CAPACITY = Integer.MAX_VALUE; + + private static final int CAPACITY_THRESHOLD = 1024 * 1024 * 4; + + private final DefaultDataBufferFactory dataBufferFactory; + + private ByteBuffer byteBuffer; + + private int capacity; + + private int readPosition; + + private int writePosition; + + private DefaultDataBuffer(DefaultDataBufferFactory dataBufferFactory, ByteBuffer byteBuffer) { + Objects.requireNonNull(dataBufferFactory, "DefaultDataBufferFactory must not be null"); + Objects.requireNonNull(byteBuffer, "ByteBuffer must not be null"); + this.dataBufferFactory = dataBufferFactory; + ByteBuffer slice = byteBuffer.slice(); + this.byteBuffer = slice; + this.capacity = slice.remaining(); + } + + static DefaultDataBuffer fromFilledByteBuffer(DefaultDataBufferFactory dataBufferFactory, ByteBuffer byteBuffer) { + DefaultDataBuffer dataBuffer = new DefaultDataBuffer(dataBufferFactory, byteBuffer); + dataBuffer.writePosition(byteBuffer.remaining()); + return dataBuffer; + } + + static DefaultDataBuffer fromEmptyByteBuffer(DefaultDataBufferFactory dataBufferFactory, ByteBuffer byteBuffer) { + return new DefaultDataBuffer(dataBufferFactory, byteBuffer); + } + + /** + * Directly exposes the native {@code ByteBuffer} that this buffer is based + * on also updating the {@code ByteBuffer's} position and limit to match + * the current {@link #readPosition()} and {@link #readableByteCount()}. + * @return the wrapped byte buffer + */ + public ByteBuffer getNativeBuffer() { + this.byteBuffer.position(this.readPosition); + this.byteBuffer.limit(readableByteCount()); + return this.byteBuffer; + } + + private void setNativeBuffer(ByteBuffer byteBuffer) { + this.byteBuffer = byteBuffer; + this.capacity = byteBuffer.remaining(); + } + + private static ByteBuffer allocate(int capacity, boolean direct) { + return (direct ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity)); + } + + @Override + public DefaultDataBufferFactory factory() { + return this.dataBufferFactory; + } + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + Objects.requireNonNull(predicate, "IntPredicate must not be null"); + if (fromIndex < 0) { + fromIndex = 0; + } + else if (fromIndex >= this.writePosition) { + return -1; + } + for (int i = fromIndex; i < this.writePosition; i++) { + byte b = this.byteBuffer.get(i); + if (predicate.test(b)) { + return i; + } + } + return -1; + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + Objects.requireNonNull(predicate, "IntPredicate must not be null"); + int i = Math.min(fromIndex, this.writePosition - 1); + for (; i >= 0; i--) { + byte b = this.byteBuffer.get(i); + if (predicate.test(b)) { + return i; + } + } + return -1; + } + + @Override + public int readableByteCount() { + return this.writePosition - this.readPosition; + } + + @Override + public int writableByteCount() { + return this.capacity - this.writePosition; + } + + @Override + public int readPosition() { + return this.readPosition; + } + + @Override + public DefaultDataBuffer readPosition(int readPosition) { + assertIndex(readPosition >= 0, "'readPosition' %d must be >= 0", readPosition); + assertIndex(readPosition <= this.writePosition, "'readPosition' %d must be <= %d", + readPosition, this.writePosition); + this.readPosition = readPosition; + return this; + } + + @Override + public int writePosition() { + return this.writePosition; + } + + @Override + public DefaultDataBuffer writePosition(int writePosition) { + assertIndex(writePosition >= this.readPosition, "'writePosition' %d must be >= %d", + writePosition, this.readPosition); + assertIndex(writePosition <= this.capacity, "'writePosition' %d must be <= %d", + writePosition, this.capacity); + this.writePosition = writePosition; + return this; + } + + @Override + public int capacity() { + return this.capacity; + } + + @Override + public DefaultDataBuffer capacity(int newCapacity) { + if (newCapacity <= 0) { + throw new IllegalArgumentException(String.format("'newCapacity' %d must be higher than 0", newCapacity)); + } + int readPosition = readPosition(); + int writePosition = writePosition(); + int oldCapacity = capacity(); + + if (newCapacity > oldCapacity) { + ByteBuffer oldBuffer = this.byteBuffer; + ByteBuffer newBuffer = allocate(newCapacity, oldBuffer.isDirect()); + oldBuffer.position(0).limit(oldBuffer.capacity()); + newBuffer.position(0).limit(oldBuffer.capacity()); + newBuffer.put(oldBuffer); + newBuffer.clear(); + setNativeBuffer(newBuffer); + } + else if (newCapacity < oldCapacity) { + ByteBuffer oldBuffer = this.byteBuffer; + ByteBuffer newBuffer = allocate(newCapacity, oldBuffer.isDirect()); + if (readPosition < newCapacity) { + if (writePosition > newCapacity) { + writePosition = newCapacity; + writePosition(writePosition); + } + oldBuffer.position(readPosition).limit(writePosition); + newBuffer.position(readPosition).limit(writePosition); + newBuffer.put(oldBuffer); + newBuffer.clear(); + } + else { + readPosition(newCapacity); + writePosition(newCapacity); + } + setNativeBuffer(newBuffer); + } + return this; + } + + @Override + public DataBuffer ensureCapacity(int length) { + if (length > writableByteCount()) { + int newCapacity = calculateCapacity(this.writePosition + length); + capacity(newCapacity); + } + return this; + } + + @Override + public byte getByte(int index) { + assertIndex(index >= 0, "index %d must be >= 0", index); + assertIndex(index <= this.writePosition - 1, "index %d must be <= %d", index, this.writePosition - 1); + return this.byteBuffer.get(index); + } + + @Override + public byte read() { + assertIndex(this.readPosition <= this.writePosition - 1, "readPosition %d must be <= %d", + this.readPosition, this.writePosition - 1); + int pos = this.readPosition; + byte b = this.byteBuffer.get(pos); + this.readPosition = pos + 1; + return b; + } + + @Override + public DefaultDataBuffer read(byte[] destination) { + Objects.requireNonNull(destination, "Byte array must not be null"); + read(destination, 0, destination.length); + return this; + } + + @Override + public DefaultDataBuffer read(byte[] destination, int offset, int length) { + Objects.requireNonNull(destination, "Byte array must not be null"); + assertIndex(this.readPosition <= this.writePosition - length, + "readPosition %d and length %d should be smaller than writePosition %d", + this.readPosition, length, this.writePosition); + + ByteBuffer tmp = this.byteBuffer.duplicate(); + int limit = this.readPosition + length; + tmp.clear().position(this.readPosition).limit(limit); + tmp.get(destination, offset, length); + + this.readPosition += length; + return this; + } + + @Override + public DefaultDataBuffer write(byte b) { + ensureCapacity(1); + int pos = this.writePosition; + this.byteBuffer.put(pos, b); + this.writePosition = pos + 1; + return this; + } + + @Override + public DefaultDataBuffer write(byte[] source) { + Objects.requireNonNull(source, "Byte array must not be null"); + write(source, 0, source.length); + return this; + } + + @Override + public DefaultDataBuffer write(byte[] source, int offset, int length) { + Objects.requireNonNull(source, "Byte array must not be null"); + ensureCapacity(length); + + ByteBuffer tmp = this.byteBuffer.duplicate(); + int limit = this.writePosition + length; + tmp.clear().position(this.writePosition).limit(limit); + tmp.put(source, offset, length); + + this.writePosition += length; + return this; + } + + @Override + public DefaultDataBuffer write(DataBuffer... buffers) { + if (buffers != null && buffers.length > 0) { + write(Arrays.stream(buffers).map(DataBuffer::asByteBuffer).toArray(ByteBuffer[]::new)); + } + return this; + } + + @Override + public DefaultDataBuffer write(ByteBuffer... buffers) { + if (buffers != null && buffers.length > 0) { + int capacity = Arrays.stream(buffers).mapToInt(ByteBuffer::remaining).sum(); + ensureCapacity(capacity); + Arrays.stream(buffers).forEach(this::write); + } + return this; + } + + private void write(ByteBuffer source) { + int length = source.remaining(); + ByteBuffer tmp = this.byteBuffer.duplicate(); + int limit = this.writePosition + source.remaining(); + tmp.clear().position(this.writePosition).limit(limit); + tmp.put(source); + this.writePosition += length; + } + + @Override + public DefaultDataBuffer slice(int index, int length) { + checkIndex(index, length); + int oldPosition = this.byteBuffer.position(); + try { + this.byteBuffer.position(index); + ByteBuffer slice = this.byteBuffer.slice(); + slice.limit(length); + return new SlicedDefaultDataBuffer(slice, this.dataBufferFactory, length); + } + finally { + this.byteBuffer.position(oldPosition); + } + } + + @Override + public ByteBuffer asByteBuffer() { + return asByteBuffer(this.readPosition, readableByteCount()); + } + + @Override + public ByteBuffer asByteBuffer(int index, int length) { + checkIndex(index, length); + + ByteBuffer duplicate = this.byteBuffer.duplicate(); + duplicate.position(index); + duplicate.limit(index + length); + return duplicate.slice(); + } + + @Override + public InputStream asInputStream() { + return new DefaultDataBufferInputStream(); + } + + @Override + public InputStream asInputStream(boolean releaseOnClose) { + return new DefaultDataBufferInputStream(); + } + + @Override + public OutputStream asOutputStream() { + return new DefaultDataBufferOutputStream(); + } + + @Override + public String toString(int index, int length, Charset charset) { + checkIndex(index, length); + Objects.requireNonNull(charset, "Charset must not be null"); + + byte[] bytes; + int offset; + + if (this.byteBuffer.hasArray()) { + bytes = this.byteBuffer.array(); + offset = this.byteBuffer.arrayOffset() + index; + } + else { + bytes = new byte[length]; + offset = 0; + ByteBuffer duplicate = this.byteBuffer.duplicate(); + duplicate.clear().position(index).limit(index + length); + duplicate.get(bytes, 0, length); + } + return new String(bytes, offset, length, charset); + } + + /** + * Calculate the capacity of the buffer. + */ + private int calculateCapacity(int neededCapacity) { + if (neededCapacity < 0) { + throw new IllegalArgumentException("'neededCapacity' must >= 0"); + } + if (neededCapacity == CAPACITY_THRESHOLD) { + return CAPACITY_THRESHOLD; + } + else if (neededCapacity > CAPACITY_THRESHOLD) { + int newCapacity = neededCapacity / CAPACITY_THRESHOLD * CAPACITY_THRESHOLD; + if (newCapacity > MAX_CAPACITY - CAPACITY_THRESHOLD) { + newCapacity = MAX_CAPACITY; + } + else { + newCapacity += CAPACITY_THRESHOLD; + } + return newCapacity; + } + else { + int newCapacity = 64; + while (newCapacity < neededCapacity) { + newCapacity <<= 1; + } + return newCapacity; + } + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof DefaultDataBuffer)) { + return false; + } + DefaultDataBuffer otherBuffer = (DefaultDataBuffer) other; + return (this.readPosition == otherBuffer.readPosition && + this.writePosition == otherBuffer.writePosition && + this.byteBuffer.equals(otherBuffer.byteBuffer)); + } + + @Override + public int hashCode() { + return this.byteBuffer.hashCode(); + } + + @Override + public String toString() { + return String.format("DefaultDataBuffer (r: %d, w: %d, c: %d)", + this.readPosition, this.writePosition, this.capacity); + } + + private void checkIndex(int index, int length) { + assertIndex(index >= 0, "index %d must be >= 0", index); + assertIndex(length >= 0, "length %d must be >= 0", length); + assertIndex(index <= this.capacity, "index %d must be <= %d", index, this.capacity); + assertIndex(length <= this.capacity, "length %d must be <= %d", length, this.capacity); + } + + private void assertIndex(boolean expression, String format, Object... args) { + if (!expression) { + String message = String.format(format, args); + throw new IndexOutOfBoundsException(message); + } + } + + private class DefaultDataBufferInputStream extends InputStream { + + @Override + public int available() { + return readableByteCount(); + } + + @Override + public int read() { + return available() > 0 ? DefaultDataBuffer.this.read() & 0xFF : -1; + } + + @Override + public int read(byte[] bytes, int off, int len) throws IOException { + int available = available(); + if (available > 0) { + len = Math.min(len, available); + DefaultDataBuffer.this.read(bytes, off, len); + return len; + } + else { + return -1; + } + } + } + + private class DefaultDataBufferOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + DefaultDataBuffer.this.write((byte) b); + } + + @Override + public void write(byte[] bytes, int off, int len) throws IOException { + DefaultDataBuffer.this.write(bytes, off, len); + } + } + + private static class SlicedDefaultDataBuffer extends DefaultDataBuffer { + + SlicedDefaultDataBuffer(ByteBuffer byteBuffer, DefaultDataBufferFactory dataBufferFactory, int length) { + super(dataBufferFactory, byteBuffer); + writePosition(length); + } + + @Override + public DefaultDataBuffer capacity(int newCapacity) { + throw new UnsupportedOperationException("Changing the capacity of a sliced buffer is not supported"); + } + } + +} diff --git a/net/src/main/java/org/xbib/net/buffer/DefaultDataBufferFactory.java b/net/src/main/java/org/xbib/net/buffer/DefaultDataBufferFactory.java new file mode 100644 index 0000000..932ae86 --- /dev/null +++ b/net/src/main/java/org/xbib/net/buffer/DefaultDataBufferFactory.java @@ -0,0 +1,110 @@ +package org.xbib.net.buffer; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Default implementation of the {@code DataBufferFactory} interface. Allows for + * specification of the default initial capacity at construction time, as well + * as whether heap-based or direct buffers are to be preferred. + */ +public class DefaultDataBufferFactory implements DataBufferFactory { + + /** + * The default capacity when none is specified. + * @see #DefaultDataBufferFactory() + * @see #DefaultDataBufferFactory(boolean) + */ + public static final int DEFAULT_INITIAL_CAPACITY = 256; + + /** + * Shared instance based on the default constructor. + */ + private static final DefaultDataBufferFactory INSTANCE = new DefaultDataBufferFactory(); + + private final boolean preferDirect; + + private final int defaultInitialCapacity; + + /** + * Creates a new {@code DefaultDataBufferFactory} with default settings. + */ + public DefaultDataBufferFactory() { + this(false); + } + + /** + * Creates a new {@code DefaultDataBufferFactory}, indicating whether direct + * buffers should be created by {@link #allocateBuffer()} and + * {@link #allocateBuffer(int)}. + * @param preferDirect {@code true} if direct buffers are to be preferred; + * {@code false} otherwise + */ + public DefaultDataBufferFactory(boolean preferDirect) { + this(preferDirect, DEFAULT_INITIAL_CAPACITY); + } + + /** + * Creates a new {@code DefaultDataBufferFactory}, indicating whether direct + * buffers should be created by {@link #allocateBuffer()} and + * {@link #allocateBuffer(int)}, and what the capacity is to be used for + * {@link #allocateBuffer()}. + * @param preferDirect {@code true} if direct buffers are to be preferred; + * {@code false} otherwise + */ + public DefaultDataBufferFactory(boolean preferDirect, int defaultInitialCapacity) { + if (defaultInitialCapacity <= 0) { + throw new IllegalArgumentException("'defaultInitialCapacity' should be larger than 0"); + } + this.preferDirect = preferDirect; + this.defaultInitialCapacity = defaultInitialCapacity; + } + + public static DataBufferFactory getInstance() { + return INSTANCE; + } + + @Override + public DefaultDataBuffer allocateBuffer() { + return allocateBuffer(this.defaultInitialCapacity); + } + + @Override + public DefaultDataBuffer allocateBuffer(int initialCapacity) { + ByteBuffer byteBuffer = (this.preferDirect ? + ByteBuffer.allocateDirect(initialCapacity) : + ByteBuffer.allocate(initialCapacity)); + return DefaultDataBuffer.fromEmptyByteBuffer(this, byteBuffer); + } + + @Override + public DefaultDataBuffer wrap(ByteBuffer byteBuffer) { + return DefaultDataBuffer.fromFilledByteBuffer(this, byteBuffer.slice()); + } + + @Override + public DefaultDataBuffer wrap(byte[] bytes) { + return DefaultDataBuffer.fromFilledByteBuffer(this, ByteBuffer.wrap(bytes)); + } + + /** + * This implementation creates a single {@link DefaultDataBuffer} + * to contain the data in {@code dataBuffers}. + */ + @Override + public DefaultDataBuffer join(List dataBuffers) { + if (dataBuffers == null || dataBuffers.isEmpty()) { + throw new IllegalArgumentException("DataBuffer List must not be empty"); + } + int capacity = dataBuffers.stream().mapToInt(DataBuffer::readableByteCount).sum(); + DefaultDataBuffer result = allocateBuffer(capacity); + dataBuffers.forEach(result::write); + dataBuffers.forEach(DataBufferUtil::release); + return result; + } + + @Override + public String toString() { + return "DefaultDataBufferFactory (preferDirect=" + this.preferDirect + ")"; + } +} diff --git a/net/src/main/java/org/xbib/net/buffer/PooledDataBuffer.java b/net/src/main/java/org/xbib/net/buffer/PooledDataBuffer.java new file mode 100644 index 0000000..ab9fc88 --- /dev/null +++ b/net/src/main/java/org/xbib/net/buffer/PooledDataBuffer.java @@ -0,0 +1,35 @@ +package org.xbib.net.buffer; + +/** + * Extension of {@link DataBuffer} that allows for buffer that share + * a memory pool. Introduces methods for reference counting. + */ +public interface PooledDataBuffer extends DataBuffer { + + /** + * Return {@code true} if this buffer is allocated; + * {@code false} if it has been deallocated. + */ + boolean isAllocated(); + + /** + * Increase the reference count for this buffer by one. + * @return this buffer + */ + PooledDataBuffer retain(); + + /** + * Associate the given hint with the data buffer for debugging purposes. + * @return this buffer + */ + PooledDataBuffer touch(Object hint); + + /** + * Decrease the reference count for this buffer by one, + * and deallocate it once the count reaches zero. + * @return {@code true} if the buffer was deallocated; + * {@code false} otherwise + */ + boolean release(); + +} diff --git a/net/src/main/java/org/xbib/net/package-info.java b/net/src/main/java/org/xbib/net/package-info.java new file mode 100644 index 0000000..740f554 --- /dev/null +++ b/net/src/main/java/org/xbib/net/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL building and parsing. + */ +package org.xbib.net; diff --git a/net/src/main/java/org/xbib/net/scheme/AbstractScheme.java b/net/src/main/java/org/xbib/net/scheme/AbstractScheme.java new file mode 100644 index 0000000..44dad94 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/AbstractScheme.java @@ -0,0 +1,34 @@ +package org.xbib.net.scheme; + +import org.xbib.net.URL; + +/** + * Base implementation for scheme. + */ +public abstract class AbstractScheme implements Scheme { + + protected final String name; + + protected final int defaultPort; + + protected AbstractScheme(String name, int defaultPort) { + this.name = name; + this.defaultPort = defaultPort; + } + + @Override + public int getDefaultPort() { + return defaultPort; + } + + @Override + public String getName() { + return name; + } + + @Override + public URL normalize(URL url) { + return url; + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/DefaultScheme.java b/net/src/main/java/org/xbib/net/scheme/DefaultScheme.java new file mode 100644 index 0000000..65a875a --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/DefaultScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * Default scheme. + */ +public class DefaultScheme extends AbstractScheme { + + public DefaultScheme(String name) { + super(name, -1); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/DnsScheme.java b/net/src/main/java/org/xbib/net/scheme/DnsScheme.java new file mode 100644 index 0000000..2bce186 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/DnsScheme.java @@ -0,0 +1,17 @@ +package org.xbib.net.scheme; + +/** + * The DNS URI scheme. + * @see DNS RFC + */ +class DnsScheme extends HttpScheme { + + DnsScheme() { + super("dns", 53); + } + + DnsScheme(String name, int port) { + super(name, port); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/FileScheme.java b/net/src/main/java/org/xbib/net/scheme/FileScheme.java new file mode 100644 index 0000000..73f17de --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/FileScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * File scheme. + */ +class FileScheme extends HttpScheme { + + FileScheme() { + super("file", -1); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/FtpScheme.java b/net/src/main/java/org/xbib/net/scheme/FtpScheme.java new file mode 100644 index 0000000..b280504 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/FtpScheme.java @@ -0,0 +1,16 @@ +package org.xbib.net.scheme; + +/** + * FTP scheme. + */ +class FtpScheme extends HttpScheme { + + FtpScheme() { + super("ftp", 21); + } + + FtpScheme(String name, int port) { + super(name, port); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/GitScheme.java b/net/src/main/java/org/xbib/net/scheme/GitScheme.java new file mode 100644 index 0000000..40777c5 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/GitScheme.java @@ -0,0 +1,16 @@ +package org.xbib.net.scheme; + +/** + * Git scheme. + */ +class GitScheme extends HttpScheme { + + GitScheme() { + super("git", 443); + } + + GitScheme(String name, int port) { + super(name, port); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/GitSecureHttpScheme.java b/net/src/main/java/org/xbib/net/scheme/GitSecureHttpScheme.java new file mode 100644 index 0000000..75417d6 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/GitSecureHttpScheme.java @@ -0,0 +1,16 @@ +package org.xbib.net.scheme; + +/** + * Git secure scheme. + */ +class GitSecureHttpScheme extends HttpScheme { + + GitSecureHttpScheme() { + super("git+https", 443); + } + + GitSecureHttpScheme(String name, int port) { + super(name, port); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/GopherScheme.java b/net/src/main/java/org/xbib/net/scheme/GopherScheme.java new file mode 100644 index 0000000..e60ebd7 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/GopherScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * Gopher scheme. + */ +class GopherScheme extends AbstractScheme { + + GopherScheme() { + super("gopher", 70); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/HttpScheme.java b/net/src/main/java/org/xbib/net/scheme/HttpScheme.java new file mode 100644 index 0000000..bc62e10 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/HttpScheme.java @@ -0,0 +1,37 @@ +package org.xbib.net.scheme; + +import org.xbib.net.URL; +import org.xbib.net.PathNormalizer; + +import java.util.Locale; + +/** + * HTTP scheme. + */ +class HttpScheme extends AbstractScheme { + + HttpScheme() { + super("http", 80); + } + + HttpScheme(String name, int port) { + super(name, port); + } + + @Override + public URL normalize(URL url) { + String host = url.getHost(); + if (host != null) { + host = host.toLowerCase(Locale.ROOT); + } + return URL.builder() + .scheme(url.getScheme()) + .userInfo(url.getUserInfo()) + .host(host, url.getProtocolVersion()) + .port(url.getPort()) + .path(PathNormalizer.normalize(url.getPath())) + .query(url.getQuery()) + .fragment(url.getFragment()) + .build(); + } +} diff --git a/net/src/main/java/org/xbib/net/scheme/ImapScheme.java b/net/src/main/java/org/xbib/net/scheme/ImapScheme.java new file mode 100644 index 0000000..e23d8d0 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/ImapScheme.java @@ -0,0 +1,17 @@ +package org.xbib.net.scheme; + +/** + * The IMAP scheme. + * + * @see IMAP RFC + */ +class ImapScheme extends AbstractScheme { + + ImapScheme() { + super("imap", 143); + } + + ImapScheme(String name, int port) { + super(name, port); + } +} diff --git a/net/src/main/java/org/xbib/net/scheme/IrcScheme.java b/net/src/main/java/org/xbib/net/scheme/IrcScheme.java new file mode 100644 index 0000000..6381b5f --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/IrcScheme.java @@ -0,0 +1,18 @@ +package org.xbib.net.scheme; + +/** + * The IRC scheme. + * + * @see IRC draft + */ +class IrcScheme extends HttpScheme { + + IrcScheme() { + super("irc", 194); + } + + IrcScheme(String name, int port) { + super(name, port); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/LdapScheme.java b/net/src/main/java/org/xbib/net/scheme/LdapScheme.java new file mode 100644 index 0000000..588dbd6 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/LdapScheme.java @@ -0,0 +1,17 @@ +package org.xbib.net.scheme; + +/** + * The LDAP scheme. + * @see LDAP RFC + */ +class LdapScheme extends AbstractScheme { + + LdapScheme() { + super("ldap", 143); + } + + LdapScheme(String name, int port) { + super(name, port); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/MailtoScheme.java b/net/src/main/java/org/xbib/net/scheme/MailtoScheme.java new file mode 100644 index 0000000..c631d94 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/MailtoScheme.java @@ -0,0 +1,13 @@ +package org.xbib.net.scheme; + +/** + * The mailto scheme. + * + */ +public class MailtoScheme extends AbstractScheme { + + public MailtoScheme() { + super("mailto", -1); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/NewsScheme.java b/net/src/main/java/org/xbib/net/scheme/NewsScheme.java new file mode 100644 index 0000000..7cfed9f --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/NewsScheme.java @@ -0,0 +1,18 @@ +package org.xbib.net.scheme; + +/** + * The news scheme. + * + * @see news RFC + */ +class NewsScheme extends AbstractScheme { + + NewsScheme() { + super("nntp", 119); + } + + NewsScheme(String name, int port) { + super(name, port); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/NntpScheme.java b/net/src/main/java/org/xbib/net/scheme/NntpScheme.java new file mode 100644 index 0000000..a5ed746 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/NntpScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The nttp scheme. + * + * @see NNTP RFC + */ +class NntpScheme extends AbstractScheme { + + NntpScheme() { + super("nntp", 119); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/Pop3Scheme.java b/net/src/main/java/org/xbib/net/scheme/Pop3Scheme.java new file mode 100644 index 0000000..dd7bda6 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/Pop3Scheme.java @@ -0,0 +1,18 @@ +package org.xbib.net.scheme; + +/** + * The POP3 scheme. + * + * @see POP3 RFC + */ +class Pop3Scheme extends AbstractScheme { + + Pop3Scheme() { + super("pop3", 110); + } + + Pop3Scheme(String name, int port) { + super(name, port); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/RedisScheme.java b/net/src/main/java/org/xbib/net/scheme/RedisScheme.java new file mode 100644 index 0000000..916d8a1 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/RedisScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * Redis scheme. + */ +class RedisScheme extends AbstractScheme { + + RedisScheme() { + super("redis", 6379); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/RsyncScheme.java b/net/src/main/java/org/xbib/net/scheme/RsyncScheme.java new file mode 100644 index 0000000..3aa6be0 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/RsyncScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * Rsync scheme. + */ +class RsyncScheme extends SshScheme { + + RsyncScheme() { + super("rsync", 873); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/RtmpScheme.java b/net/src/main/java/org/xbib/net/scheme/RtmpScheme.java new file mode 100644 index 0000000..7860df6 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/RtmpScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * Rtmp scheme. + */ +class RtmpScheme extends AbstractScheme { + + RtmpScheme() { + super("rtmp", 1935); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/RtspScheme.java b/net/src/main/java/org/xbib/net/scheme/RtspScheme.java new file mode 100644 index 0000000..eaca520 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/RtspScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The RTSP scheme. + * + * @see RTSP RFC + */ +class RtspScheme extends AbstractScheme { + + RtspScheme() { + super("rtsp", 554); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/Scheme.java b/net/src/main/java/org/xbib/net/scheme/Scheme.java new file mode 100644 index 0000000..cda1012 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/Scheme.java @@ -0,0 +1,48 @@ +package org.xbib.net.scheme; + +import org.xbib.net.URL; + +/** + * Interface implemented by custom scheme parsers. + */ +public interface Scheme { + + String DNS = "dns"; + String FILE = "file"; + String FTP = "ftp"; + String GIT = "git"; + String GIT_HTTPS = "git+https"; + String GOPHER = "gopher"; + String HTTP = "http"; + String HTTPS = "https"; + String IMAP = "imap"; + String IMAPS = "imaps"; + String IRC = "irc"; + String LDAP = "ldap"; + String LDAPS = "ldaps"; + String MAILTO = "mailto"; + String NEWS = "news"; + String NNTP = "nntp"; + String POP3 = "pop3"; + String POP3S = "pop3s"; + String REDIS = "redis"; + String RSYNC = "rsync"; + String RTMP = "rtmp"; + String RTSP = "rtsp"; + String SFTP = "sftp"; + String SMTP = "smtp"; + String SMTPS = "smtps"; + String SNEWS = "snews"; + String SSH = "ssh"; + String TELNET = "telnet"; + String TFTP = "tftp"; + String URN = "urn"; + String WS = "ws"; + String WSS = "wss"; + + String getName(); + + int getDefaultPort(); + + URL normalize(URL url); +} diff --git a/net/src/main/java/org/xbib/net/scheme/SchemeRegistry.java b/net/src/main/java/org/xbib/net/scheme/SchemeRegistry.java new file mode 100644 index 0000000..d536745 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SchemeRegistry.java @@ -0,0 +1,80 @@ +package org.xbib.net.scheme; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.ServiceLoader; + +/** + * Registry of URL schemes. + */ +public final class SchemeRegistry { + + private static final SchemeRegistry registry = new SchemeRegistry(); + + private final Map schemes; + + private SchemeRegistry() { + schemes = new HashMap<>(); + schemes.put(Scheme.DNS , new DnsScheme()); + schemes.put(Scheme.FILE , new FileScheme()); + schemes.put(Scheme.FTP, new FtpScheme()); + schemes.put(Scheme.GIT, new GitScheme()); + schemes.put(Scheme.GIT_HTTPS, new GitSecureHttpScheme()); + schemes.put(Scheme.GOPHER, new GopherScheme()); + schemes.put(Scheme.HTTP, new HttpScheme()); + schemes.put(Scheme.HTTPS, new SecureHttpScheme()); + schemes.put(Scheme.IMAP, new ImapScheme()); + schemes.put(Scheme.IMAPS, new SecureImapScheme()); + schemes.put(Scheme.IRC, new IrcScheme()); + schemes.put(Scheme.LDAP, new LdapScheme()); + schemes.put(Scheme.LDAPS, new SecureLdapScheme()); + schemes.put(Scheme.MAILTO, new MailtoScheme()); + schemes.put(Scheme.NEWS, new NewsScheme()); + schemes.put(Scheme.NNTP, new NntpScheme()); + schemes.put(Scheme.POP3, new Pop3Scheme()); + schemes.put(Scheme.POP3S, new SecurePop3Scheme()); + schemes.put(Scheme.REDIS, new RedisScheme()); + schemes.put(Scheme.RSYNC, new RsyncScheme()); + schemes.put(Scheme.RTMP, new RtmpScheme()); + schemes.put(Scheme.RTSP, new RtspScheme()); + schemes.put(Scheme.SFTP, new SftpScheme()); + schemes.put(Scheme.SMTP, new SmtpScheme()); + schemes.put(Scheme.SMTPS, new SecureSmtpScheme()); + schemes.put(Scheme.SNEWS, new SecureNewsScheme()); + schemes.put(Scheme.SSH, new SshScheme()); + schemes.put(Scheme.TELNET, new TelnetScheme()); + schemes.put(Scheme.TFTP, new TftpScheme()); + schemes.put(Scheme.URN, new UrnScheme()); + schemes.put(Scheme.WS, new WebSocketScheme()); + schemes.put(Scheme.WSS, new SecureWebSocketScheme()); + for (Scheme scheme : ServiceLoader.load(Scheme.class)) { + register(scheme); + } + } + + public static SchemeRegistry getInstance() { + return registry; + } + + public boolean register(Scheme scheme) { + String name = scheme.getName(); + if (name == null) { + return false; + } + if (!schemes.containsKey(name)) { + schemes.put(name.toLowerCase(Locale.ROOT), scheme); + return true; + } else { + return false; + } + } + + public Scheme getScheme(String scheme) { + if (scheme == null) { + return null; + } + Scheme s = schemes.get(scheme.toLowerCase(Locale.ROOT)); + return s != null ? s : new DefaultScheme(scheme); + } +} diff --git a/net/src/main/java/org/xbib/net/scheme/SecureHttpScheme.java b/net/src/main/java/org/xbib/net/scheme/SecureHttpScheme.java new file mode 100644 index 0000000..a87dfe6 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SecureHttpScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * Secure HTTP scheme. + */ +class SecureHttpScheme extends HttpScheme { + + SecureHttpScheme() { + super("https", 443); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/SecureImapScheme.java b/net/src/main/java/org/xbib/net/scheme/SecureImapScheme.java new file mode 100644 index 0000000..e3eb80c --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SecureImapScheme.java @@ -0,0 +1,13 @@ +package org.xbib.net.scheme; + +/** + * The secure IMAP scheme. + * @see IMAP scheme RFC + */ +class SecureImapScheme extends ImapScheme { + + SecureImapScheme() { + super("imaps", 993); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/SecureLdapScheme.java b/net/src/main/java/org/xbib/net/scheme/SecureLdapScheme.java new file mode 100644 index 0000000..bc90cfd --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SecureLdapScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The secure LDAP scheme. + * + * @see LDAP RFC + */ +class SecureLdapScheme extends LdapScheme { + + SecureLdapScheme() { + super("ldaps", 636); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/SecureNewsScheme.java b/net/src/main/java/org/xbib/net/scheme/SecureNewsScheme.java new file mode 100644 index 0000000..dda8e41 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SecureNewsScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The secure news scheme. + * + * @see news RFC + */ +class SecureNewsScheme extends NewsScheme { + + SecureNewsScheme() { + super("snews", 563); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/SecurePop3Scheme.java b/net/src/main/java/org/xbib/net/scheme/SecurePop3Scheme.java new file mode 100644 index 0000000..bea7185 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SecurePop3Scheme.java @@ -0,0 +1,13 @@ +package org.xbib.net.scheme; + +/** + * The secure POP3 scheme. + * + * @see POP3 RFC + */ +class SecurePop3Scheme extends Pop3Scheme { + + SecurePop3Scheme() { + super("pop3s", 995); + } +} diff --git a/net/src/main/java/org/xbib/net/scheme/SecureSmtpScheme.java b/net/src/main/java/org/xbib/net/scheme/SecureSmtpScheme.java new file mode 100644 index 0000000..fc6d38f --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SecureSmtpScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The secure SMTP scheme. + * + * @see SMTP RFC + */ +class SecureSmtpScheme extends SmtpScheme { + + SecureSmtpScheme() { + super("smtps", 587); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/SecureWebSocketScheme.java b/net/src/main/java/org/xbib/net/scheme/SecureWebSocketScheme.java new file mode 100644 index 0000000..09eb0b5 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SecureWebSocketScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * Secure web socket scheme. + */ +class SecureWebSocketScheme extends WebSocketScheme { + + SecureWebSocketScheme() { + super("wss", 443); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/SftpScheme.java b/net/src/main/java/org/xbib/net/scheme/SftpScheme.java new file mode 100644 index 0000000..9233336 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SftpScheme.java @@ -0,0 +1,12 @@ +package org.xbib.net.scheme; + +/** + * Secure FTP scheme. + */ +class SftpScheme extends SshScheme { + + SftpScheme() { + super("sftp", 22); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/SmtpScheme.java b/net/src/main/java/org/xbib/net/scheme/SmtpScheme.java new file mode 100644 index 0000000..f19fc0f --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SmtpScheme.java @@ -0,0 +1,18 @@ +package org.xbib.net.scheme; + +/** + * The SMTP scheme. + * + * @see SMTP RFC + */ +class SmtpScheme extends AbstractScheme { + + SmtpScheme() { + super("smtp", 25); + } + + SmtpScheme(String name, int port) { + super(name, port); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/SshScheme.java b/net/src/main/java/org/xbib/net/scheme/SshScheme.java new file mode 100644 index 0000000..2736049 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/SshScheme.java @@ -0,0 +1,15 @@ +package org.xbib.net.scheme; + +/** + * Secure shell scheme. + */ +class SshScheme extends HttpScheme { + + SshScheme() { + super("ssh", 22); + } + + SshScheme(String name, int port) { + super(name, port); + } +} diff --git a/net/src/main/java/org/xbib/net/scheme/TelnetScheme.java b/net/src/main/java/org/xbib/net/scheme/TelnetScheme.java new file mode 100644 index 0000000..a885a30 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/TelnetScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The TELNET scheme. + * + * @see TELNET RFC + */ +class TelnetScheme extends AbstractScheme { + + TelnetScheme() { + super("telnet", 23); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/TftpScheme.java b/net/src/main/java/org/xbib/net/scheme/TftpScheme.java new file mode 100644 index 0000000..42f4797 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/TftpScheme.java @@ -0,0 +1,13 @@ +package org.xbib.net.scheme; + +/** + * The TFTP scheme. + * + * @see TFTP RFC + */ +class TftpScheme extends FtpScheme { + + TftpScheme() { + super("tftp", 69); + } +} diff --git a/net/src/main/java/org/xbib/net/scheme/UrnScheme.java b/net/src/main/java/org/xbib/net/scheme/UrnScheme.java new file mode 100644 index 0000000..246dc84 --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/UrnScheme.java @@ -0,0 +1,14 @@ +package org.xbib.net.scheme; + +/** + * The URN scheme. + * + * @see URN RFC + */ +class UrnScheme extends AbstractScheme { + + UrnScheme() { + super("urn", -1); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/WebSocketScheme.java b/net/src/main/java/org/xbib/net/scheme/WebSocketScheme.java new file mode 100644 index 0000000..5b996de --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/WebSocketScheme.java @@ -0,0 +1,16 @@ +package org.xbib.net.scheme; + +/** + * Web socket scheme. + */ +class WebSocketScheme extends HttpScheme { + + WebSocketScheme() { + super("ws", 80); + } + + WebSocketScheme(String name, int port) { + super(name, port); + } + +} diff --git a/net/src/main/java/org/xbib/net/scheme/package-info.java b/net/src/main/java/org/xbib/net/scheme/package-info.java new file mode 100644 index 0000000..df1a75c --- /dev/null +++ b/net/src/main/java/org/xbib/net/scheme/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for schemes. + */ +package org.xbib.net.scheme; diff --git a/net/src/main/java/org/xbib/net/template/URITemplate.java b/net/src/main/java/org/xbib/net/template/URITemplate.java new file mode 100644 index 0000000..d2f0c82 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/URITemplate.java @@ -0,0 +1,48 @@ +package org.xbib.net.template; + +import org.xbib.net.URL; +import org.xbib.net.template.expression.URITemplateExpression; +import org.xbib.net.template.parse.URITemplateParser; +import org.xbib.net.template.vars.Variables; + +import java.util.List; + +/** + * URI templates. + */ +public class URITemplate { + + private final List expressions; + + public URITemplate(String input) { + this.expressions = URITemplateParser.parse(input); + } + + public List expressions() { + return expressions; + } + + /** + * Expand this template to a string given a list of variables. + * + * @param vars the variable map (names as keys, contents as values) + * @return expanded string + */ + public String toString(Variables vars) { + StringBuilder sb = new StringBuilder(); + for (URITemplateExpression expression : expressions) { + sb.append(expression.expand(vars)); + } + return sb.toString(); + } + + /** + * Expand this template to a URL given a set of variables. + * + * @param vars the variables + * @return a URL + */ + public URL toURL(Variables vars) { + return URL.from(toString(vars)); + } +} diff --git a/net/src/main/java/org/xbib/net/template/expression/ExpressionType.java b/net/src/main/java/org/xbib/net/template/expression/ExpressionType.java new file mode 100644 index 0000000..25cca6f --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/expression/ExpressionType.java @@ -0,0 +1,104 @@ +package org.xbib.net.template.expression; + +/** + * Expression types. + */ +public enum ExpressionType { + /* + * Simple character expansion. + */ + SIMPLE("", ',', false, ""), + /* + * Reserved character expansion. + */ + RESERVED("", ',', false, ""), + /* + * Name labels expansion. + */ + NAME_LABELS(".", '.', false, ""), + /* + * Path segments expansion. + */ + PATH_SEGMENTS("/", '/', false, ""), + /* + * Path parameters expansion. + */ + PATH_PARAMETERS(";", ';', true, ""), + /* + * Query string expansion. + */ + QUERY_STRING("?", '&', true, "="), + /* + * Query string continuation expansion. + */ + QUERY_CONT("&", '&', true, "="), + /* + * Fragment expansion. + */ + FRAGMENT("#", ',', false, ""); + + /** + * Prefix string of expansion (requires at least one expanded token). + */ + private final String prefix; + + /** + * Separator if several tokens are present. + */ + private final char separator; + + /** + * Whether the variable (string, list) or key (map) name should be included + * if no explode modifier is found. + */ + private final boolean named; + + /** + * String to append to a name if the matching value is empty (empty string, + * empty list element, empty map value). + */ + private final String ifEmpty; + + ExpressionType(String prefix, char separator, boolean named, String ifEmpty) { + this.prefix = prefix; + this.separator = separator; + this.named = named; + this.ifEmpty = ifEmpty; + } + + /** + * Get the prefix string for this expansion type. + * + * @return the prefix string + */ + public String getPrefix() { + return prefix; + } + + /** + * Get the separator between token expansion elements. + * + * @return the separator + */ + public char getSeparator() { + return separator; + } + + /** + * Tell whether the variable name should be used in expansion. + * + * @return true if this is the case + */ + public boolean isNamed() { + return named; + } + + /** + * Get the substitution string for empty values. + * + * @return the substitution string + */ + public String getIfEmpty() { + return ifEmpty; + } +} diff --git a/net/src/main/java/org/xbib/net/template/expression/TemplateExpression.java b/net/src/main/java/org/xbib/net/template/expression/TemplateExpression.java new file mode 100644 index 0000000..2b63c1a --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/expression/TemplateExpression.java @@ -0,0 +1,72 @@ +package org.xbib.net.template.expression; + +import org.xbib.net.template.render.ValueRenderer; +import org.xbib.net.template.vars.Variables; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.ArrayList; +import java.util.List; + +/** + * Template expression. + */ +public class TemplateExpression implements URITemplateExpression { + + private final ExpressionType expressionType; + + private final List variableSpecs; + + public TemplateExpression(ExpressionType expressionType, List variableSpecs) { + this.expressionType = expressionType; + this.variableSpecs = variableSpecs; + if (expressionType == null) { + throw new IllegalArgumentException("expression type must not be null"); + } + if (variableSpecs == null) { + throw new IllegalArgumentException("variables must not be null"); + } + } + + @Override + public String expand(Variables vars) { + List expansions = new ArrayList<>(); + VariableValue value; + ValueRenderer renderer; + for (VariableSpec varspec : variableSpecs) { + value = vars.get(varspec.getName()); + if (value == null) { + continue; + } + renderer = value.getType().selectRenderer(expressionType); + List list = renderer.render(varspec, value); + if (list != null) { + expansions.addAll(list); + } + } + if (expansions.isEmpty()) { + return ""; + } + return expressionType.getPrefix() + String.join(Character.toString(expressionType.getSeparator()), expansions); + } + + @Override + public int hashCode() { + return 31 * expressionType.hashCode() + variableSpecs.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + TemplateExpression other = (TemplateExpression) obj; + return expressionType == other.expressionType && variableSpecs.equals(other.variableSpecs); + } +} diff --git a/net/src/main/java/org/xbib/net/template/expression/TemplateLiteral.java b/net/src/main/java/org/xbib/net/template/expression/TemplateLiteral.java new file mode 100644 index 0000000..35135b1 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/expression/TemplateLiteral.java @@ -0,0 +1,21 @@ +package org.xbib.net.template.expression; + +import org.xbib.net.template.vars.Variables; + +/** + * Template literal. + */ +public +class TemplateLiteral implements URITemplateExpression { + + private final String literal; + + public TemplateLiteral(String literal) { + this.literal = literal; + } + + @Override + public String expand(Variables vars) { + return literal; + } +} diff --git a/net/src/main/java/org/xbib/net/template/expression/URITemplateExpression.java b/net/src/main/java/org/xbib/net/template/expression/URITemplateExpression.java new file mode 100644 index 0000000..0cc2ff5 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/expression/URITemplateExpression.java @@ -0,0 +1,11 @@ +package org.xbib.net.template.expression; + +import org.xbib.net.template.vars.Variables; + +/** + * Template expression interface. + */ +public interface URITemplateExpression { + + String expand(Variables vars); +} diff --git a/net/src/main/java/org/xbib/net/template/expression/package-info.java b/net/src/main/java/org/xbib/net/template/expression/package-info.java new file mode 100644 index 0000000..b11b99b --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/expression/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template expressions. + */ +package org.xbib.net.template.expression; diff --git a/net/src/main/java/org/xbib/net/template/package-info.java b/net/src/main/java/org/xbib/net/template/package-info.java new file mode 100644 index 0000000..d9e3c11 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL templates. + */ +package org.xbib.net.template; diff --git a/net/src/main/java/org/xbib/net/template/parse/ExpressionParser.java b/net/src/main/java/org/xbib/net/template/parse/ExpressionParser.java new file mode 100644 index 0000000..4e6cd4b --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/parse/ExpressionParser.java @@ -0,0 +1,66 @@ +package org.xbib.net.template.parse; + +import org.xbib.net.util.CharMatcher; +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.expression.TemplateExpression; +import org.xbib.net.template.expression.URITemplateExpression; +import org.xbib.net.template.vars.specs.VariableSpec; + +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Expression parser. + */ +public class ExpressionParser implements TemplateParser { + + private static final Map EXPRESSION_TYPE_MAP; + static { + EXPRESSION_TYPE_MAP = new HashMap<>(); + EXPRESSION_TYPE_MAP.put('+', ExpressionType.RESERVED); + EXPRESSION_TYPE_MAP.put('#', ExpressionType.FRAGMENT); + EXPRESSION_TYPE_MAP.put('.', ExpressionType.NAME_LABELS); + EXPRESSION_TYPE_MAP.put('/', ExpressionType.PATH_SEGMENTS); + EXPRESSION_TYPE_MAP.put(';', ExpressionType.PATH_PARAMETERS); + EXPRESSION_TYPE_MAP.put('?', ExpressionType.QUERY_STRING); + EXPRESSION_TYPE_MAP.put('&', ExpressionType.QUERY_CONT); + } + private static final CharMatcher COMMA = CharMatcher.is(','); + private static final CharMatcher END_EXPRESSION = CharMatcher.is('}'); + + public ExpressionParser() { + } + + @Override + public URITemplateExpression parse(CharBuffer buffer) { + buffer.get(); + if (!buffer.hasRemaining()) { + throw new IllegalArgumentException("early end of expression"); + } + ExpressionType type = ExpressionType.SIMPLE; + char c = buffer.charAt(0); + if (EXPRESSION_TYPE_MAP.containsKey(c)) { + char s = buffer.get(); + type = EXPRESSION_TYPE_MAP.get(s); + } + List varspecs = new ArrayList<>(); + while (true) { + varspecs.add(VariableSpecParser.parse(buffer)); + if (!buffer.hasRemaining()) { + throw new IllegalArgumentException("early end of expression"); + } + c = buffer.get(); + if (COMMA.matches(c)) { + continue; + } + if (END_EXPRESSION.matches(c)) { + break; + } + throw new IllegalArgumentException("unexpected token"); + } + return new TemplateExpression(type, varspecs); + } +} diff --git a/net/src/main/java/org/xbib/net/template/parse/LiteralParser.java b/net/src/main/java/org/xbib/net/template/parse/LiteralParser.java new file mode 100644 index 0000000..b310634 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/parse/LiteralParser.java @@ -0,0 +1,45 @@ +package org.xbib.net.template.parse; + +import org.xbib.net.util.CharMatcher; +import org.xbib.net.template.expression.TemplateLiteral; +import org.xbib.net.template.expression.URITemplateExpression; + +import java.nio.CharBuffer; + +public class LiteralParser implements TemplateParser { + + public LiteralParser() { + } + + @Override + public URITemplateExpression parse(CharBuffer buffer) { + StringBuilder sb = new StringBuilder(); + char c; + while (buffer.hasRemaining()) { + c = buffer.charAt(0); + if (!CharMatcher.LITERALS.matches(c)) { + break; + } + sb.append(buffer.get()); + if (CharMatcher.PERCENT.matches(c)) { + parsePercentEncoded(buffer, sb); + } + } + return new TemplateLiteral(sb.toString()); + } + + private static void parsePercentEncoded(CharBuffer buffer, StringBuilder sb) { + if (buffer.remaining() < 2) { + throw new IllegalArgumentException("short read"); + } + char first = buffer.get(); + if (!CharMatcher.HEXDIGIT.matches(first)) { + throw new IllegalArgumentException("illegal percent encoding"); + } + char second = buffer.get(); + if (!CharMatcher.HEXDIGIT.matches(second)) { + throw new IllegalArgumentException("illegal percent encoding"); + } + sb.append(first).append(second); + } +} diff --git a/net/src/main/java/org/xbib/net/template/parse/TemplateParser.java b/net/src/main/java/org/xbib/net/template/parse/TemplateParser.java new file mode 100644 index 0000000..ccaa1a1 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/parse/TemplateParser.java @@ -0,0 +1,12 @@ +package org.xbib.net.template.parse; + +import org.xbib.net.template.expression.URITemplateExpression; + +import java.nio.CharBuffer; + +/** + * Template parser interface. + */ +public interface TemplateParser { + URITemplateExpression parse(CharBuffer buffer); +} diff --git a/net/src/main/java/org/xbib/net/template/parse/URITemplateParser.java b/net/src/main/java/org/xbib/net/template/parse/URITemplateParser.java new file mode 100644 index 0000000..972671b --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/parse/URITemplateParser.java @@ -0,0 +1,48 @@ +package org.xbib.net.template.parse; + +import org.xbib.net.util.CharMatcher; +import org.xbib.net.template.expression.URITemplateExpression; + +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * URI template parser. + */ +public class URITemplateParser { + + private static final CharMatcher BEGIN_EXPRESSION = CharMatcher.is('{'); + + private URITemplateParser() { + } + + public static List parse(String input) { + return parse(CharBuffer.wrap(input).asReadOnlyBuffer()); + } + + public static List parse(CharBuffer buffer) { + List ret = new ArrayList<>(); + TemplateParser templateParser; + URITemplateExpression expression; + while (buffer.hasRemaining()) { + templateParser = selectParser(buffer); + expression = templateParser.parse(buffer); + ret.add(expression); + } + return ret; + } + + private static TemplateParser selectParser(CharBuffer buffer) { + char c = buffer.charAt(0); + TemplateParser parser; + if (CharMatcher.LITERALS.matches(c)) { + parser = new LiteralParser(); + } else if (BEGIN_EXPRESSION.matches(c)) { + parser = new ExpressionParser(); + } else { + throw new IllegalArgumentException("no parser"); + } + return parser; + } +} diff --git a/net/src/main/java/org/xbib/net/template/parse/VariableSpecParser.java b/net/src/main/java/org/xbib/net/template/parse/VariableSpecParser.java new file mode 100644 index 0000000..4cc8cce --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/parse/VariableSpecParser.java @@ -0,0 +1,129 @@ +package org.xbib.net.template.parse; + +import org.xbib.net.util.CharMatcher; +import org.xbib.net.template.vars.specs.ExplodedVariable; +import org.xbib.net.template.vars.specs.PrefixVariable; +import org.xbib.net.template.vars.specs.SimpleVariable; +import org.xbib.net.template.vars.specs.VariableSpec; + +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Variable spec parser. + */ +public class VariableSpecParser { + + private static final CharMatcher DIGIT = CharMatcher.inRange('0', '9') + .precomputed(); + + private static final CharMatcher VARCHAR = DIGIT + .or(CharMatcher.inRange('a', 'z')) + .or(CharMatcher.inRange('A', 'Z')) + .or(CharMatcher.is('_')) + .or(CharMatcher.PERCENT) + .precomputed(); + + private static final CharMatcher DOT = CharMatcher.is('.'); + + private static final CharMatcher COLON = CharMatcher.is(':'); + + private static final CharMatcher STAR = CharMatcher.is('*'); + + private VariableSpecParser() { + } + + public static VariableSpec parse(CharBuffer buffer) { + String name = parseFullName(buffer); + if (!buffer.hasRemaining()) { + return new SimpleVariable(name); + } + char c = buffer.charAt(0); + if (STAR.matches(c)) { + buffer.get(); + return new ExplodedVariable(name); + } + if (COLON.matches(c)) { + buffer.get(); + return new PrefixVariable(name, getPrefixLength(buffer)); + } + return new SimpleVariable(name); + } + + private static String parseFullName(CharBuffer buffer) { + List components = new ArrayList<>(); + while (true) { + components.add(readName(buffer)); + if (!buffer.hasRemaining()) { + break; + } + if (!DOT.matches(buffer.charAt(0))) { + break; + } + buffer.get(); + } + return String.join(".", components); + } + + private static String readName(CharBuffer buffer) { + StringBuilder sb = new StringBuilder(); + char c; + while (buffer.hasRemaining()) { + c = buffer.charAt(0); + if (!VARCHAR.matches(c)) { + break; + } + sb.append(buffer.get()); + if (CharMatcher.PERCENT.matches(c)) { + parsePercentEncoded(buffer, sb); + } + } + String ret = sb.toString(); + if (ret.isEmpty()) { + throw new IllegalArgumentException("empty var name"); + } + return ret; + } + + private static void parsePercentEncoded(CharBuffer buffer, StringBuilder sb) { + if (buffer.remaining() < 2) { + throw new IllegalArgumentException("short read"); + } + char first = buffer.get(); + if (!CharMatcher.HEXDIGIT.matches(first)) { + throw new IllegalArgumentException("illegal percent encoding"); + } + char second = buffer.get(); + if (!CharMatcher.HEXDIGIT.matches(second)) { + throw new IllegalArgumentException("illegal percent encoding"); + } + sb.append(first).append(second); + } + + private static int getPrefixLength(CharBuffer buffer) { + StringBuilder sb = new StringBuilder(); + char c; + while (buffer.hasRemaining()) { + c = buffer.charAt(0); + if (!DIGIT.matches(c)) { + break; + } + sb.append(buffer.get()); + } + String s = sb.toString(); + if (s.isEmpty()) { + throw new IllegalArgumentException("empty prefix"); + } + int ret; + try { + ret = Integer.parseInt(s); + if (ret > 10000) { + throw new NumberFormatException(); + } + return ret; + } catch (NumberFormatException ignored) { + throw new IllegalArgumentException("prefix invalid / too large"); + } + } +} diff --git a/net/src/main/java/org/xbib/net/template/parse/package-info.java b/net/src/main/java/org/xbib/net/template/parse/package-info.java new file mode 100644 index 0000000..b711206 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/parse/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template parsers. + */ +package org.xbib.net.template.parse; diff --git a/net/src/main/java/org/xbib/net/template/render/ListRenderer.java b/net/src/main/java/org/xbib/net/template/render/ListRenderer.java new file mode 100644 index 0000000..c161959 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/render/ListRenderer.java @@ -0,0 +1,50 @@ +package org.xbib.net.template.render; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Liste renderer. + */ +public class ListRenderer extends MultiValueRenderer { + + public ListRenderer(ExpressionType type) { + super(type); + } + + @Override + protected List renderNamedExploded(String varname, VariableValue value) { + return value.getListValue().stream().map(element -> + element.isEmpty() ? varname + ifEmpty : varname + '=' + pctEncode(element) + ).collect(Collectors.toList()); + } + + @Override + protected List renderUnnamedExploded(VariableValue value) { + return value.getListValue().stream().map(this::pctEncode).collect(Collectors.toList()); + } + + @Override + protected List renderNamedNormal(String varname, VariableValue value) { + StringBuilder sb = new StringBuilder(varname); + if (value.isEmpty()) { + return Collections.singletonList(sb.append(ifEmpty).toString()); + } + sb.append('='); + List elements = value.getListValue().stream().map(this::pctEncode).collect(Collectors.toList()); + return Collections.singletonList(sb.toString() + String.join(",", elements)); + } + + @Override + protected List renderUnnamedNormal(VariableValue value) { + if (value.isEmpty()) { + return Collections.emptyList(); + } + List elements = value.getListValue().stream().map(this::pctEncode).collect(Collectors.toList()); + return Collections.singletonList(String.join(",", elements)); + } +} diff --git a/net/src/main/java/org/xbib/net/template/render/MapRenderer.java b/net/src/main/java/org/xbib/net/template/render/MapRenderer.java new file mode 100644 index 0000000..4b1080e --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/render/MapRenderer.java @@ -0,0 +1,62 @@ +package org.xbib.net.template.render; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Map renderer. + */ +public class MapRenderer extends MultiValueRenderer { + + public MapRenderer(ExpressionType type) { + super(type); + } + + @Override + protected List renderNamedExploded(String varname, VariableValue value) { + List ret = new ArrayList<>(); + value.getMapValue().forEach((k, v) -> ret.add(pctEncode(k) + (v.isEmpty() ? ifEmpty : '=' + pctEncode(v)))); + return ret; + } + + @Override + protected List renderUnnamedExploded(VariableValue value) { + List ret = new ArrayList<>(); + value.getMapValue().forEach((k, v) -> ret.add(pctEncode(k) + '=' + pctEncode(v))); + return ret; + } + + @Override + protected List renderNamedNormal(String varname, VariableValue value) { + StringBuilder sb = new StringBuilder(varname); + if (value.isEmpty()) { + return Collections.singletonList(sb.append(ifEmpty).toString()); + } + sb.append('='); + List elements = mapAsList(value).stream().map(this::pctEncode).collect(Collectors.toList()); + return Collections.singletonList(sb.toString() + String.join(",", elements)); + } + + @Override + protected List renderUnnamedNormal(VariableValue value) { + if (value.isEmpty()) { + return Collections.emptyList(); + } + List elements = mapAsList(value).stream().map(this::pctEncode).collect(Collectors.toList()); + return Collections.singletonList(String.join(",", elements)); + } + + private static List mapAsList(VariableValue value) { + List ret = new ArrayList<>(); + value.getMapValue().forEach((k, v) -> { + ret.add(k); + ret.add(v); + }); + return ret; + } +} diff --git a/net/src/main/java/org/xbib/net/template/render/MultiValueRenderer.java b/net/src/main/java/org/xbib/net/template/render/MultiValueRenderer.java new file mode 100644 index 0000000..6bf0dca --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/render/MultiValueRenderer.java @@ -0,0 +1,49 @@ +package org.xbib.net.template.render; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.List; + +/** + * Multi value renderer. + */ +abstract class MultiValueRenderer extends ValueRenderer { + + MultiValueRenderer(ExpressionType type) { + super(type); + } + + @Override + public List render(VariableSpec varspec, VariableValue value) { + if (varspec.getPrefixLength() != -1) { + throw new IllegalArgumentException("incompatible var spec value"); + } + String varname = varspec.getName(); + return named ? + (varspec.isExploded() ? renderNamedExploded(varname, value) : renderNamedNormal(varname, value)) : + (varspec.isExploded() ? renderUnnamedExploded(value) : renderUnnamedNormal(value)); + } + + protected abstract List renderNamedExploded(String varname, VariableValue value); + + protected abstract List renderUnnamedExploded(VariableValue value); + + /** + * Rendering method for named expressions and non exploded varspecs. + * + * @param varname name of the variable (used in lists) + * @param value value of the variable + * @return list of rendered elements + */ + protected abstract List renderNamedNormal(String varname, VariableValue value); + + /** + * Rendering method for non named expressions and non exploded varspecs. + * + * @param value value of the variable + * @return list of rendered elements + */ + protected abstract List renderUnnamedNormal(VariableValue value); +} diff --git a/net/src/main/java/org/xbib/net/template/render/NullRenderer.java b/net/src/main/java/org/xbib/net/template/render/NullRenderer.java new file mode 100644 index 0000000..181c76d --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/render/NullRenderer.java @@ -0,0 +1,22 @@ +package org.xbib.net.template.render; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.List; + +/** + * Null renderer. + */ +public class NullRenderer extends ValueRenderer { + + public NullRenderer(ExpressionType type) { + super(type); + } + + @Override + public List render(VariableSpec varspec, VariableValue value) { + return null; + } +} diff --git a/net/src/main/java/org/xbib/net/template/render/StringRenderer.java b/net/src/main/java/org/xbib/net/template/render/StringRenderer.java new file mode 100644 index 0000000..4fa9d69 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/render/StringRenderer.java @@ -0,0 +1,53 @@ +package org.xbib.net.template.render; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.Collections; +import java.util.List; + +/** + * String renderer. + */ +public class StringRenderer extends ValueRenderer { + + public StringRenderer(ExpressionType type) { + super(type); + } + + @Override + public List render(VariableSpec varspec, VariableValue value) { + return Collections.singletonList(doRender(varspec, value.getScalarValue())); + } + + private String doRender(VariableSpec varspec, String value) { + if (value == null) { + return ""; + } + StringBuilder sb = new StringBuilder(value.length()); + if (named) { + sb.append(varspec.getName()); + if (value.isEmpty()) { + return sb.append(ifEmpty).toString(); + } + sb.append('='); + } + int prefixLen = varspec.getPrefixLength(); + if (prefixLen == -1) { + return sb.append(pctEncode(value)).toString(); + } + int len = value.codePointCount(0, value.length()); + return len <= prefixLen ? + sb.append(pctEncode(value)).toString() : + sb.append(pctEncode(nFirstChars(value, prefixLen))).toString(); + } + + private static String nFirstChars(String s, int n) { + int realIndex = n; + while (s.codePointCount(0, realIndex) != n) { + realIndex++; + } + return s.substring(0, realIndex); + } +} diff --git a/net/src/main/java/org/xbib/net/template/render/ValueRenderer.java b/net/src/main/java/org/xbib/net/template/render/ValueRenderer.java new file mode 100644 index 0000000..2e5fa43 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/render/ValueRenderer.java @@ -0,0 +1,80 @@ +package org.xbib.net.template.render; + +import org.xbib.net.PercentEncoder; +import org.xbib.net.PercentEncoders; +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.values.VariableValue; + +import java.nio.charset.CharacterCodingException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * The algorithm used for rendering is centered around this class, and is + * adapted from the algorithm suggested in the RFC's appendix. + * + * Eventually, rendering can be viewed as joining a list of rendered strings + * with the expression type separator; if the resulting list is empty, the end + * result is the empty string; otherwise, it is the expression's prefix string + * (if any) followed by the joined list of rendered strings. + * + * This class renders one variable value according to the expression type and + * value type. The rendering method returns a list, which can be empty. + */ +public abstract class ValueRenderer { + /** + * Whether variable values are named during expansion. + */ + protected final boolean named; + + /** + * Substitution string for an empty value/list member/map value. + */ + protected final String ifEmpty; + + /** + * The percent encoder. + */ + private final PercentEncoder percentEncoder; + + protected ValueRenderer(ExpressionType type) { + named = type.isNamed(); + ifEmpty = type.getIfEmpty(); + switch (type) { + case RESERVED: + case FRAGMENT: + percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); + break; + default: + percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); + break; + } + } + + /** + * Render a value given a varspec and value. + * + * @param varspec the varspec + * @param value the matching variable value + * @return a list of rendered strings + */ + public abstract List render(VariableSpec varspec, VariableValue value); + + /** + * Render a string value, doing character percent-encoding where needed. + * + * The character set on which to perform percent encoding is dependent + * on the expression type. + * + * @param s the string to encode + * @return an encoded string + */ + protected String pctEncode(String s) { + try { + return percentEncoder.encode(s); + } catch (CharacterCodingException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/net/src/main/java/org/xbib/net/template/render/package-info.java b/net/src/main/java/org/xbib/net/template/render/package-info.java new file mode 100644 index 0000000..411e81b --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/render/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template renderers. + */ +package org.xbib.net.template.render; diff --git a/net/src/main/java/org/xbib/net/template/vars/Variables.java b/net/src/main/java/org/xbib/net/template/vars/Variables.java new file mode 100644 index 0000000..cb4c8af --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/Variables.java @@ -0,0 +1,132 @@ +package org.xbib.net.template.vars; + +import org.xbib.net.template.vars.values.ListValue; +import org.xbib.net.template.vars.values.MapValue; +import org.xbib.net.template.vars.values.ScalarValue; +import org.xbib.net.template.vars.values.VariableValue; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Variables. + */ +public class Variables { + + private final Map vars; + + private Variables(Builder builder) { + this.vars = builder.vars; + } + + /** + * Create a new builder for this class. + * + * @return a {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Get the value associated with a variable name. + * + * @param varname the variable name + * @return the value, or {@code null} if there is no matching value + */ + public VariableValue get(String varname) { + return vars.get(varname); + } + + @Override + public String toString() { + return vars.toString(); + } + + /** + * A Builder for variables. + */ + public static class Builder { + + private final Map vars = new LinkedHashMap<>(); + + Builder() { + } + + /** + * Associate a map, list, or object to a variable name. + * + * @param varname the variable name + * @param value the value, as a {@link VariableValue} + * @return this + */ + @SuppressWarnings("unchecked") + public Builder add(String varname, Object value) { + if (value instanceof VariableValue) { + addValue(varname, (VariableValue) value); + } else if (value instanceof Map) { + addValue(varname, (Map) value); + } else if (value instanceof List) { + addValue(varname, (List) value); + } else { + addValue(varname, new ScalarValue(value)); + } + return this; + } + + /** + * Associate a value to a variable name. + * + * @param varname the variable name + * @param value the value, as a {@link VariableValue} + * @return this + */ + private Builder addValue(String varname, VariableValue value) { + vars.put(varname, value); + return this; + } + + /** + * Shortcut method to associate a name with a list value. + * Any {@link Iterable} can be used (thereby including all collections: + * sets, lists, etc). Note that it is your responsibility that objects in + * this iterable implement {@link Object#toString()} correctly. + * + * @param varname the variable name + * @param iterable the iterable + * @return this + */ + private Builder addValue(String varname, Iterable iterable) { + return add(varname, ListValue.copyOf(iterable)); + } + + /** + * Method to associate a variable name to a map value. + * Values of the map can be of any type. You should ensure that they + * implement {@link Object#toString()} correctly. + * + * @param varname the variable name + * @param map the map + * @return this + */ + private Builder addValue(String varname, Map map) { + return add(varname, MapValue.copyOf(map)); + } + + /** + * Add all variable definitions from another variable map. + * @param other the other variable map to copy definitions from + * @return this + * @throws NullPointerException other variable map is null + */ + public Builder add(Variables other) { + vars.putAll(other.vars); + return this; + } + + public Variables build() { + return new Variables(this); + } + } +} diff --git a/net/src/main/java/org/xbib/net/template/vars/package-info.java b/net/src/main/java/org/xbib/net/template/vars/package-info.java new file mode 100644 index 0000000..70bf35a --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template variables. + */ +package org.xbib.net.template.vars; diff --git a/net/src/main/java/org/xbib/net/template/vars/specs/ExplodedVariable.java b/net/src/main/java/org/xbib/net/template/vars/specs/ExplodedVariable.java new file mode 100644 index 0000000..a7443f7 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/specs/ExplodedVariable.java @@ -0,0 +1,46 @@ +package org.xbib.net.template.vars.specs; + +/** + * Exploded variable. + */ +public class ExplodedVariable extends VariableSpec { + + public ExplodedVariable(String name) { + super(VariableSpecType.EXPLODED, name); + } + + @Override + public boolean isExploded() { + return true; + } + + @Override + public int getPrefixLength() { + return -1; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + ExplodedVariable other = (ExplodedVariable) obj; + return name.equals(other.name); + } + + @Override + public String toString() { + return name + " (exploded)"; + } +} diff --git a/net/src/main/java/org/xbib/net/template/vars/specs/PrefixVariable.java b/net/src/main/java/org/xbib/net/template/vars/specs/PrefixVariable.java new file mode 100644 index 0000000..d9042ae --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/specs/PrefixVariable.java @@ -0,0 +1,49 @@ +package org.xbib.net.template.vars.specs; + +/** + * A varspec with a prefix modifier (for instance, {@code foo:3} in {@code {foo:3}}. + */ +public class PrefixVariable extends VariableSpec { + + private final int length; + + public PrefixVariable(String name, int length) { + super(VariableSpecType.PREFIX, name); + this.length = length; + } + + @Override + public boolean isExploded() { + return false; + } + + @Override + public int getPrefixLength() { + return length; + } + + @Override + public int hashCode() { + return 31 * name.hashCode() + length; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + PrefixVariable other = (PrefixVariable) obj; + return name.equals(other.name) && length == other.length; + } + + @Override + public String toString() { + return name + " (prefix length: " + length + ')'; + } +} diff --git a/net/src/main/java/org/xbib/net/template/vars/specs/SimpleVariable.java b/net/src/main/java/org/xbib/net/template/vars/specs/SimpleVariable.java new file mode 100644 index 0000000..f06bb4c --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/specs/SimpleVariable.java @@ -0,0 +1,46 @@ +package org.xbib.net.template.vars.specs; + +/** + * A varspec without modifier (for instance, {@code foo} in {@code {foo}}. + */ +public class SimpleVariable extends VariableSpec { + + public SimpleVariable(String name) { + super(VariableSpecType.SIMPLE, name); + } + + @Override + public boolean isExploded() { + return false; + } + + @Override + public int getPrefixLength() { + return -1; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + SimpleVariable other = (SimpleVariable) obj; + return name.equals(other.name); + } + + @Override + public String toString() { + return name + " (simple)"; + } +} diff --git a/net/src/main/java/org/xbib/net/template/vars/specs/VariableSpec.java b/net/src/main/java/org/xbib/net/template/vars/specs/VariableSpec.java new file mode 100644 index 0000000..cc1ef96 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/specs/VariableSpec.java @@ -0,0 +1,66 @@ +package org.xbib.net.template.vars.specs; + +/** + * A variable specifier. + * + * A template expression can have one or more variable specifiers. For + * instance, in {@code {+path:3,var}}, variable specifiers are {@code path:3} + * and {@code var}. + * + * This class records the name of this specifier and its modifier, if any. + */ +public abstract class VariableSpec { + + protected final String name; + + private final VariableSpecType type; + + protected VariableSpec(VariableSpecType type, String name) { + this.type = type; + this.name = name; + } + + /** + * Get the modifier type for this var spec. + * + * @return the modifier type + */ + public final VariableSpecType getType() { + return type; + } + + /** + * Get the name for this var spec. + * + * @return the name + */ + public final String getName() { + return name; + } + + /** + * Tell whether this varspec has an explode modifier. + * + * @return true if an explode modifier is present + */ + public abstract boolean isExploded(); + + /** + * Return the prefix length for this varspec. + * + * Returns -1 if no prefix length is specified. Recall: valid values are + * integers between 0 and 10000. + * + * @return the prefix length, or -1 if no prefix modidifer + */ + public abstract int getPrefixLength(); + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object obj); + + @Override + public abstract String toString(); +} diff --git a/net/src/main/java/org/xbib/net/template/vars/specs/VariableSpecType.java b/net/src/main/java/org/xbib/net/template/vars/specs/VariableSpecType.java new file mode 100644 index 0000000..a8230a6 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/specs/VariableSpecType.java @@ -0,0 +1,21 @@ +package org.xbib.net.template.vars.specs; + +/** + * Enumeration of a variable modifier type. + */ +public enum VariableSpecType { + /** + * No modifier. + */ + SIMPLE, + /** + * Prefix modifier ({@code :xxx} where {@code xxx} is an integer). + * Only makes sense for string values. + */ + PREFIX, + /** + * Explode modifier ({@code *}). + * Only makes sense for list and map values. + */ + EXPLODED +} diff --git a/net/src/main/java/org/xbib/net/template/vars/specs/package-info.java b/net/src/main/java/org/xbib/net/template/vars/specs/package-info.java new file mode 100644 index 0000000..c1ddd4c --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/specs/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template var specs. + */ +package org.xbib.net.template.vars.specs; diff --git a/net/src/main/java/org/xbib/net/template/vars/values/ListValue.java b/net/src/main/java/org/xbib/net/template/vars/values/ListValue.java new file mode 100644 index 0000000..bb4f770 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/values/ListValue.java @@ -0,0 +1,104 @@ +package org.xbib.net.template.vars.values; + +import java.util.ArrayList; +import java.util.List; + +/** + * List value. + */ +public class ListValue extends VariableValue { + + private final List list; + + private ListValue(Builder builder) { + super(ValueType.ARRAY); + list = builder.list; + } + + /** + * Create a new list value builder. + * + * @return a builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Build a list value out of an existing iterable (list, set, other). + * + * This calls {@link Builder#addAll(Iterable)} internally. + * + * @param iterable the iterable + * @param the type of iterable elements + * @return a new list value + */ + public static VariableValue copyOf(Iterable iterable) { + return new Builder().addAll(iterable).build(); + } + + @Override + public List getListValue() { + return list; + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public String toString() { + return list.toString(); + } + + /** + * Builder class for a {@link ListValue}. + */ + public static class Builder { + + private final List list = new ArrayList<>(); + + Builder() { + } + + /** + * Add a series of elements to this list. + * + * @param first first element + * @param other other elements, if any + * @return this + * @throws NullPointerException one argument at least is null + */ + public Builder add(Object first, Object... other) { + list.add(first.toString()); + for (Object o : other) { + list.add(o.toString()); + } + return this; + } + + /** + * Add elements from an iterable. + * + * @param iterable the iterable + * @param type of elements in the iterable + * @return this + */ + public Builder addAll(Iterable iterable) { + for (T t : iterable) { + list.add(t.toString()); + } + return this; + } + + /** + * Build the value. + * + * @return the list value as a {@link VariableValue} + */ + public VariableValue build() { + return new ListValue(this); + } + } +} diff --git a/net/src/main/java/org/xbib/net/template/vars/values/MapValue.java b/net/src/main/java/org/xbib/net/template/vars/values/MapValue.java new file mode 100644 index 0000000..12c9f03 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/values/MapValue.java @@ -0,0 +1,104 @@ +package org.xbib.net.template.vars.values; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Map value. + */ +public class MapValue extends VariableValue { + + private final Map map; + + private MapValue(Builder builder) { + super(ValueType.MAP); + map = builder.map; + } + + /** + * Create a new builder for this class. + * + * @return a {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Convenience method to build a variable value from an existing {@link Map}. + * + * @param map the map + * @param the type of values in this map + * @return a new map value as a {@link VariableValue} + * @throws NullPointerException map is null, or one of its keys or values + * is null + */ + public static VariableValue copyOf(Map map) { + return builder().putAll(map).build(); + } + + @Override + public Map getMapValue() { + return map; + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public String toString() { + return map.toString(); + } + + /** + * Builder class for a {@link MapValue}. + */ + public static class Builder { + + private final Map map = new LinkedHashMap<>(); + + Builder() { + } + + /** + * Add one key/value pair to the map. + * + * @param key the key + * @param value the value + * @param the type of the value + * @return this + * @throws NullPointerException the key or value is null + */ + public Builder put(String key, T value) { + map.put(key, value.toString()); + return this; + } + + /** + * Inject a map of key/value pairs. + * + * @param map the map + * @param the type of this map's values + * @return this + * @throws NullPointerException map is null, or one of its keys or + * values is null + */ + public Builder putAll(Map map) { + for (Map.Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Build the value. + * + * @return the map value as a {@link VariableValue} + */ + public VariableValue build() { + return new MapValue(this); + } + } +} diff --git a/net/src/main/java/org/xbib/net/template/vars/values/NullValue.java b/net/src/main/java/org/xbib/net/template/vars/values/NullValue.java new file mode 100644 index 0000000..059fd92 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/values/NullValue.java @@ -0,0 +1,16 @@ +package org.xbib.net.template.vars.values; + +/** + * Null value. + */ +public class NullValue extends VariableValue { + + public NullValue() { + super(ValueType.NULL); + } + + @Override + public boolean isEmpty() { + return true; + } +} diff --git a/net/src/main/java/org/xbib/net/template/vars/values/ScalarValue.java b/net/src/main/java/org/xbib/net/template/vars/values/ScalarValue.java new file mode 100644 index 0000000..56938e2 --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/values/ScalarValue.java @@ -0,0 +1,29 @@ +package org.xbib.net.template.vars.values; + +/** + * Scalar value. + */ +public class ScalarValue extends VariableValue { + + private final String value; + + public ScalarValue(Object value) { + super(ValueType.SCALAR); + this.value = (String) value; + } + + @Override + public String getScalarValue() { + return value; + } + + @Override + public boolean isEmpty() { + return value.isEmpty(); + } + + @Override + public String toString() { + return value; + } +} diff --git a/net/src/main/java/org/xbib/net/template/vars/values/ValueType.java b/net/src/main/java/org/xbib/net/template/vars/values/ValueType.java new file mode 100644 index 0000000..dbff65c --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/values/ValueType.java @@ -0,0 +1,69 @@ +package org.xbib.net.template.vars.values; + +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.render.ListRenderer; +import org.xbib.net.template.render.MapRenderer; +import org.xbib.net.template.render.NullRenderer; +import org.xbib.net.template.render.StringRenderer; +import org.xbib.net.template.render.ValueRenderer; + +/** + * Value types. + */ +public enum ValueType { + + NULL("null") { + @Override + public ValueRenderer selectRenderer(ExpressionType type) { + return new NullRenderer(type); + } + }, + /** + * Render scalar values (simple string values). + */ + SCALAR("scalar") { + @Override + public ValueRenderer selectRenderer(ExpressionType type) { + return new StringRenderer(type); + } + }, + /** + * Render array/list values. + */ + ARRAY("list") { + @Override + public ValueRenderer selectRenderer(ExpressionType type) { + return new ListRenderer(type); + } + }, + /** + * Map values. + * + * Note: the RFC calls these "associative arrays". + */ + MAP("map") { + @Override + public ValueRenderer selectRenderer(ExpressionType type) { + return new MapRenderer(type); + } + }; + + private final String name; + + ValueType(String name) { + this.name = name; + } + + /** + * Get the renderer for this value type and expression type. + * + * @param type the expression type + * @return the appropriate renderer + */ + public abstract ValueRenderer selectRenderer(ExpressionType type); + + @Override + public String toString() { + return name; + } +} diff --git a/net/src/main/java/org/xbib/net/template/vars/values/VariableValue.java b/net/src/main/java/org/xbib/net/template/vars/values/VariableValue.java new file mode 100644 index 0000000..ad17a6b --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/values/VariableValue.java @@ -0,0 +1,67 @@ +package org.xbib.net.template.vars.values; + +import java.util.List; +import java.util.Map; + +/** + * Variable value. + */ +public abstract class VariableValue { + + private final ValueType type; + + VariableValue(ValueType type) { + this.type = type; + } + + /** + * Get the type for this value. + * + * @return the value type + */ + public ValueType getType() { + return type; + } + + /** + * Get a simple string for this value. + * Only valid for string values. + * + * @return the string + * @throws IllegalArgumentException value is not a string value + */ + public String getScalarValue() { + throw new IllegalArgumentException("not a scalar"); + } + + /** + * Get a list for this value. + * Only valid for list values. + * + * @return the list + * @throws IllegalArgumentException value is not a list value + */ + public List getListValue() { + throw new IllegalArgumentException("not a list"); + } + + /** + * Get a map for this value. + * Only valid for map values. + * + * @return the map + * @throws IllegalArgumentException value is not a map value + */ + public Map getMapValue() { + throw new IllegalArgumentException("not a map"); + } + + /** + * Tell whether this value is empty. + * For strings, this tells whether the string itself is empty. For lists + * and maps, this tells whether the list or map have no elements/entries. + * + * @return true if the value is empty + */ + public abstract boolean isEmpty(); +} diff --git a/net/src/main/java/org/xbib/net/template/vars/values/package-info.java b/net/src/main/java/org/xbib/net/template/vars/values/package-info.java new file mode 100644 index 0000000..7ef9cfe --- /dev/null +++ b/net/src/main/java/org/xbib/net/template/vars/values/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for URL template variables values. + */ +package org.xbib.net.template.vars.values; diff --git a/net/src/main/java/org/xbib/net/util/ByteBufferInputStream.java b/net/src/main/java/org/xbib/net/util/ByteBufferInputStream.java new file mode 100644 index 0000000..7ab924b --- /dev/null +++ b/net/src/main/java/org/xbib/net/util/ByteBufferInputStream.java @@ -0,0 +1,70 @@ +package org.xbib.net.util; + +import java.io.InputStream; +import java.nio.ByteBuffer; + +public class ByteBufferInputStream extends InputStream { + + private final ByteBuffer byteBuffer; + + public ByteBufferInputStream(ByteBuffer byteBuffer) { + byteBuffer.mark(); + this.byteBuffer = byteBuffer; + } + + @Override + public int read() { + if (!byteBuffer.hasRemaining()) { + return -1; + } + return byteBuffer.get() & 0xFF; + } + + @Override + public int read(byte[] bytes, int offset, int length) { + if (length == 0) { + return 0; + } + int count = Math.min(byteBuffer.remaining(), length); + if (count == 0) { + return -1; + } + byteBuffer.get(bytes, offset, count); + return count; + } + + @Override + public int available() { + return byteBuffer.remaining(); + } + + @Override + public long skip(long n) { + if (n < 0L) { + return 0L; + } + int skipped = Math.min((int) n, byteBuffer.remaining()); + byteBuffer.position(byteBuffer.position() + skipped); + return skipped; + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void mark(int readAheadLimit) { + byteBuffer.mark(); + } + + @Override + public void reset() { + byteBuffer.reset(); + } + + @Override + public void close() { + byteBuffer.position(byteBuffer.limit()); + } +} diff --git a/net/src/main/java/org/xbib/net/util/CharMatcher.java b/net/src/main/java/org/xbib/net/util/CharMatcher.java new file mode 100644 index 0000000..5f832c3 --- /dev/null +++ b/net/src/main/java/org/xbib/net/util/CharMatcher.java @@ -0,0 +1,790 @@ +package org.xbib.net.util; + +import java.util.Arrays; +import java.util.BitSet; + +/** + * The character matcher class is a fast table-based matcher class, able to match whitespace, control, + * literals, persent or hexdigit character groups. + */ +public abstract class CharMatcher { + + private static final String WHITESPACE_TABLE; + + private static final int WHITESPACE_MULTIPLIER; + + private static final int WHITESPACE_SHIFT; + + private static final CharMatcher WHITESPACE; + + private static final CharMatcher JAVA_ISO_CONTROL; + + public static final CharMatcher LITERALS; + + public static final CharMatcher PERCENT; + + public static final CharMatcher HEXDIGIT; + + static { + WHITESPACE_TABLE = "\u2002\u3000\r\u0085\u200A\u2005\u2000\u3000\u2029\u000B\u3000\u2008\u2003\u205F\u3000" + + "\u1680\u0009\u0020\u2006\u2001\u202F\u00A0\u000C\u2009\u3000\u2004\u3000\u3000\u2028\n\u2007\u3000"; + WHITESPACE_MULTIPLIER = 1682554634; + WHITESPACE_SHIFT = Integer.numberOfLeadingZeros(WHITESPACE_TABLE.length() - 1); + WHITESPACE = new FastMatcher() { + @Override + public boolean matches(char c) { + return WHITESPACE_TABLE.charAt((WHITESPACE_MULTIPLIER * c) >>> WHITESPACE_SHIFT) == c; + } + + @Override + void setBits(BitSet table) { + for (int i = 0; i < WHITESPACE_TABLE.length(); i++) { + table.set(WHITESPACE_TABLE.charAt(i)); + } + } + }; + JAVA_ISO_CONTROL = inRange('\u0000', '\u001f') + .or(inRange('\u007f', '\u009f')); + LITERALS = CharMatcher.JAVA_ISO_CONTROL + .or(CharMatcher.WHITESPACE) + .or(CharMatcher.anyOf("\"'<>\\^`{|}")) + .precomputed().negate(); + PERCENT = CharMatcher.is('%'); + HEXDIGIT = CharMatcher.inRange('0', '9') + .or(CharMatcher.inRange('a', 'f')) + .or(CharMatcher.inRange('A', 'F')) + .precomputed(); + } + + private static final int DISTINCT_CHARS = (Character.MAX_VALUE) - (Character.MIN_VALUE) + 1; + + private static T checkNotNull(T reference) { + if (reference == null) { + throw new NullPointerException(); + } + return reference; + } + + private static void checkArgument(boolean expression) { + if (!expression) { + throw new IllegalArgumentException(); + } + } + + private static int checkPositionIndex(int index, int size) { + if (index < 0 || index > size) { + throw new IndexOutOfBoundsException("index=" + index + " size=" + size); + } + return index; + } + + private static final CharMatcher ANY = new FastMatcher() { + @Override + public boolean matches(char c) { + return true; + } + + @Override + int indexIn(CharSequence sequence) { + return sequence.length() == 0 ? -1 : 0; + } + + @Override + int indexIn(CharSequence sequence, int start) { + int length = sequence.length(); + checkPositionIndex(start, length); + return start == length ? -1 : start; + } + + @Override + int lastIndexIn(CharSequence sequence) { + return sequence.length() - 1; + } + + @Override + public boolean matchesAllOf(CharSequence sequence) { + checkNotNull(sequence); + return true; + } + + @Override + public boolean matchesNoneOf(CharSequence sequence) { + return sequence.length() == 0; + } + + @Override + String removeFrom(CharSequence sequence) { + checkNotNull(sequence); + return ""; + } + + @Override + String replaceFrom(CharSequence sequence, char replacement) { + char[] array = new char[sequence.length()]; + Arrays.fill(array, replacement); + return new String(array); + } + + @Override + int countIn(CharSequence sequence) { + return sequence.length(); + } + + @Override + public CharMatcher and(CharMatcher other) { + return checkNotNull(other); + } + + @Override + public CharMatcher or(CharMatcher other) { + checkNotNull(other); + return this; + } + + @Override + public CharMatcher negate() { + return NONE; + } + }; + + private static final CharMatcher NONE = new FastMatcher() { + @Override + public boolean matches(char c) { + return false; + } + + @Override + int indexIn(CharSequence sequence) { + checkNotNull(sequence); + return -1; + } + + @Override + int indexIn(CharSequence sequence, int start) { + int length = sequence.length(); + checkPositionIndex(start, length); + return -1; + } + + @Override + int lastIndexIn(CharSequence sequence) { + checkNotNull(sequence); + return -1; + } + + @Override + public boolean matchesAllOf(CharSequence sequence) { + return sequence.length() == 0; + } + + @Override + public boolean matchesNoneOf(CharSequence sequence) { + checkNotNull(sequence); + return true; + } + + @Override + String removeFrom(CharSequence sequence) { + return sequence.toString(); + } + + @Override + String replaceFrom(CharSequence sequence, char replacement) { + return sequence.toString(); + } + + @Override + int countIn(CharSequence sequence) { + checkNotNull(sequence); + return 0; + } + + @Override + public CharMatcher and(CharMatcher other) { + checkNotNull(other); + return this; + } + + @Override + public CharMatcher or(CharMatcher other) { + return checkNotNull(other); + } + + @Override + public CharMatcher negate() { + return ANY; + } + }; + + public static CharMatcher is(char match) { + return new FastMatcher() { + @Override + public boolean matches(char c) { + return c == match; + } + + @Override + String replaceFrom(CharSequence sequence, char replacement) { + return sequence.toString().replace(match, replacement); + } + + @Override + public CharMatcher and(CharMatcher other) { + return other.matches(match) ? this : NONE; + } + + @Override + public CharMatcher or(CharMatcher other) { + return other.matches(match) ? other : super.or(other); + } + + @Override + public CharMatcher negate() { + return isNot(match); + } + + @Override + void setBits(BitSet table) { + table.set((int) match); + } + }; + } + + public static CharMatcher isNot(char match) { + return new FastMatcher() { + @Override + public boolean matches(char c) { + return c != match; + } + + @Override + public CharMatcher and(CharMatcher other) { + return other.matches(match) ? super.and(other) : other; + } + + @Override + public CharMatcher or(CharMatcher other) { + return other.matches(match) ? ANY : this; + } + + @Override + void setBits(BitSet table) { + table.set(0, match); + table.set((match) + 1, (Character.MAX_VALUE) + 1); + } + + @Override + public CharMatcher negate() { + return is(match); + } + }; + } + + public static CharMatcher anyOf(CharSequence sequence) { + switch (sequence.length()) { + case 0: + return NONE; + case 1: + return is(sequence.charAt(0)); + case 2: + return isEither(sequence.charAt(0), sequence.charAt(1)); + default: + break; + } + char[] chars = sequence.toString().toCharArray(); + Arrays.sort(chars); + return new CharMatcher() { + @Override + public boolean matches(char c) { + return Arrays.binarySearch(chars, c) >= 0; + } + + @Override + void setBits(BitSet table) { + for (char c : chars) { + table.set(c); + } + } + }; + } + + public static CharMatcher isEither(char match1, char match2) { + return new FastMatcher() { + @Override + public boolean matches(char c) { + return c == match1 || c == match2; + } + + @Override + void setBits(BitSet table) { + table.set(match1); + table.set(match2); + } + }; + } + + public static CharMatcher noneOf(CharSequence sequence) { + return anyOf(sequence).negate(); + } + + public static CharMatcher inRange(char startInclusive, char endInclusive) { + checkArgument(endInclusive >= startInclusive); + return new FastMatcher() { + @Override + public boolean matches(char c) { + return startInclusive <= c && c <= endInclusive; + } + + @Override + void setBits(BitSet table) { + table.set(startInclusive, (endInclusive) + 1); + } + }; + } + + public static CharMatcher ascii() { + return Ascii.INSTANCE; + } + + public static CharMatcher javaIsoControl() { + return JAVA_ISO_CONTROL; + } + + protected CharMatcher() { + } + + public abstract boolean matches(char c); + + public CharMatcher negate() { + return new NegatedMatcher(this); + } + + public CharMatcher and(CharMatcher other) { + return new And(this, checkNotNull(other)); + } + + public CharMatcher or(CharMatcher other) { + return new Or(this, other); + } + + public CharMatcher precomputed() { + return precomputedInternal(); + } + + private CharMatcher precomputedInternal() { + BitSet table = new BitSet(); + setBits(table); + int totalCharacters = table.cardinality(); + if (totalCharacters * 2 <= DISTINCT_CHARS) { + return precomputedPositive(totalCharacters, table); + } else { + table.flip(Character.MIN_VALUE, (Character.MAX_VALUE) + 1); + int negatedCharacters = DISTINCT_CHARS - totalCharacters; + return new NegatedFastMatcher(precomputedPositive(negatedCharacters, table)); + } + } + + private static CharMatcher precomputedPositive(int totalCharacters, BitSet table) { + switch (totalCharacters) { + case 0: + return NONE; + case 1: + return is((char) table.nextSetBit(0)); + case 2: + char c1 = (char) table.nextSetBit(0); + char c2 = (char) table.nextSetBit((c1) + 1); + return isEither(c1, c2); + default: + return isSmall(totalCharacters, table.length()) ? + SmallCharMatcher.from(table) : new BitSetMatcher(table); + } + } + + private static boolean isSmall(int totalCharacters, int tableLength) { + return totalCharacters <= SmallCharMatcher.MAX_SIZE && + tableLength > (totalCharacters * 4 * Character.SIZE); + } + + void setBits(BitSet table) { + for (int c = Character.MAX_VALUE; c >= Character.MIN_VALUE; c--) { + if (matches((char) c)) { + table.set(c); + } + } + } + + public boolean matchesAnyOf(CharSequence sequence) { + return !matchesNoneOf(sequence); + } + + public boolean matchesAllOf(CharSequence sequence) { + for (int i = sequence.length() - 1; i >= 0; i--) { + if (!matches(sequence.charAt(i))) { + return false; + } + } + return true; + } + + public boolean matchesNoneOf(CharSequence sequence) { + return indexIn(sequence) == -1; + } + + int indexIn(CharSequence sequence) { + int length = sequence.length(); + for (int i = 0; i < length; i++) { + if (matches(sequence.charAt(i))) { + return i; + } + } + return -1; + } + + int indexIn(CharSequence sequence, int start) { + int length = sequence.length(); + checkPositionIndex(start, length); + for (int i = start; i < length; i++) { + if (matches(sequence.charAt(i))) { + return i; + } + } + return -1; + } + + int lastIndexIn(CharSequence sequence) { + for (int i = sequence.length() - 1; i >= 0; i--) { + if (matches(sequence.charAt(i))) { + return i; + } + } + return -1; + } + + int countIn(CharSequence sequence) { + int count = 0; + for (int i = 0; i < sequence.length(); i++) { + if (matches(sequence.charAt(i))) { + count++; + } + } + return count; + } + + String removeFrom(CharSequence sequence) { + String string = sequence.toString(); + int pos = indexIn(string); + if (pos == -1) { + return string; + } + char[] chars = string.toCharArray(); + int spread = 1; + OUT: + while (true) { + pos++; + while (true) { + if (pos == chars.length) { + break OUT; + } + if (matches(chars[pos])) { + break; + } + chars[pos - spread] = chars[pos]; + pos++; + } + spread++; + } + return new String(chars, 0, pos - spread); + } + + String retainFrom(CharSequence sequence) { + return negate().removeFrom(sequence); + } + + String replaceFrom(CharSequence sequence, char replacement) { + String string = sequence.toString(); + int pos = indexIn(string); + if (pos == -1) { + return string; + } + char[] chars = string.toCharArray(); + chars[pos] = replacement; + for (int i = pos + 1; i < chars.length; i++) { + if (matches(chars[i])) { + chars[i] = replacement; + } + } + return new String(chars); + } + + boolean apply(Character character) { + return matches(character); + } + + private abstract static class FastMatcher extends CharMatcher { + FastMatcher() { + super(); + } + + @Override + public CharMatcher precomputed() { + return this; + } + + @Override + public CharMatcher negate() { + return new NegatedFastMatcher(this); + } + } + + private static class NegatedMatcher extends CharMatcher { + CharMatcher original; + + NegatedMatcher(CharMatcher original) { + super(); + this.original = original; + } + + @Override + public boolean matches(char c) { + return !original.matches(c); + } + + @Override + public boolean matchesAllOf(CharSequence sequence) { + return original.matchesNoneOf(sequence); + } + + @Override + public boolean matchesNoneOf(CharSequence sequence) { + return original.matchesAllOf(sequence); + } + + @Override + int countIn(CharSequence sequence) { + return sequence.length() - original.countIn(sequence); + } + + @Override + void setBits(BitSet table) { + BitSet tmp = new BitSet(); + original.setBits(tmp); + tmp.flip((Character.MIN_VALUE), (Character.MAX_VALUE) + 1); + table.or(tmp); + } + + @Override + public CharMatcher negate() { + return original; + }; + } + + private static class NegatedFastMatcher extends NegatedMatcher { + NegatedFastMatcher(CharMatcher original) { + super(original); + } + + @Override + public CharMatcher precomputed() { + return this; + } + } + + private static class And extends CharMatcher { + private final CharMatcher first; + private final CharMatcher second; + + And(CharMatcher a, CharMatcher b) { + super(); + first = checkNotNull(a); + second = checkNotNull(b); + } + + @Override + public boolean matches(char c) { + return first.matches(c) && second.matches(c); + } + + @Override + void setBits(BitSet table) { + BitSet tmp1 = new BitSet(); + first.setBits(tmp1); + BitSet tmp2 = new BitSet(); + second.setBits(tmp2); + tmp1.and(tmp2); + table.or(tmp1); + } + } + + private static class Or extends CharMatcher { + private final CharMatcher first; + private final CharMatcher second; + + Or(CharMatcher a, CharMatcher b) { + super(); + first = checkNotNull(a); + second = checkNotNull(b); + } + + @Override + void setBits(BitSet table) { + first.setBits(table); + second.setBits(table); + } + + @Override + public boolean matches(char c) { + return first.matches(c) || second.matches(c); + } + } + + private static class BitSetMatcher extends FastMatcher { + private final BitSet table; + + private BitSetMatcher(BitSet table) { + if (table.length() + Long.SIZE < table.size()) { + table = (BitSet) table.clone(); + } + this.table = table; + } + + @Override + public boolean matches(char c) { + return table.get(c); + } + + @Override + void setBits(BitSet bitSet) { + bitSet.or(table); + } + } + + private static class SmallCharMatcher extends FastMatcher { + + static final int MAX_SIZE = 1023; + + private static final int C1 = 0xcc9e2d51; + + private static final int C2 = 0x1b873593; + + private static final double DESIRED_LOAD_FACTOR = 0.5d; + + private final char[] table; + + private final boolean containsZero; + + private final long filter; + + private SmallCharMatcher(char[] table, long filter, boolean containsZero) { + super(); + this.table = table; + this.filter = filter; + this.containsZero = containsZero; + } + + static int smear(int hashCode) { + return C2 * Integer.rotateLeft(hashCode * C1, 15); + } + + private boolean checkFilter(int c) { + return 1 == (1 & (filter >> c)); + } + + static int chooseTableSize(int setSize) { + if (setSize == 1) { + return 2; + } + int tableSize = Integer.highestOneBit(setSize - 1) << 1; + while (tableSize * DESIRED_LOAD_FACTOR < setSize) { + tableSize <<= 1; + } + return tableSize; + } + + static CharMatcher from(BitSet chars) { + long filter = 0; + int size = chars.cardinality(); + boolean containsZero = chars.get(0); + char[] table = new char[chooseTableSize(size)]; + int mask = table.length - 1; + for (int c = chars.nextSetBit(0); c != -1; c = chars.nextSetBit(c + 1)) { + filter |= 1L << c; + int index = smear(c) & mask; + while (true) { + if (table[index] == 0) { + table[index] = (char) c; + break; + } + index = (index + 1) & mask; + } + } + return new SmallCharMatcher(table, filter, containsZero); + } + + @Override + public boolean matches(char c) { + if (c == 0) { + return containsZero; + } + if (!checkFilter(c)) { + return false; + } + int mask = table.length - 1; + int startingIndex = smear(c) & mask; + int index = startingIndex; + while (true) { + if (table[index] == 0) { + return false; + } else if (table[index] == c) { + return true; + } else { + index = (index + 1) & mask; + } + if (index == startingIndex) { + break; + } + } + return false; + } + + @Override + void setBits(BitSet table) { + if (containsZero) { + table.set(0); + } + for (char c : this.table) { + if (c != 0) { + table.set(c); + } + } + } + } + + abstract static class NamedFastMatcher extends FastMatcher { + + private final String description; + + NamedFastMatcher(String description) { + this.description = checkNotNull(description); + } + + @Override + public final String toString() { + return description; + } + } + + private static final class Ascii extends NamedFastMatcher { + + static final Ascii INSTANCE = new Ascii(); + + Ascii() { + super("CharMatcher.ascii()"); + } + + @Override + public boolean matches(char c) { + return c <= '\u007f'; + } + } + +} diff --git a/net/src/main/java/org/xbib/net/util/DateTimeUtil.java b/net/src/main/java/org/xbib/net/util/DateTimeUtil.java new file mode 100644 index 0000000..c7d5a42 --- /dev/null +++ b/net/src/main/java/org/xbib/net/util/DateTimeUtil.java @@ -0,0 +1,67 @@ +package org.xbib.net.util; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Locale; + +public class DateTimeUtil { + + private static final ZoneId ZONE_UTC = ZoneId.of("UTC"); + + private static final Locale ROOT_LOCALE = Locale.ROOT; + + private static final String RFC1036_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss zzz"; + + private static final String ASCIITIME_PATTERN = "EEE MMM d HH:mm:ss yyyyy"; + + private DateTimeUtil() { + } + + public static String formatRfc1123(Instant instant) { + return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)); + } + + public static String formatRfc1123(long millis) { + return formatRfc1123(Instant.ofEpochMilli(millis)); + } + + // RFC 2616 allows RFC 1123, RFC 1036, ASCII time + private static final DateTimeFormatter[] dateTimeFormatters = { + DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(ROOT_LOCALE).withZone(ZONE_UTC), + DateTimeFormatter.ofPattern(RFC1036_PATTERN).withLocale(ROOT_LOCALE).withZone(ZONE_UTC), + DateTimeFormatter.ofPattern(ASCIITIME_PATTERN).withLocale(ROOT_LOCALE).withZone(ZONE_UTC) + }; + + public static Instant parseDate(String date, int start, int end) { + int length = end - start; + if (length == 0) { + return null; + } else if (length < 0) { + throw new IllegalArgumentException("Can't have end < start"); + } else if (length > 64) { + throw new IllegalArgumentException("Can't parse more than 64 chars," + + "looks like a user error or a malformed header"); + } + return parseDate(date.substring(start, end)); + } + + public static Instant parseDate(String input) { + if (input == null) { + return null; + } + int semicolonIndex = input.indexOf(';'); + String trimmedDate = semicolonIndex >= 0 ? input.substring(0, semicolonIndex) : input; + for (DateTimeFormatter formatter : dateTimeFormatters) { + try { + return Instant.from(formatter.parse(trimmedDate)); + } catch (DateTimeParseException e) { + // + } + } + return null; + } +} diff --git a/net/src/main/java/org/xbib/net/util/ExceptionFormatter.java b/net/src/main/java/org/xbib/net/util/ExceptionFormatter.java new file mode 100644 index 0000000..bbafa4f --- /dev/null +++ b/net/src/main/java/org/xbib/net/util/ExceptionFormatter.java @@ -0,0 +1,56 @@ +package org.xbib.net.util; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Format exception messages and stack traces. + */ +public final class ExceptionFormatter { + + private ExceptionFormatter() { + } + + /** + * Format exception with stack trace. + * + * @param t the thrown object + * @return the formatted exception + */ + public static String format(Throwable t) { + StringBuilder sb = new StringBuilder(); + append(sb, t, 0, true); + return sb.toString(); + } + + /** + * Append Exception to string builder. + */ + private static void append(StringBuilder sb, Throwable t, int level, boolean details) { + if (((t != null) && (t.getMessage() != null)) && (!t.getMessage().isEmpty())) { + if (details && (level > 0)) { + sb.append("\n\nCaused by\n"); + } + sb.append(t.getMessage()); + } + if (details) { + if (t != null) { + if ((t.getMessage() != null) && (t.getMessage().isEmpty())) { + sb.append("\n\nCaused by "); + } else { + sb.append("\n\n"); + } + } + StringWriter sw = new StringWriter(); + if (t != null) { + t.printStackTrace(new PrintWriter(sw)); + } + sb.append(sw.toString()); + } + if (t != null) { + if (t.getCause() != null) { + append(sb, t.getCause(), level + 1, details); + } + } + } +} diff --git a/net/src/main/java/org/xbib/net/util/JsonUtil.java b/net/src/main/java/org/xbib/net/util/JsonUtil.java new file mode 100644 index 0000000..14e90d0 --- /dev/null +++ b/net/src/main/java/org/xbib/net/util/JsonUtil.java @@ -0,0 +1,660 @@ +package org.xbib.net.util; + +import java.io.IOException; +import java.time.Instant; +import java.util.Collection; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class JsonUtil { + + private JsonUtil() { + } + + public static String toString(Map map) throws IOException { + return map != null ? new JsonBuilder().buildMap(map).build() : null; + } + + @SuppressWarnings("unchecked") + public static Map toMap(String json) throws IOException { + if (json == null) { + return null; + } + JsonParser parser = new JsonParser(); + parser.parse(json); + Object object = parser.getResult(); + if (object instanceof Map) { + return (Map) parser.getResult(); + } + throw new IllegalArgumentException(("unexpected, not a map instance: " + object.getClass())); + } + + private static class JsonParser { + + private static final char EOS = (char) -1; + + private static final char DOUBLE_QUOTE = '"'; + + private static final char BACKSLASH = '\\'; + + private static final char OPEN_MAP = '{'; + + private static final char CLOSE_MAP = '}'; + + private static final char OPEN_LIST = '['; + + private static final char CLOSE_LIST = ']'; + + private static final char COMMA = ','; + + private static final char COLON = ':'; + + private String input; + + private int i; + + private char ch; + + private Object result; + + private final Deque stack = new LinkedList<>(); + + public JsonParser() { + } + + public void parse(String input) throws IOException { + Objects.requireNonNull(input); + this.input = input; + this.i = 0; + stack.clear(); + ch = next(); + skipWhitespace(); + parseValue(); + skipWhitespace(); + if (ch != EOS) { + throw new IOException("malformed json: " + ch); + } + } + + public Object getResult() { + return result; + } + + private void parseValue() throws IOException { + switch (ch) { + case DOUBLE_QUOTE: + ch = next(); + parseString(false); + break; + case OPEN_MAP: + parseMap(); + break; + case OPEN_LIST: + parseList(); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + parseNumber(); + break; + case 't': + parseTrue(); + break; + case 'f': + parseFalse(); + break; + case 'n': + parseNull(); + break; + default: + throw new IOException("illegal character: " + ch); + } + } + + private void parseNumber() throws IOException { + boolean minus = false; + boolean dot = false; + boolean exponent = false; + int start = i - 1; + while (true) { + if (ch == '-') { + if (i - start > 1) { + throw new IOException("minus inside number"); + } + ch =next(); + minus = true; + } else if (ch == 'e' || ch == 'E') { + ch = next(); + if (exponent) { + throw new IOException("double exponents"); + } + exponent = true; + ch = next(); + if (ch == '-' || ch == '+') { + ch = next(); + if (ch < '0' || ch > '9') { + throw new IOException("invalid exponent"); + } + } else if (ch < '0' || ch > '9') { + throw new IOException("invalid exponent"); + } + } else if (ch == '.') { + ch = next(); + if (dot) { + throw new IOException("multiple dots"); + } + if (i - start == 1) { + throw new IOException("no digit before dot"); + } + dot = true; + } else if (ch >= '0' && ch <= '9') { + ch = next(); + } else { + break; + } + } + if (minus && i - start == 1) { + throw new IOException("isolated minus"); + } + if (dot || exponent) { + valueNode(Double.parseDouble(input.substring(start, i - 1))); + } else { + valueNode(Long.parseLong(input.substring(start, i - 1))); + } + } + + private void parseString(boolean isKey) throws IOException { + boolean escaped = false; + int start = i - 1; + while (true) { + if (ch == DOUBLE_QUOTE) { + if (escaped) { + CharSequence s = unescape(input.substring(start, i - 1)); + if (isKey) { + stack.push(new KeyNode(s)); + } else { + valueNode(s); + } + } else { + if (isKey) { + stack.push(new KeyNode(input.substring(start, i - 1))); + } else { + valueNode(input.substring(start, i - 1)); + } + } + ch = next(); + return; + } else if (ch == BACKSLASH) { + escaped = true; + ch = next(); + if (ch == DOUBLE_QUOTE || ch == '/' || ch == BACKSLASH || ch == 'b' || ch == 'f' || ch == 'n' || ch == 'r' || ch == 't') { + ch = next(); + } else if (ch == 'u') { + expectHex(); + expectHex(); + expectHex(); + expectHex(); + } else { + throw new IOException("illegal escape char: " + ch); + } + } else if (ch < 32) { + throw new IOException("illegal control char: " + ch); + } else { + ch = next(); + } + } + } + + private void parseList() throws IOException { + int count = 0; + List list = new LinkedList<>(); + stack.push(list); + ch = next(); + while (true) { + skipWhitespace(); + if (ch == CLOSE_LIST) { + result = stack.pop(); + tryAppend(result); + ch = next(); + return; + } + if (count > 0) { + expectChar(COMMA); + ch = next(); + skipWhitespace(); + } + parseValue(); + count++; + } + } + + private void parseMap() throws IOException { + int count = 0; + Map map = new LinkedHashMap<>(); + stack.push(map); + ch = next(); + while (true) { + skipWhitespace(); + if (ch == CLOSE_MAP) { + result = stack.pop(); + tryAppend(result); + ch = next(); + return; + } + if (count > 0) { + expectChar(COMMA); + ch = next(); + skipWhitespace(); + } + expectChar(DOUBLE_QUOTE); + ch = next(); + parseString(true); + skipWhitespace(); + expectChar(COLON); + ch = next(); + skipWhitespace(); + parseValue(); + count++; + } + } + + private void parseNull() throws IOException { + ch = next(); + expectChar('u'); + ch = next(); + expectChar('l'); + ch = next(); + expectChar('l'); + valueNode(null); + ch = next(); + } + + private void parseTrue() throws IOException { + ch = next(); + expectChar('r'); + ch = next(); + expectChar('u'); + ch = next(); + expectChar('e'); + valueNode(true); + ch = next(); + } + + private void parseFalse() throws IOException { + ch = next(); + expectChar('a'); + ch = next(); + expectChar('l'); + ch = next(); + expectChar('s'); + ch = next(); + expectChar('e'); + valueNode(false); + ch = next(); + } + + private void expectChar(char expected) throws IOException { + if (ch != expected) { + throw new IOException("expected char " + expected + " but got " + ch); + } + } + + private void expectHex() throws IOException { + ch = next(); + if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) { + return; + } + throw new IOException("invalid hex char " + ch); + } + + private void skipWhitespace() { + while (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') { + ch = next(); + } + } + + private static CharSequence unescape(CharSequence input) { + StringBuilder result = new StringBuilder(input.length()); + int i = 0; + while (i < input.length()) { + if (input.charAt(i) == BACKSLASH) { + i++; + switch (input.charAt(i)) { + case BACKSLASH: + result.append(BACKSLASH); + break; + case '/': + result.append('/'); + break; + case DOUBLE_QUOTE: + result.append(DOUBLE_QUOTE); + break; + case 'b': + result.append('\b'); + break; + case 'f': + result.append('\f'); + break; + case 'n': + result.append('\n'); + break; + case 'r': + result.append('\r'); + break; + case 't': + result.append('\t'); + break; + case 'u': { + result.append(Character.toChars(Integer.parseInt(input.toString().substring(i + 1, i + 5), 16))); + i += 4; + } + } + } else { + result.append(input.charAt(i)); + } + i++; + } + return result; + } + + private char next() { + try { + return input.charAt(i++); + } catch (StringIndexOutOfBoundsException e) { + return (char) -1; + } + } + + + private void valueNode(Object object) { + if (!tryAppend(object)) { + stack.push(object); + result = object; + } + } + + @SuppressWarnings("unchecked") + private boolean tryAppend(Object object) { + if (!stack.isEmpty()) { + if (stack.peek() instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) stack.peek(); + list.add(object); + return true; + } else if (stack.peek() instanceof KeyNode){ + KeyNode key = (KeyNode) stack.pop(); + if (stack.peek() instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) stack.peek(); + if (map != null) { + String k = key != null ? key.get().toString() : null; + map.put(k, object); + return true; + } + } + } + } + return false; + } + } + + static class KeyNode { + + private final CharSequence value; + + public KeyNode(CharSequence value) { + this.value = value; + } + + public CharSequence get() { + return value; + } + + } + + static class JsonBuilder { + + private final Appendable appendable; + + private State state; + + protected JsonBuilder() { + this(new StringBuilder()); + } + + protected JsonBuilder(Appendable appendable) { + this.appendable = appendable; + this.state = new State(null, 0, Structure.DOCSTART, true); + } + + public JsonBuilder beginCollection() throws IOException { + this.state = new State(state, state.level + 1, Structure.COLLECTION, true); + appendable.append('['); + return this; + } + + public JsonBuilder endCollection() throws IOException { + if (state.structure != Structure.COLLECTION) { + throw new IOException("no array to close"); + } + appendable.append(']'); + this.state = state != null ? state.parent : null; + return this; + } + + public JsonBuilder beginMap() throws IOException { + if (state.structure == Structure.COLLECTION) { + beginArrayValue(); + } + this.state = new State(state, state.level + 1, Structure.MAP, true); + appendable.append('{'); + return this; + } + + public JsonBuilder endMap() throws IOException { + if (state.structure != Structure.MAP && state.structure != Structure.KEY) { + throw new IOException("no object to close"); + } + appendable.append('}'); + this.state = state != null ? state.parent : null; + return this; + } + + public JsonBuilder buildMap(Map map) throws IOException { + Objects.requireNonNull(map); + boolean wrap = state.structure != Structure.MAP; + if (wrap) { + beginMap(); + } + for (Map.Entry entry : map.entrySet()) { + buildKey(entry.getKey()); + buildValue(entry.getValue()); + } + if (wrap) { + endMap(); + } + return this; + } + + public JsonBuilder buildCollection(Collection collection) throws IOException { + Objects.requireNonNull(collection); + beginCollection(); + for (Object object : collection) { + buildValue(object); + } + endCollection(); + return this; + } + + @SuppressWarnings("unchecked") + public JsonBuilder buildValue(Object object) throws IOException { + if (object instanceof Map) { + buildMap((Map) object); + return this; + } else if (object instanceof Collection) { + buildCollection((Collection) object); + return this; + } + if (state.structure == Structure.COLLECTION) { + beginArrayValue(); + } + if (object == null) { + buildNull(); + } else if (object instanceof CharSequence) { + buildString((CharSequence) object, true); + } else if (object instanceof Boolean) { + buildBoolean((Boolean) object); + } else if (object instanceof Byte) { + buildNumber((byte) object); + } else if (object instanceof Integer) { + buildNumber((int) object); + } else if (object instanceof Long) { + buildNumber((long) object); + } else if (object instanceof Float) { + buildNumber((float) object); + } else if (object instanceof Double) { + buildNumber((double) object); + } else if (object instanceof Number) { + buildNumber((Number) object); + } else if (object instanceof Instant) { + buildInstant((Instant) object); + } else { + throw new IllegalArgumentException("unable to write object class " + object.getClass()); + } + return this; + } + + public JsonBuilder buildKey(CharSequence string) throws IOException { + if (state.structure == Structure.COLLECTION) { + beginArrayValue(); + } else if (state.structure == Structure.MAP || state.structure == Structure.KEY) { + beginKey(string != null ? string.toString() : null); + } + buildString(string, true); + if (state.structure == Structure.MAP || state.structure == Structure.KEY) { + endKey(string != null ? string.toString() : null); + } + state.structure = Structure.KEY; + return this; + } + + public JsonBuilder buildNull() throws IOException { + if (state.structure == Structure.COLLECTION) { + beginArrayValue(); + } + buildString("null", false); + return this; + } + + public String build() { + return appendable.toString(); + } + + private void beginKey(String k) throws IOException { + if (state.first) { + state.first = false; + } else { + appendable.append(","); + } + } + + private void endKey(String k) throws IOException { + appendable.append(":"); + } + + private void beginArrayValue() throws IOException { + if (state.first) { + state.first = false; + } else { + appendable.append(","); + } + } + + private void buildBoolean(boolean bool) throws IOException { + buildString(bool ? "true" : "false", false); + } + + private void buildNumber(Number number) throws IOException { + buildString(number != null ? number.toString() : null, false); + } + + private void buildInstant(Instant instant) throws IOException { + buildString(instant.toString(), true); + } + + private void buildString(CharSequence string, boolean escape) throws IOException { + appendable.append(escape ? escapeString(string) : string); + } + + private CharSequence escapeString(CharSequence string) { + StringBuilder sb = new StringBuilder(); + sb.append('"'); + int start = 0; + int l = string.length(); + for (int i = 0; i < l; i++) { + char c = string.charAt(i); + if (c == '"' || c == '\\' || c < 32) { + if (i > start) { + sb.append(string, start, i); + } + start = i + 1; + sb.append(escapeCharacter(c)); + } + } + if (l > start) { + sb.append(string, start, l); + } + sb.append('"'); + return sb; + } + + private static String escapeCharacter(char c) { + switch (c) { + case '\n': + return "\\n"; + case '\r': + return "\\r"; + case '\t': + return "\\t"; + case '\\': + return "\\\\"; + case '\'': + return "\\'"; + case '\"': + return "\\\""; + } + String hex = Integer.toHexString(c); + return "\\u0000".substring(0, 6 - hex.length()) + hex; + } + + private enum Structure { + DOCSTART, MAP, KEY, COLLECTION + } + + private static class State { + State parent; + int level; + Structure structure; + boolean first; + + State(State parent, int level, Structure structure, boolean first) { + this.parent = parent; + this.level = level; + this.structure = structure; + this.first = first; + } + } + } +} diff --git a/net/src/main/java/org/xbib/net/util/NamedThreadFactory.java b/net/src/main/java/org/xbib/net/util/NamedThreadFactory.java new file mode 100644 index 0000000..d89e0d4 --- /dev/null +++ b/net/src/main/java/org/xbib/net/util/NamedThreadFactory.java @@ -0,0 +1,21 @@ +package org.xbib.net.util; + +import java.util.concurrent.ThreadFactory; + +public class NamedThreadFactory implements ThreadFactory { + + private final String name; + + private long counter = 0; + + public NamedThreadFactory(String name) { + this.name = name; + } + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, name + "-" + (counter++)); + thread.setDaemon(true); + return thread; + } +} diff --git a/net/src/main/java/org/xbib/net/util/RandomUtil.java b/net/src/main/java/org/xbib/net/util/RandomUtil.java new file mode 100644 index 0000000..c7fd536 --- /dev/null +++ b/net/src/main/java/org/xbib/net/util/RandomUtil.java @@ -0,0 +1,31 @@ +package org.xbib.net.util; + +import java.security.SecureRandom; + +public class RandomUtil { + + private static final SecureRandom secureRandom = new SecureRandom(); + + private RandomUtil() { + } + + public static String randomString(int length) { + byte[] b = new byte[length]; + secureRandom.nextBytes(b); + return encodeHex(b); + } + + public static byte[] randomBytes(int length) { + byte[] b = new byte[length]; + secureRandom.nextBytes(b); + return b; + } + + private static String encodeHex(byte[] data) { + StringBuilder sb = new StringBuilder(); + for (byte b : data) { + sb.append(Character.forDigit((b & 240) >> 4, 16)).append(Character.forDigit((b & 15), 16)); + } + return sb.toString(); + } +} diff --git a/net/src/main/java/org/xbib/net/util/ThreadLoggingFormatter.java b/net/src/main/java/org/xbib/net/util/ThreadLoggingFormatter.java new file mode 100644 index 0000000..1216811 --- /dev/null +++ b/net/src/main/java/org/xbib/net/util/ThreadLoggingFormatter.java @@ -0,0 +1,84 @@ +package org.xbib.net.util; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.function.Function; +import java.util.logging.Formatter; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; + +public class ThreadLoggingFormatter extends Formatter { + + private static final String key = "org.xbib.net.util.ThreadLoggingFormatter.format"; + + private static final String DEFAULT_FORMAT = + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] [%5$s] %6$s %7$s%n"; + + ThreadMXBean threadMXBean; + + public ThreadLoggingFormatter() { + this.threadMXBean = ManagementFactory.getThreadMXBean(); + } + + @Override + public String format(LogRecord record) { + ZonedDateTime zdt = ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault()); + String source; + if (record.getSourceClassName() != null) { + source = record.getSourceClassName(); + if (record.getSourceMethodName() != null) { + source += " " + record.getSourceMethodName(); + } + } else { + source = record.getLoggerName(); + } + String message = formatMessage(record); + String throwable = ""; + if (record.getThrown() != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + pw.println(); + record.getThrown().printStackTrace(pw); + pw.close(); + throwable = sw.toString(); + } + return String.format(format, + zdt, + source, + record.getLoggerName(), + record.getLevel().getName(), + threadMXBean.getThreadInfo(record.getLongThreadID()).getThreadName(), + message, + throwable); + } + + private static String getLoggingProperty(String name) { + return LogManager.getLogManager().getProperty(name); + } + + private static final String format = getSimpleFormat(ThreadLoggingFormatter::getLoggingProperty); + + private static String getSimpleFormat(Function defaultPropertyGetter) { + String format = System.getProperty(key); + if (format == null && defaultPropertyGetter != null) { + format = defaultPropertyGetter.apply(key); + } + if (format != null) { + try { + // validate the user-defined format string + String.format(format, ZonedDateTime.now(), "", "", "", "", ""); + } catch (IllegalArgumentException e) { + // illegal syntax; fall back to the default format + format = DEFAULT_FORMAT; + } + } else { + format = DEFAULT_FORMAT; + } + return format; + } + +} diff --git a/net/src/main/java/org/xbib/net/util/package-info.java b/net/src/main/java/org/xbib/net/util/package-info.java new file mode 100644 index 0000000..afd779b --- /dev/null +++ b/net/src/main/java/org/xbib/net/util/package-info.java @@ -0,0 +1,4 @@ +/** + * Utilities for Net HTTP classes. + */ +package org.xbib.net.util; diff --git a/net/src/main/resources/META-INF/services/org.xbib.net.buffer.DataBufferFactory b/net/src/main/resources/META-INF/services/org.xbib.net.buffer.DataBufferFactory new file mode 100644 index 0000000..ae9ead1 --- /dev/null +++ b/net/src/main/resources/META-INF/services/org.xbib.net.buffer.DataBufferFactory @@ -0,0 +1 @@ +org.xbib.net.buffer.DefaultDataBufferFactory \ No newline at end of file diff --git a/net/src/test/java/org/xbib/net/IRITest.java b/net/src/test/java/org/xbib/net/IRITest.java new file mode 100644 index 0000000..e42fae8 --- /dev/null +++ b/net/src/test/java/org/xbib/net/IRITest.java @@ -0,0 +1,176 @@ +package org.xbib.net; + +import org.junit.jupiter.api.Test; + +import java.text.Normalizer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class IRITest { + + @Test + void testIpv4() { + URL iri = URL.create("http://127.0.0.1"); + assertEquals("http://127.0.0.1", iri.toExternalForm()); + } + + @Test + void testIpv6() { + URL iri = URL.from("http://[2001:0db8:85a3:08d3:1319:8a2e:0370:7344]"); + assertEquals(iri.getProtocolVersion(), ProtocolVersion.IPV6); + assertEquals("http://[2001:db8:85a3:8d3:1319:8a2e:370:7344]", iri.toString()); + } + + @Test + void testIpv6Invalid() { + URL iri = URL.from("http://[2001:0db8:85a3:08d3:1319:8a2e:0370:734o]"); + assertEquals(URL.nullUrl(), iri); + } + + @Test + void testSimple() { + URL iri = URL.create("http://validator.w3.org/check?uri=http%3A%2F%2Fr\u00E9sum\u00E9.example.org"); + assertEquals("http://validator.w3.org/check?uri=http%3A%2F%2Fr\u00E9sum\u00E9.example.org", iri.toString()); + } + + @Test + void testFile() throws Exception { + URL iri = URL.create("file:///tmp/test/foo"); + assertEquals("", iri.getHost()); + assertEquals("/tmp/test/foo", iri.getPath()); + assertEquals("file:///tmp/test/foo", iri.toExternalForm()); + assertEquals("file:///tmp/test/foo", iri.toString()); + } + + @Test + void testSimple2() throws Exception { + URL iri = URL.create("http://www.example.org/red%09ros\u00E9#red"); + assertEquals("http://www.example.org/red%09ros%C3%A9#red", iri.toExternalForm()); + } + + @Test + void testNotSoSimple() throws Exception { + URL iri = URL.create("http://example.com/\uD800\uDF00\uD800\uDF01\uD800\uDF02"); + assertEquals("http://example.com/%F0%90%8C%80%F0%90%8C%81%F0%90%8C%82", iri.toExternalForm()); + } + + @Test + void testIRItoURI() throws Exception { + URL iri = URL.from("http://\u7D0D\u8C46.example.org/%E2%80%AE"); + assertEquals("http://xn--99zt52a.example.org/%E2%80%AE", iri.toExternalForm()); + } + + @Test + void testComparison() { + URL url1 = URL.create("http://www.example.org/"); + URL url2 = URL.create("http://www.example.org/.."); + URL url3 = URL.create("http://www.Example.org:80"); + assertNotEquals(url1, url2); + assertNotEquals(url1, url3); + assertNotEquals(url2, url1); + assertNotEquals(url2, url3); + assertNotEquals(url3, url1); + assertNotEquals(url3, url2); + assertEquals(url1.normalize(), url2.normalize()); + assertEquals(url1.normalize(), url3.normalize()); + assertEquals(url2.normalize(), url1.normalize()); + assertEquals(url2.normalize(), url3.normalize()); + assertEquals(url3.normalize(), url1.normalize()); + assertEquals(url3.normalize(), url2.normalize()); + } + + @Test + void testUCN() { + URL iri1 = URL.create("http://www.example.org/r\u00E9sum\u00E9.html"); + String s = Normalizer.normalize("http://www.example.org/re\u0301sume\u0301.html", Normalizer.Form.NFC); + URL iri2 = URL.create(s); + assertEquals(iri2, iri1); + } + + @Test + void testNormalizePath() { + URL iri1 = URL.create("http://example.org/%7e%2Fuser%2f"); + URL iri2 = URL.create("http://example.org/%7E%2fuser/"); + assertEquals(iri1.normalize(), iri2.normalize()); + } + + @Test + void testIDN() { + URL iri1 = URL.from("http://r\u00E9sum\u00E9.example.org"); + assertEquals("xn--rsum-bpad.example.org", iri1.getHost()); + } + + @Test + void testResolveRelative() { + URL base = URL.create("http://example.org/foo/"); + assertEquals("http://example.org/", base.resolve("/").toString()); + assertEquals("http://example.org/test", base.resolve("/test").toString()); + assertEquals("http://example.org/foo/test", base.resolve("test").toString()); + assertEquals("http://example.org/test", base.resolve("../test").toString()); + assertEquals("http://example.org/foo/test", base.resolve("./test").toString()); + assertEquals("http://example.org/foo/", base.resolve("test/test/../../").toString()); + assertEquals("http://example.org/foo/?test", base.resolve("?test").toString()); + assertEquals("http://example.org/foo/#test", base.resolve("#test").toString()); + assertEquals("http://example.org/foo/", base.resolve(".").toString()); + } + + @Test + void testSchemes() { + URL iri = URL.create("http://a:b@c.org:80/d/e?f#g"); + assertEquals("http", iri.getScheme()); + assertEquals("a:b", iri.getUserInfo()); + assertEquals("c.org", iri.getHost()); + assertEquals(Integer.valueOf(80), iri.getPort()); + assertEquals("/d/e", iri.getPath()); + assertEquals("f", iri.getQuery()); + assertEquals("g", iri.getFragment()); + iri = URL.create("https://a:b@c.org:80/d/e?f#g"); + assertEquals("https", iri.getScheme()); + assertEquals("a:b", iri.getUserInfo()); + assertEquals("c.org", iri.getHost()); + assertEquals(Integer.valueOf(80), iri.getPort()); + assertEquals("/d/e", iri.getPath()); + assertEquals("f", iri.getQuery()); + assertEquals("g", iri.getFragment()); + iri = URL.create("ftp://a:b@c.org:80/d/e?f#g"); + assertEquals("ftp", iri.getScheme()); + assertEquals("a:b", iri.getUserInfo()); + assertEquals("c.org", iri.getHost()); + assertEquals(Integer.valueOf(80), iri.getPort()); + assertEquals("/d/e", iri.getPath()); + assertEquals("f", iri.getQuery()); + assertEquals("g", iri.getFragment()); + iri = URL.create("mailto:joe@example.org?subject=foo"); + assertEquals("mailto", iri.getScheme()); + assertEquals(null, iri.getUserInfo()); + assertEquals(null, iri.getHost()); + assertEquals(null, iri.getPort()); + assertEquals("joe@example.org?subject=foo", iri.getSchemeSpecificPart()); + assertEquals(null, iri.getFragment()); + iri = URL.create("tag:example.org,2006:foo"); + assertEquals("tag", iri.getScheme()); + assertEquals(null, iri.getUserInfo()); + assertEquals(null, iri.getHost()); + assertEquals(null, iri.getPort()); + assertEquals("example.org,2006:foo", iri.getSchemeSpecificPart()); + assertEquals(null, iri.getQuery()); + assertEquals(null, iri.getFragment()); + iri = URL.create("urn:lsid:ibm.com:example:82437234964354895798234d"); + assertEquals("urn", iri.getScheme()); + assertEquals(null, iri.getUserInfo()); + assertEquals(null, iri.getHost()); + assertEquals(null, iri.getPort()); + assertEquals("lsid:ibm.com:example:82437234964354895798234d", iri.getSchemeSpecificPart()); + assertEquals(null, iri.getQuery()); + assertEquals(null, iri.getFragment()); + iri = URL.create("data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP"); + assertEquals("data", iri.getScheme()); + assertEquals(null, iri.getUserInfo()); + assertEquals(null, iri.getHost()); + assertEquals(null, iri.getPort()); + assertEquals("image/gif;base64,R0lGODdhMAAwAPAAAAAAAP", iri.getSchemeSpecificPart()); + assertEquals(null, iri.getQuery()); + assertEquals(null, iri.getFragment()); + } +} diff --git a/net/src/test/java/org/xbib/net/NetworkUtilsTest.java b/net/src/test/java/org/xbib/net/NetworkUtilsTest.java new file mode 100644 index 0000000..7baf209 --- /dev/null +++ b/net/src/test/java/org/xbib/net/NetworkUtilsTest.java @@ -0,0 +1,30 @@ +package org.xbib.net; + +import org.junit.jupiter.api.Test; + +import java.net.UnixDomainSocketAddress; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class NetworkUtilsTest { + + private static final Logger logger = Logger.getLogger(NetworkUtilsTest.class.getName()); + + @Test + public void testInterfaceProperties() { + logger.log(Level.INFO, NetworkUtils.createProperties().toString()); + } + + @Test + public void testInterfaceDisplay() { + logger.log(Level.INFO, NetworkUtils.getNetworkInterfacesAsString()); + } + + @Test + public void testIP() { + logger.log(Level.INFO, Boolean.toString(NetworkUtils.isIpv4Available())); + logger.log(Level.INFO, Boolean.toString(NetworkUtils.isIpv4Active())); + logger.log(Level.INFO, Boolean.toString(NetworkUtils.isIpv6Available())); + logger.log(Level.INFO, Boolean.toString(NetworkUtils.isIpv6Active())); + } +} diff --git a/net/src/test/java/org/xbib/net/ParameterTest.java b/net/src/test/java/org/xbib/net/ParameterTest.java new file mode 100644 index 0000000..ae47359 --- /dev/null +++ b/net/src/test/java/org/xbib/net/ParameterTest.java @@ -0,0 +1,163 @@ +package org.xbib.net; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ParameterTest { + + @Test + public void testEmptyBuilder() { + Parameter parameter = Parameter.builder().build(); + assertNotNull(parameter); + assertEquals("DEFAULT", parameter.getDomain()); + assertFalse(parameter.containsKey("DEFAULT", "param1")); + } + + @Test + public void testSingleParameter() { + Parameter parameter = Parameter.builder() + .add("Hello", "World") + .build(); + assertNotNull(parameter); + assertEquals("DEFAULT", parameter.getDomain()); + assertTrue(parameter.containsKey("DEFAULT", "Hello")); + } + + @Test + public void testDuplicateParameter() { + Parameter parameter = Parameter.builder() + .enableDuplicates() + .add("Hello", "World") + .add("Hello", "World") + .add("Hello", "World") + .build(); + assertNotNull(parameter); + assertEquals("DEFAULT", parameter.getDomain()); + assertTrue(parameter.containsKey("DEFAULT", "Hello")); + assertEquals(List.of("World", "World", "World"), parameter.getAll("DEFAULT", "Hello")); + } + + @Test + public void testHttpHeaderParameter() { + Parameter parameter = Parameter.builder() + .charset(StandardCharsets.US_ASCII) + .lowercase() + .domain("HEADER") + .add("Content-Type", "text/plain") + .add("Accept", "*/*") + .add("Connection", "close") + .build(); + assertNotNull(parameter); + assertEquals("HEADER", parameter.getDomain()); + assertTrue(parameter.containsKey("HEADER", "content-type")); + assertEquals(List.of("close"), parameter.getAll("HEADER", "connection")); + } + + @Test + public void testQueryParameters() { + Map map = Map.of( + "version", "1.1", + "operation", "searchRetrieve", + "recordSchema", "MARC21plus-1-xml", + "query", "iss = 00280836" + ); + Parameter parameter = Parameter.builder() + .enableSort() + .enableQueryString(true) + .add(map) + .build(); + assertEquals("operation=searchRetrieve&query=iss%20%3D%2000280836&recordSchema=MARC21plus-1-xml&version=1.1", + parameter.getAsQueryString()); + } + + @Test + public void testParameters() { + Map map = Map.of( + "version", "1.1", + "operation", "searchRetrieve", + "recordSchema", "MARC21plus-1-xml", + "query", "iss = 00280836" + ); + Parameter parameter = Parameter.builder() + .enableSort() + .enableQueryString(true) + .add(map) + .build(); + assertEquals("operation=searchRetrieve&query=iss%20%3D%2000280836&recordSchema=MARC21plus-1-xml&version=1.1", + parameter.getAsQueryString()); + } + + @Test + public void testMutatorQueryParameters() { + URL url = URL.from("http://localhost"); + String requestPath = "/path"; + Map map = Map.of( + "version", "1.1", + "operation", "searchRetrieve", + "recordSchema", "MARC21plus-1-xml", + "query", "iss = 00280836" + ); + Parameter httpParameter = Parameter.builder() + .enableSort() + .enableQueryString(true) + .add(map) + .build(); + URLBuilder mutator = url.mutator(); + mutator.path(requestPath); + httpParameter.stream("DEFAULT").forEach(e -> mutator.queryParam(e.getKey(), e.getValue())); + url = mutator.build(); + assertEquals("http://localhost/path?operation=searchRetrieve&query=iss%20%3D%2000280836&recordSchema=MARC21plus-1-xml&version=1.1", + url.toExternalForm()); + } + + @Test + void testSimpleParse() { + ParameterBuilder queryParameters = Parameter.builder(); + String body = "a=b&c=d&e=f"; + queryParameters.addPercentEncodedBody(body); + Parameter parameter = queryParameters.build(); + assertEquals("b", parameter.getAll("DEFAULT", "a").get(0)); + assertEquals("d", parameter.getAll("DEFAULT", "c").get(0)); + assertEquals("f", parameter.getAll("DEFAULT", "e").get(0)); + } + + @Test + void testParseExceedingParamLimit() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + ParameterBuilder queryParameters = Parameter.builder().limit(100); + List list = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + list.add("a" + i + "=b" + i); + } + String body = String.join("&", list); + queryParameters.addPercentEncodedBody(body); + Parameter parameter = queryParameters.build(); + assertEquals("b0", parameter.getAll("DEFAULT", "a0").get(0)); + assertEquals("b99", parameter.getAll("DEFAULT", "a99").get(0)); + assertEquals("[]", parameter.getAll("DEFAULT", "a100").toString()); + }); + } + + @Test + void testSubDomains() { + Parameter p1 = Parameter.builder().domain("A").add("a", "a").build(); + Parameter p2 = Parameter.builder().domain("B").add("b", "b").build(); + Parameter p3 = Parameter.builder().domain("C").add("c", "c").build(); + Parameter p = Parameter.builder() + .add(p1) + .add(p2) + .add(p3) + .build(); + assertEquals("[a]", p.get("A", "a").toString()); + assertEquals("[b]", p.get("B", "b").toString()); + assertEquals("[c]", p.get("C", "c").toString()); + } +} diff --git a/net/src/test/java/org/xbib/net/PathNormalizerTest.java b/net/src/test/java/org/xbib/net/PathNormalizerTest.java new file mode 100644 index 0000000..c442842 --- /dev/null +++ b/net/src/test/java/org/xbib/net/PathNormalizerTest.java @@ -0,0 +1,75 @@ +package org.xbib.net; + +import org.junit.jupiter.api.Test; +import org.xbib.net.PathNormalizer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PathNormalizerTest { + + @Test + void normalizeNullPath() { + assertEquals("/", PathNormalizer.normalize(null)); + } + + @Test + void normalizeEmptyPath() { + assertEquals("/", PathNormalizer.normalize("")); + } + + @Test + void normalizeSlashPath() { + assertEquals("/", PathNormalizer.normalize("/")); + } + + @Test + void normalizeDoubleSlashPath() { + assertEquals("/", PathNormalizer.normalize("//")); + } + + @Test + void normalizeTripleSlashPath() { + assertEquals("/", PathNormalizer.normalize("///")); + } + + @Test + void normalizePathWithPoint() { + assertEquals("/", PathNormalizer.normalize("/.")); + } + + @Test + void normalizePathWithPointAndElement() { + assertEquals("/a", PathNormalizer.normalize("/./a")); + } + + @Test + void normalizePathWithTwoPointsAndElement() { + assertEquals("/a", PathNormalizer.normalize("/././a")); + } + + @Test + void normalizePathWithDoublePoint() { + assertEquals("/", PathNormalizer.normalize("/..")); + assertEquals("/", PathNormalizer.normalize("/../..")); + assertEquals("/", PathNormalizer.normalize("/../../..")); + } + + @Test + void normalizePathWithFirstElementAndDoublePoint() { + assertEquals("/", PathNormalizer.normalize("/a/..")); + assertEquals("/", PathNormalizer.normalize("/a/../..")); + assertEquals("/", PathNormalizer.normalize("/a/../../..")); + } + + @Test + void normalizePathWithTwoElementsAndDoublePoint() { + assertEquals("/b", PathNormalizer.normalize("/a/../b")); + assertEquals("/b", PathNormalizer.normalize("/a/../../b")); + assertEquals("/b", PathNormalizer.normalize("/a/../../../b")); + } + + @Test + void doNotnormalizeEmbeddedSemicolon() { + assertEquals("/auth/cert;foo=bar/smartcard.xhtml", PathNormalizer.normalize("/auth/cert;foo=bar/smartcard.xhtml")); + } +} diff --git a/net/src/test/java/org/xbib/net/PercentDecoderTest.java b/net/src/test/java/org/xbib/net/PercentDecoderTest.java new file mode 100644 index 0000000..a91c2d5 --- /dev/null +++ b/net/src/test/java/org/xbib/net/PercentDecoderTest.java @@ -0,0 +1,122 @@ +package org.xbib.net; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +import static java.lang.Character.isHighSurrogate; +import static java.lang.Character.isLowSurrogate; +import static java.lang.Integer.toHexString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class PercentDecoderTest { + + private static final int CODE_POINT_IN_SUPPLEMENTARY = 2; + + private static final int CODE_POINT_IN_BMP = 1; + + private final PercentDecoder decoder = new PercentDecoder(); + + @Test + void testDecodesWithoutPercents() throws Exception { + assertEquals("asdf", decoder.decode("asdf")); + } + + @Test + void testDecodeSingleByte() throws Exception { + assertEquals("#", decoder.decode("%23")); + } + + @Test + void testIncompletePercentPairNoNumbers() { + Assertions.assertThrows(MalformedInputException.class, () ->{ + decoder.decode("%"); + }); + } + + @Test + void testIncompletePercentPairOneNumber() { + Assertions.assertThrows(MalformedInputException.class, () ->{ + decoder.decode("%2"); + }); + } + + @Test + void testInvalidHex() { + Assertions.assertThrows(MalformedInputException.class, () ->{ + decoder.decode("%xz"); + }); + } + + @Test + void testRandomStrings() throws MalformedInputException, UnmappableCharacterException { + PercentEncoder encoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); + Random rand = new Random(); + long seed = rand.nextLong(); + rand.setSeed(seed); + char[] charBuf = new char[2]; + List codePoints = new ArrayList<>(); + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + buf.setLength(0); + codePoints.clear(); + randString(buf, codePoints, charBuf, rand, 1 + rand.nextInt(1000)); + byte[] origBytes = buf.toString().getBytes(StandardCharsets.UTF_8); + byte[] decodedBytes = null; + String codePointsHex = codePoints.stream().map(Integer::toHexString).collect(Collectors.joining("")); + try { + decodedBytes = decoder.decode(encoder.encode(buf.toString())).getBytes(StandardCharsets.UTF_8); + assertEquals(toHex(origBytes), toHex(decodedBytes)); + } catch (IllegalArgumentException e) { + List charHex = new ArrayList<>(); + for (int j = 0; j < buf.toString().length(); j++) { + charHex.add(toHexString((int) buf.toString().charAt(j))); + } + fail("seed: " + seed + " code points: " + codePointsHex + " chars " + charHex + " " + e.getMessage()); + } + assertEquals(toHex(origBytes), toHex(decodedBytes)); + } + } + + /** + * Generate a random string. + * @param buf buffer to write into + * @param codePoints list of code points to write into + * @param charBuf char buf for temporary char wrangling (size 2) + * @param rand random source + * @param length max string length + */ + private static void randString(StringBuilder buf, List codePoints, char[] charBuf, Random rand, + int length) { + while (buf.length() < length) { + int codePoint = rand.nextInt(17 * 65536); + if (Character.isDefined(codePoint)) { + int res = Character.toChars(codePoint, charBuf, 0); + if (res == CODE_POINT_IN_BMP && (isHighSurrogate(charBuf[0]) || isLowSurrogate(charBuf[0]))) { + continue; + } + buf.append(charBuf[0]); + codePoints.add(codePoint); + if (res == CODE_POINT_IN_SUPPLEMENTARY) { + buf.append(charBuf[1]); + } + } + } + } + + private static List toHex(byte[] bytes) { + List list = new ArrayList<>(); + for (byte b: bytes) { + list.add(Integer.toHexString((int) b & 0xFF)); + } + return list; + } +} diff --git a/net/src/test/java/org/xbib/net/PercentEncoderTest.java b/net/src/test/java/org/xbib/net/PercentEncoderTest.java new file mode 100644 index 0000000..c36dde9 --- /dev/null +++ b/net/src/test/java/org/xbib/net/PercentEncoderTest.java @@ -0,0 +1,77 @@ +package org.xbib.net; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.BitSet; + +import static java.nio.charset.CodingErrorAction.REPLACE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PercentEncoderTest { + + private static PercentEncoder alnum; + + private static PercentEncoder alnum16; + + @BeforeAll + static void setUp() { + BitSet bs = new BitSet(); + for (int i = 'a'; i <= 'z'; i++) { + bs.set(i); + } + for (int i = 'A'; i <= 'Z'; i++) { + bs.set(i); + } + for (int i = '0'; i <= '9'; i++) { + bs.set(i); + } + + alnum = new PercentEncoder(bs, StandardCharsets.UTF_8.newEncoder().onMalformedInput(REPLACE) + .onUnmappableCharacter(REPLACE)); + alnum16 = new PercentEncoder(bs, StandardCharsets.UTF_16BE.newEncoder().onMalformedInput(REPLACE) + .onUnmappableCharacter(REPLACE)); + } + + @Test + void testDoesntEncodeSafe() throws Exception { + BitSet set = new BitSet(); + for (int i = 'a'; i <= 'z'; i++) { + set.set(i); + } + PercentEncoder pe = new PercentEncoder(set, StandardCharsets.UTF_8.newEncoder().onMalformedInput(REPLACE) + .onUnmappableCharacter(REPLACE)); + assertEquals("abcd%41%42%43%44", pe.encode("abcdABCD")); + } + + @Test + void testEncodeInBetweenSafe() throws Exception { + assertEquals("abc%20123", alnum.encode("abc 123")); + } + + @Test + void testSafeInBetweenEncoded() throws Exception { + assertEquals("%20abc%20", alnum.encode(" abc ")); + } + + @Test + void testEncodeUtf8() throws Exception { + assertEquals("snowman%E2%98%83", alnum.encode("snowman\u2603")); + } + + @Test + void testEncodeUtf8SurrogatePair() throws Exception { + assertEquals("clef%F0%9D%84%9E", alnum.encode("clef\ud834\udd1e")); + } + + @Test + void testEncodeUtf16() throws Exception { + assertEquals("snowman%26%03", alnum16.encode("snowman\u2603")); + } + + @Test + void testUrlEncodedUtf16SurrogatePair() throws Exception { + assertEquals("clef%D8%34%DD%1E", alnum16.encode("clef\ud834\udd1e")); + } +} diff --git a/net/src/test/java/org/xbib/net/URIComponentTest.java b/net/src/test/java/org/xbib/net/URIComponentTest.java new file mode 100644 index 0000000..db8d899 --- /dev/null +++ b/net/src/test/java/org/xbib/net/URIComponentTest.java @@ -0,0 +1,66 @@ +package org.xbib.net; + +import org.junit.jupiter.api.Test; + +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class URIComponentTest { + + @Test + void testURI() { + URI uri = URI.create("ftp://user:pass@host:1234/path/to/filename.txt"); + assertEquals("ftp", scheme(uri)); + assertEquals("user", user(uri)); + assertEquals("pass", pass(uri)); + assertEquals("host", host(uri)); + assertEquals(1234, port(uri)); + assertEquals("/path/to/", parent(uri)); + assertEquals("filename.txt", filename(uri)); + } + + @Test + void testURI2() { + URI uri = URI.create("sftp://user:pass@host:1234/filename.txt"); + assertEquals("sftp", scheme(uri)); + assertEquals("user", user(uri)); + assertEquals("pass", pass(uri)); + assertEquals("host", host(uri)); + assertEquals(1234, port(uri)); + assertEquals("/", parent(uri)); + assertEquals("filename.txt", filename(uri)); + } + + private static String scheme(URI uri) { + return uri.getScheme(); + } + + private static String user(URI uri) { + String auth = uri.getAuthority(); + return auth != null ? auth.split(":")[0] : null; + } + + private static String pass(URI uri) { + String auth = uri.getAuthority(); + return auth != null ? auth.split("@")[0].split(":")[1] : null; + } + + private static String host(URI uri) { + return uri.getHost(); + } + + private static int port(URI uri) { + return uri.getPort(); + } + + private static String parent(URI uri) { + return uri.resolve(".").getPath(); + } + + private static String filename(URI uri) { + String path = uri.getPath(); + int pos = path.lastIndexOf('/'); + return pos >= 0 ? path.substring(pos + 1) : path; + } +} diff --git a/net/src/test/java/org/xbib/net/URLBuilderTest.java b/net/src/test/java/org/xbib/net/URLBuilderTest.java new file mode 100644 index 0000000..977dd77 --- /dev/null +++ b/net/src/test/java/org/xbib/net/URLBuilderTest.java @@ -0,0 +1,294 @@ +package org.xbib.net; + +import org.junit.jupiter.api.Test; + +import java.net.SocketException; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class URLBuilderTest { + + private static final Logger logger = Logger.getLogger(URLBuilderTest.class.getName()); + + @Test + void testNoUrlParts() { + assertUrl(URL.http().resolveFromHost("foo.com").toUrlString(), "http://foo.com"); + } + + @Test + void testWithPort() { + assertUrl(URL.http().resolveFromHost("foo.com").port(33).toUrlString(), "http://foo.com:33"); + } + + @Test + void testSimplePath() { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("seg1") + .pathSegment("seg2") + .toUrlString(), + "http://foo.com/seg1/seg2"); + } + + @Test + void testPathWithReserved() { + // RFC 1738 S3.3 + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("seg/;?ment") + .pathSegment("seg=&2") + .toUrlString(), "http://foo.com/seg%2F%3B%3Fment/seg=&2"); + } + + @Test + void testPathSegments() { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegments("seg1", "seg2", "seg3") + .toUrlString(), "http://foo.com/seg1/seg2/seg3"); + } + + @Test + void testMatrixWithReserved() { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("foo") + .matrixParam("foo", "bar") + .matrixParam("res;=?#/erved", "value") + .pathSegment("baz") + .toUrlString(), "http://foo.com/foo;foo=bar;res%3B%3D%3F%23%2Ferved=value/baz"); + } + + @Test + void testUrlEncodedPathSegmentUtf8() { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("snowman").pathSegment("\u2603") + .toUrlString(), "http://foo.com/snowman/%E2%98%83"); + } + + @Test + void testUrlEncodedPathSegmentUtf8SurrogatePair() { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("clef").pathSegment("\ud834\udd1e") + .toUrlString(), "http://foo.com/clef/%F0%9D%84%9E"); + } + + @Test + void testQueryParamNoPath() { + assertUrl(URL.http().resolveFromHost("foo.com") + .queryParam("foo", "bar") + .toUrlString(), "http://foo.com?foo=bar"); + } + + @Test + void testQueryParamsDuplicated() { + assertUrl(URL.http().resolveFromHost("foo.com") + .queryParam("foo", "bar") + .queryParam("foo", "bar2") + .queryParam("baz", "quux") + .queryParam("baz", "quux2") + .toUrlString(), "http://foo.com?foo=bar&foo=bar2&baz=quux&baz=quux2"); + } + + @Test + void testEncodeQueryParams() { + assertUrl(URL.http().resolveFromHost("foo.com") + .queryParam("foo", "bar&=#baz") + .queryParam("foo", "bar?/2") + .toUrlString(), "http://foo.com?foo=bar%26%3D%23baz&foo=bar?/2"); + } + + @Test + void testEncodeQueryParamWithSpaceAndPlus() { + assertUrl(URL.http().resolveFromHost("foo.com") + .queryParam("foo", "spa ce") + .queryParam("fo+o", "plus+") + .toUrlString(), "http://foo.com?foo=spa%20ce&fo%2Bo=plus%2B"); + } + + @Test + void testPlusInVariousParts() { + assertUrl(URL.http().resolveFromHost("foo.com") + .pathSegment("has+plus") + .matrixParam("plusMtx", "pl+us") + .queryParam("plusQp", "pl+us") + .fragment("plus+frag") + .toUrlString(), "http://foo.com/has+plus;plusMtx=pl+us?plusQp=pl%2Bus#plus+frag"); + } + + @Test + void testFragment() { + assertUrl(URL.http().resolveFromHost("foo.com") + .queryParam("foo", "bar") + .fragment("#frag/?") + .toUrlString(), "http://foo.com?foo=bar#%23frag/?"); + } + + @Test + void testAllParts() { + assertUrl(URL.https().resolveFromHost("foobar.com").port(3333) + .pathSegment("foo") + .pathSegment("bar") + .matrixParam("mtx1", "val1") + .matrixParam("mtx2", "val2") + .queryParam("q1", "v1") + .queryParam("q2", "v2") + .fragment("zomg it's a fragment") + .toUrlString(), + "https://foobar.com:3333/foo/bar;mtx1=val1;mtx2=val2?q1=v1&q2=v2#zomg%20it's%20a%20fragment"); + } + + @Test + void testSlashHost() { + URL.http().resolveFromHost("/").toUrlString(); + } + + @Test + void testGoogle() { + URL url = URL.https().resolveFromHost("google.com").build(); + assertEquals("https://google.com", url.toString()); + } + + @Test + void testBadIPv4LiteralDoesntChoke() { + assertUrl(URL.http().resolveFromHost("300.100.50.1") + .toUrlString(), "http://300.100.50.1"); + } + + @Test + void testIPv4Literal() throws SocketException { + String s = URL.http().resolveFromHost("127.0.0.1").toUrlString(); + if (!NetworkUtils.isIpv6Active()) { + assertUrl(s, "http://localhost"); + } else { + // we assume entry + // 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 + // in /etc/hosts + assertEquals("http://localhost", s); + } + } + + @Test + void testIPv6Literal() throws SocketException { + if (NetworkUtils.isIpv6Active()) { + String s = URL.http().resolveFromHost("[2001:db8:85a3::8a2e:370:7334]").toUrlString(); + assertEquals("http://[2001:db8:85a3:0:0:8a2e:370:7334]", s); + } + } + + @Test + void testEncodedRegNameSingleByte() { + String s = URL.http().resolveFromHost("host?name;").toUrlString(); + assertEquals("http://host%3Fname;", s); + } + + @Test + void testEncodedRegNameMultiByte() { + String s = URL.http().host("snow\u2603man") + .toUrlString(); + assertEquals("http://snow%E2%98%83man", s); + } + + @Test + void testThreePathSegments() { + String s = URL.https().resolveFromHost("foo.com") + .pathSegments("a", "b", "c") + .toUrlString(); + assertEquals("https://foo.com/a/b/c", s); + } + + @Test + void testThreePathSegmentsWithQueryParams() { + String s = URL.https().resolveFromHost("foo.com") + .pathSegments("a", "b", "c") + .queryParam("foo", "bar") + .toUrlString(); + assertEquals("https://foo.com/a/b/c?foo=bar", s); + } + + @Test + void testIntermingledMatrixParamsAndPathSegments() { + String s = URL.http().resolveFromHost("foo.com") + .pathSegments("seg1", "seg2") + .matrixParam("m1", "v1") + .pathSegment("seg3") + .matrixParam("m2", "v2") + .toUrlString(); + assertEquals("http://foo.com/seg1/seg2;m1=v1/seg3;m2=v2", s); + } + + @Test + void testUseQueryParamAfterQuery() { + String s = URL.http().resolveFromHost("foo.com") + .query("q") + .queryParam("foo", "bar") + .toUrlString(); + assertEquals("http://foo.com?q", s); + } + + @Test + void testUseQueryAfterQueryParam() { + String s = URL.http().resolveFromHost("foo.com") + .queryParam("foo", "bar") + .query("q") + .toUrlString(); + assertEquals("http://foo.com?q", s); + } + + @Test + void testQueryWithNoSpecialChars() { + String s = URL.http().resolveFromHost("foo.com") + .query("q") + .toUrlString(); + assertEquals("http://foo.com?q", s); + } + + @Test + void testQueryWithOkSpecialChars() { + String s = URL.http().resolveFromHost("foo.com") + .query("q?/&=").toUrlString(); + assertEquals("http://foo.com?q?/&=", s); + } + + @Test + void testQueryWithEscapedSpecialChars() { + String s = URL.http().resolveFromHost("foo.com") + .query("q#+").toUrlString(); + assertEquals("http://foo.com?q#+", s); + } + + @Test + void testMutator() { + URLBuilder builder = URL.from("http://google.com:8008/foobar").mutator(); + builder.queryParam("a%", "b%"); + builder.queryParam("c", " d "); + builder.scheme("https"); + URL url = builder.build(); + assertEquals("/foobar?a%25=b%25&c=%20d%20", url.relativeReference()); + assertEquals("https://google.com:8008/foobar?a%25=b%25&c=%20d%20", url.toExternalForm()); + assertEquals("https://google.com:8008/foobar?a%=b%&c= d ", url.toString()); + } + + @Test + void testNewBuilder() { + URL url = URL.from("http://google.com:8008/foobar"); + URLBuilder builder = url.newBuilder() + .scheme(url.getScheme()) + .schemeSpecificPart(url.getSchemeSpecificPart()); + assertEquals("http://google.com:8008/foobar", builder.build().toString()); + } + + @Test + void testUserInfo(){ + String s = URL.http().userInfo("foo:bar").host("foo.com").toUrlString(); + assertEquals("http://foo:bar@foo.com", s); + s = URL.http().userInfo("foo:foo:bar").host("foo.com").toUrlString(); + assertEquals("http://foo:foo:bar@foo.com", s); + s = URL.http().userInfo("foo:foo%3Abar").host("foo.com").toUrlString(); + assertEquals("http://foo:foo%3Abar@foo.com", s); + s = URL.http().userInfo("foo", "foo:bar").host("foo.com").toUrlString(); + assertEquals("http://foo:foo%3Abar@foo.com", s); + } + + private void assertUrl(String urlString, String expected) { + assertEquals(expected, urlString); + assertEquals(expected, URL.from(urlString).toExternalForm()); + } +} diff --git a/net/src/test/java/org/xbib/net/URLParserTest.java b/net/src/test/java/org/xbib/net/URLParserTest.java new file mode 100644 index 0000000..2e9ba83 --- /dev/null +++ b/net/src/test/java/org/xbib/net/URLParserTest.java @@ -0,0 +1,485 @@ +package org.xbib.net; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class URLParserTest { + + @Test + void testNull() { + assertEquals(URL.nullUrl(), URL.from(null)); + } + + @Test + void testEmpty() { + assertEquals(URL.nullUrl(), URL.from("")); + } + + @Test + void testNewline() { + assertEquals(URL.nullUrl(), URL.from("\n")); + } + + @Test + void testInvalidScheme() { + Assertions.assertThrows(IllegalArgumentException.class, () -> URL.from("/:23")); + } + + @Test + void testScheme() { + URL url = URL.from("http://"); + assertEquals("http://", url.toExternalForm()); + assertEquals("http://", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testPath(){ + URL url = URL.from("http"); + assertFalse(url.isAbsolute()); + assertNull(url.getScheme()); + assertEquals("", url.getHostInfo()); + assertEquals("http", url.getPath()); + assertEquals("http", url.toExternalForm()); + assertEquals("http", url.toString()); + } + + @Test + void testOpaque() { + URL url = URL.from("a:b"); + assertEquals("a", url.getScheme()); + assertEquals("b", url.getSchemeSpecificPart()); + assertEquals("a:b", url.toExternalForm()); + assertEquals("a:b", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testGopher() { + URL url = URL.from("gopher:/example.com/"); + assertEquals("gopher:/example.com/", url.toExternalForm()); + } + + @Test + void testWithoutDoubleSlash() { + URL url = URL.from("http:foo.com"); + assertEquals("http:foo.com", url.toExternalForm()); + assertEquals("http:foo.com", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testSlashAfterScheme() { + URL url = URL.from("http:/example.com/"); + assertEquals("http:/example.com/", url.toExternalForm()); + } + + @Test + void testSchemeHost() { + URL url = URL.from("http://foo.bar"); + assertEquals("http://foo.bar", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testSchemeHostPort() { + URL url = URL.from("http://f:/c"); + assertEquals("http://f:/c", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testNetworkLocation() { + URL url = URL.from("//foo.bar"); + assertEquals("//foo.bar", url.toExternalForm()); + assertEquals("//foo.bar", url.toString()); + } + + @Test + void testSchemeHostAuthInfo() { + URL url = URL.from("http://auth@foo.bar"); + assertEquals("http://auth@foo.bar", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testSchemeHostAuthInfoPort() { + URL url = URL.from("http://auth@foo.bar:1"); + assertEquals("http://auth@foo.bar:1", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testSchemeHostAuthInfoPortPath() { + URL url = URL.from("http://auth@foo.bar:1/path"); + assertEquals("http://auth@foo.bar:1/path", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testTrailingSlash() { + URL url = URL.from("http://foo.bar/path/"); + assertEquals("http://foo.bar/path/", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testBackslash() { + URL url = URL.from("http://foo.com/\\@"); + assertEquals("http://foo.com/@", url.toExternalForm()); + } + + @Test + void testBackslashHost() { + URL url = URL.from("https://malicious.com\\google.com"); + assertEquals("malicious.com", url.getDecodedHost()); + assertEquals("/google.com", url.getPath()); + assertEquals("https://malicious.com/google.com", url.toExternalForm()); + } + + @Test + void testQuery() { + URL url = URL.from("http://auth@foo.bar:1/path?query"); + assertEquals("http://auth@foo.bar:1/path?query", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testFragment() { + URL url = URL.from("http://auth@foo.bar:1/path#fragment"); + assertEquals("http://auth@foo.bar:1/path#fragment", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testReservedChar() { + URL url = URL.from("http://www.google.com/ig/calculator?q=1USD=?EUR"); + if ("false".equals(System.getProperty("java.net.preferIPv6Addresses"))) { + assertEquals("http://www.google.com/ig/calculator?q=1USD%3D?EUR", url.toString()); + } + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testPassword() { + URL url = URL.from("ftp://aaa:b%2B1@www.google.com"); + assertEquals("b+1", url.getPassword()); + assertRoundTrip(url.toExternalForm()); + url = URL.from("ftp://aaa:b+1@www.google.com"); + assertEquals("b+1", url.getPassword()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testPlus() { + URL url = URL.from("http://foobar:8080/test/print?value=%EA%B0%80+%EB%82%98"); + assertEquals("http://foobar:8080/test/print?value=%EA%B0%80+%EB%82%98", url.toExternalForm()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testIPv6() { + URL url = URL.from("http://[2001:db8:85a3::8a2e:370:7334]"); + assertEquals("http://[2001:db8:85a3:0:0:8a2e:370:7334]", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testIPv6WithScope() { + // test scope ID. Must be a valid IPv6 + URL url = URL.from("http://[3002:0:0:0:20c:29ff:fe64:614a%2]:8080/resource"); + assertEquals("http://[3002:0:0:0:20c:29ff:fe64:614a%2]:8080/resource", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testIPv6WithIPv4() { + URL url = URL.from("http://[::192.168.1.1]:8080/resource"); + assertEquals("http://[0:0:0:0:0:0:c0a8:101]:8080/resource", url.toString()); + assertRoundTrip(url.toExternalForm()); + } + + @Test + void testFromUrlWithEverything() throws Exception { + assertUrlCompatibility("https://foo.bar.com:3333/foo/ba%20r;mtx1=val1;mtx2=val%202/" + + "seg%203;m2=v2?q1=v1&q2=v%202#zomg%20it's%20a%20fragment"); + } + + @Test + void testFromUrlWithEmptyPath() throws Exception { + assertUrlCompatibility("http://foo.com"); + } + + @Test + void testFromUrlWithPort() throws Exception { + assertUrlCompatibility("http://foo.com:1234"); + } + + @Test + void testFromUrlWithEncodedHost() throws Exception { + assertUrlCompatibility("http://f%20oo.com/bar"); + } + + @Test + void testFromUrlWithEncodedPathSegment() throws Exception { + assertUrlCompatibility("http://foo.com/foo/b%20ar"); + } + + @Test + void testFromUrlWithEncodedMatrixParam() throws Exception { + assertUrlCompatibility("http://foo.com/foo;m1=v1;m%202=v%202"); + } + + @Test + void testFromUrlWithEncodedQueryParam() throws Exception { + assertUrlCompatibility("http://foo.com/foo?q%201=v%202&q2=v2"); + } + + @Test + void testFromUrlWithEncodedQueryParamDelimiter() throws Exception { + assertUrlCompatibility("http://foo.com/foo?q1=%3Dv1&%26q2=v2"); + } + + @Test + void testFromUrlWithEncodedFragment() throws Exception { + assertUrlCompatibility("http://foo.com/foo#b%20ar"); + } + + @Test + void testFromUrlWithEmptyPathSegmentWithMatrixParams() throws Exception { + assertUrlCompatibility("http://foo.com/foo/;m1=v1"); + } + + @Test + void testFromUrlWithEmptyPathWithMatrixParams() throws Exception { + assertUrlCompatibility("http://foo.com/;m1=v1"); + } + + @Test + void testFromUrlWithEmptyPathWithMultipleMatrixParams() throws Exception { + assertUrlCompatibility("http://foo.com/;m1=v1;m2=v2"); + } + + @Test + void testFromUrlMalformedQueryParamNoValue() throws Exception { + assertUrlCompatibility("http://foo.com/foo?q1=v1&q2"); + } + + @Test + void testFromUrlMalformedQueryParamMultiValues() { + assertRoundTrip("http://foo.com/foo?q1=v1=v2"); + } + + @Test + void testFromUrlQueryWithEscapedChars() { + assertRoundTrip("http://foo.com/foo?query==&%23"); + } + + @Test + void testSimple() throws Exception { + URL url = URL.parser().parse("http://foo.com/seg1/seg2"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("/seg1/seg2", url.getPath()); + } + + @Test + void testReserved() throws Exception { + URL url = URL.parser().parse("http://foo.com/seg%2F%3B%3Fment/seg=&2"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("/seg%2F%3B%3Fment/seg=&2", url.getPath()); + } + + @Test + void testMatrix() throws Exception { + URL url = URL.parser().parse("http://foo.com/;foo=bar"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("/;foo=bar", url.getPath()); + } + + @Test + void testMatrix2() throws Exception { + URL url = URL.parser().parse("http://foo.com/some;p1=v1/path;p2=v2?q1=v3"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("/some;p1=v1/path;p2=v2", url.getPath()); + Iterator iterator = url.getPathSegments().iterator(); + URLBuilder.PathSegment pathSegment = iterator.next(); + assertEquals("", pathSegment.getSegment()); + assertEquals("[]", pathSegment.getMatrixParams().toString()); + pathSegment = iterator.next(); + assertEquals("some", pathSegment.getSegment()); + assertEquals("p1", pathSegment.getMatrixParams().get(0).getKey()); + assertEquals("v1", pathSegment.getMatrixParams().get(0).getValue()); + pathSegment = iterator.next(); + assertEquals("path", pathSegment.getSegment()); + assertEquals("p2", pathSegment.getMatrixParams().get(0).getKey()); + assertEquals("v2", pathSegment.getMatrixParams().get(0).getValue()); + assertEquals("v3", url.getQueryParams().getAll("DEFAULT", "q1").get(0)); + } + + @Test + void testAnotherQuery() throws Exception { + URL url = URL.parser().parse("http://foo.com?foo=bar"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("foo=bar", url.getQuery()); + } + + @Test + void testQueryAndFragment() throws Exception { + URL url = URL.parser().parse("http://foo.com?foo=bar#fragment"); + assertEquals("http", url.getScheme()); + assertEquals("foo.com", url.getHostInfo()); + assertEquals("foo=bar", url.getQuery()); + assertEquals("fragment", url.getFragment()); + } + + @Test + void testRelative() throws Exception { + URL url = URL.parser().parse("/some/path?foo=bar#fragment"); + assertNull(url.getScheme()); + assertEquals("", url.getHostInfo()); + assertEquals("/some/path", url.getPath()); + assertEquals("foo=bar", url.getQuery()); + assertEquals("fragment", url.getFragment()); + assertEquals("[foo=bar]", url.getQueryParams().toString()); + } + + @Test + void testQueryParams() throws Exception { + URL url = URL.parser().parse("?foo=bar"); + assertEquals("foo=bar", url.getQuery()); + assertEquals("[foo=bar]", url.getQueryParams().toString()); + assertEquals("[k1=v1, k2=v2]", URL.parseQueryString("k1=v1&k2=v2").toString()); + } + + @Test + void testRelativeDecoded() throws Exception { + URL url = URL.parser().parse("/foo/bar%2F?foo=b%2Far#frag%2Fment"); + assertNull(url.getScheme()); + assertEquals("", url.getHostInfo()); + assertEquals("/foo/bar/", url.getDecodedPath()); + assertEquals("foo=b/ar", url.getDecodedQuery()); + assertEquals("frag/ment", url.getDecodedFragment()); + } + + @Test + void testFileSchemeSpecificPart() throws Exception { + URL url = URL.parser().parse("file:foo/bar?foo=bar#fragment"); + assertEquals("", url.getHostInfo()); + assertNotNull(url.getSchemeSpecificPart()); + assertEquals("foo/bar?foo=bar#fragment", url.getSchemeSpecificPart()); + } + + @Test + void testRelativeFilePath() throws Exception { + URL url = URL.parser().parse("file:/foo/bar?foo=bar#fragment"); + assertEquals("file", url.getScheme()); + assertEquals("", url.getHostInfo()); + assertEquals("/foo/bar", url.getPath()); + assertEquals("foo=bar", url.getQuery()); + assertEquals("fragment", url.getFragment()); + } + + @Test + void testAbsoluteFilePath() throws Exception { + URL url = URL.parser().parse("file:///foo/bar?foo=bar#fragment"); + assertEquals("file", url.getScheme()); + assertEquals("", url.getHostInfo()); + assertEquals("/foo/bar", url.getPath()); + assertEquals("foo=bar", url.getQuery()); + assertEquals("fragment", url.getFragment()); + } + + @Test + void testMoreQuery() throws Exception { + URL url = URL.parser().parse("http://foo.com?foo=bar%26%3D%23baz&foo=bar?/2"); + assertEquals("foo=bar%26%3D%23baz&foo=bar?/2", url.getQuery()); + assertEquals("foo=bar&=#baz&foo=bar?/2", url.getDecodedQuery()); + } + + @Test + void testAnotherPlus() throws Exception { + URL url = URL.parser().parse("http://foo.com/has+plus;plusMtx=pl+us?plusQp=pl%2Bus#plus+frag"); + assertEquals("/has+plus;plusMtx=pl+us", url.getPath()); + assertEquals("plusQp=pl%2Bus", url.getQuery()); + assertEquals("plus+frag", url.getFragment()); + } + + @Test + void testUserInfo() throws Exception { + URL url = URL.parser().parse("http://foo:bar@foo.com/"); + assertEquals("foo:bar", url.getUserInfo()); + url = URL.parser().parse("http://foo:foo:bar@foo.com/"); + assertEquals("foo:foo:bar", url.getUserInfo()); + url = URL.parser().parse("http://foo:foo%3Abar@foo.com/"); + assertEquals("foo:foo:bar", url.getUserInfo()); + assertEquals("foo", url.getUser()); + assertEquals("foo:bar", url.getPassword()); + } + + @Test + void testCharset() throws Exception { + // default parser uses UTF-8 + Assertions.assertThrows(URLSyntaxException.class, () -> { + String string = "http%3A%2F%2Flibrary.fes.de%2Flibrary%2Fjournals%2Fde-part%2Fdas-rote-bl%E4ttla%2Findex.html"; + URL url = URL.parser().parse(string); + }); + String string = "http%3A%2F%2Flibrary.fes.de%2Flibrary%2Fjournals%2Fde-part%2Fdas-rote-bl%E4ttla%2Findex.html"; + URL url = URL.parser(StandardCharsets.ISO_8859_1, CodingErrorAction.REPLACE).parse(string); + assertEquals("http://library.fes.de/library/journals/de-part/das-rote-blättla/index.html", url.toString()); + } + + @Test + void testPathQueryFragmentFromPath(){ + URL url = URL.builder() + .path("/a/b?c=d#e") + .build(); + assertEquals("/a/b", url.getPath()); + assertEquals("c=d", url.getQuery()); + assertEquals("e", url.getFragment()); + } + + @Test + void testUrlCharsetReplacementAndReport() { + Charset charset = StandardCharsets.UTF_8; + URL url = URL.builder() + .charset(charset, CodingErrorAction.REPLACE) + .path("/bla%PDblabla?a=b") + .build(); + Parameter queryParameters = url.getQueryParams(); + // %EF%BF%B = 0xFFFD UNICODE REPLACEMENT CHARACTER + assertEquals("/bla%EF%BF%BDblabla", url.getPath()); + assertEquals("[a=b]", queryParameters.toString()); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + URL.builder() + .charset(charset, CodingErrorAction.REPORT) + .path("/bla%PDblabla?a=b") + .build(); + }); + } + + private void assertUrlCompatibility(String url) throws Exception { + String s = URL.from(url).toExternalForm(); + assertEquals(s, URL.from(s).toExternalForm()); + assertEquals(s, new java.net.URL(url).toExternalForm()); + } + + private void assertRoundTrip(String url) { + String s = URL.from(url).toExternalForm(); + assertEquals(s, URL.from(s).toExternalForm()); + } +} diff --git a/net/src/test/java/org/xbib/net/URLResolverTest.java b/net/src/test/java/org/xbib/net/URLResolverTest.java new file mode 100644 index 0000000..e76e2f2 --- /dev/null +++ b/net/src/test/java/org/xbib/net/URLResolverTest.java @@ -0,0 +1,109 @@ +package org.xbib.net; + +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class URLResolverTest { + + @Test + void testResolveURI() { + URI base = URI.create("http://example.org/foo"); + assertEquals("http://example.org/", base.resolve("/").toString()); + assertEquals("http://example.org/foo", base.toString()); + URI rel = URI.create("http://foobar/rel"); + assertEquals("http://foobar/rel", base.resolve(rel).toString()); + assertEquals("http://example.org/foo", base.toString()); + assertEquals("http://example.org/foobar", base.resolve("/foobar").toString()); + assertEquals("http://example.org/foobar", base.resolve("foobar").toString()); + base = URI.create("http://example.org/foo/"); + assertEquals("http://example.org/", base.resolve("/").toString()); + assertEquals("http://example.org/foobar", base.resolve("/foobar").toString()); + assertEquals("http://example.org/foo/foobar", base.resolve("foobar").toString()); + } + + @Test + void testResolveURL() { + URL base = URL.create("http://example.org/foo"); + assertEquals("http://example.org/", base.resolve("/").toString()); + assertEquals("http://example.org/foobar", base.resolve("/foobar").toString()); + assertEquals("http://example.org/foobar", base.resolve("foobar").toString()); + base = URL.create("http://example.org/foo/"); + assertEquals("http://example.org/", base.resolve("/").toString()); + assertEquals("http://example.org/foobar", base.resolve("/foobar").toString()); + assertEquals("http://example.org/foo/foobar", base.resolve("foobar").toString()); + } + + @Test + void testMultiResolve() { + URL base = URL.create("http://example:8080"); + String pathSpec = "foobar/"; + String index = "index.html"; + String queryString = "a=b"; + URL url = base.resolve(pathSpec).resolve(index).mutator().query(queryString).build().normalize(); + assertEquals("http://example:8080/foobar/index.html?a=b", url.toString()); + } + + @Test + void testFielding() throws Exception { + // http://www.ics.uci.edu/~fielding/url/test1.html + resolve("http://a/b/c/d;p?q", "g:h", "g:h"); + resolve("http://a/b/c/d;p?q", "g", "http://a/b/c/g"); + resolve("http://a/b/c/d;p?q", "./g", "http://a/b/c/g"); + resolve("http://a/b/c/d;p?q", "g/", "http://a/b/c/g/"); + resolve("http://a/b/c/d;p?q", "/g", "http://a/g"); + resolve("http://a/b/c/d;p?q", "//g", "http://g"); + resolve("http://a/b/c/d;p?q", "?y", "http://a/b/c/d;p?y"); + resolve("http://a/b/c/d;p?q", "g?y", "http://a/b/c/g?y"); + resolve("http://a/b/c/d;p?q", "#s", "http://a/b/c/d;p?q#s"); + resolve("http://a/b/c/d;p?q", "g#s", "http://a/b/c/g#s"); + resolve("http://a/b/c/d;p?q", "g?y#s", "http://a/b/c/g?y#s"); + resolve("http://a/b/c/d;p?q", ";x", "http://a/b/c/;x"); + resolve("http://a/b/c/d;p?q", "g;x", "http://a/b/c/g;x"); + resolve("http://a/b/c/d;p?q", "g;x?y#s", "http://a/b/c/g;x?y#s"); + resolve("http://a/b/c/d;p?q", ".", "http://a/b/c/"); + resolve("http://a/b/c/d;p?q", "./", "http://a/b/c/"); + resolve("http://a/b/c/d;p?q", "..", "http://a/b/"); + resolve("http://a/b/c/d;p?q", "../", "http://a/b/"); + resolve("http://a/b/c/d;p?q", "../g", "http://a/b/g"); + resolve("http://a/b/c/d;p?q", "../..", "http://a/"); + resolve("http://a/b/c/d;p?q", "../../", "http://a/"); + resolve("http://a/b/c/d;p?q", "../../g", "http://a/g"); + // abnormal cases + resolve("http://a/b/c/d;p?q", "../../../g", "http://a/g"); + resolve("http://a/b/c/d;p?q", "../../../../g", "http://a/g"); + resolve("http://a/b/c/d;p?q", "/./g", "http://a/g"); + resolve("http://a/b/c/d;p?q", "/../g", "http://a/g"); + resolve("http://a/b/c/d;p?q", "g.", "http://a/b/c/g."); + resolve("http://a/b/c/d;p?q", ".g", "http://a/b/c/.g"); + resolve("http://a/b/c/d;p?q", "g..", "http://a/b/c/g.."); + resolve("http://a/b/c/d;p?q", "..g", "http://a/b/c/..g"); + // less likely + resolve("http://a/b/c/d;p?q", "./../g", "http://a/b/g"); + resolve("http://a/b/c/d;p?q", "./g/.", "http://a/b/c/g/"); + resolve("http://a/b/c/d;p?q", "g/./h", "http://a/b/c/g/h"); + resolve("http://a/b/c/d;p?q", "g/../h", "http://a/b/c/h"); + resolve("http://a/b/c/d;p?q", "g;x=1/./y", "http://a/b/c/g;x=1/y"); + resolve("http://a/b/c/d;p?q", "g;x=1/../y", "http://a/b/c/y"); + // query component + resolve("http://a/b/c/d;p?q", "g?y/./x", "http://a/b/c/g?y/./x"); + resolve("http://a/b/c/d;p?q", "g?y/../x", "http://a/b/c/g?y/../x"); + // fragment component + resolve("http://a/b/c/d;p?q", "g#s/./x", "http://a/b/c/g#s/./x"); + resolve("http://a/b/c/d;p?q", "g#s/../x", "http://a/b/c/g#s/../x"); + // scheme + resolve("http://a/b/c/d;p?q", "http:g", "http:g"); + resolve("http://a/b/c/d;p?q", "http:", "http:"); + // absolute + resolve("http://a/b/c/d;p?q", "http://e/f/g/h", "http://e/f/g/h"); + } + + private void resolve(String inputBase, String spec, String expected) + throws URLSyntaxException, MalformedInputException, UnmappableCharacterException { + assertEquals(expected, URL.base(inputBase).resolve(spec).toExternalForm()); + } +} diff --git a/net/src/test/java/org/xbib/net/URLTest.java b/net/src/test/java/org/xbib/net/URLTest.java new file mode 100644 index 0000000..698f341 --- /dev/null +++ b/net/src/test/java/org/xbib/net/URLTest.java @@ -0,0 +1,119 @@ +package org.xbib.net; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class URLTest { + + @Test + void test() throws Exception { + List tests = readTests(fromResource("/urltestdata.json")); + for (JsonTest test : tests) { + String base = test.base; + String input = test.input; + if (test.skip) { + continue; + } + if (test.failure) { + try { + URL.base(base).resolve(input); + fail("base = " + base + " input = " + input); + } catch (Exception e) { + // pass + } + } else { + if (base != null && input != null) { + try { + URL url = URL.base(base).resolve(input); + if (test.protocol != null) { + assertEquals(test.protocol, url.getScheme() + ":"); + } + if (test.hostname != null) { + // default in Mac OS + String host = url.getHost(); + if ("broadcasthost".equals(host)) { + host = "255.255.255.255"; + } + assertEquals(test.hostname, host); + } + if (test.port != null && !test.port.isEmpty() && url.getPort() != null) { + assertEquals(Integer.parseInt(test.port), (int) url.getPort()); + } + // TODO(jprante) + //if (test.pathname != null && !test.pathname.isEmpty() && url.getPath() != null) { + // assertEquals(test.pathname, url.getPath()); + //} + //System.err.println("passed: " + base + " " + input); + } catch (URLSyntaxException e) { + //System.err.println("unable to resolve: " + base + " " + input + " reason: " + e.getMessage()); + } + } + } + } + } + + private JsonNode fromResource(String path) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + ObjectReader reader = mapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true) + .readerFor(JsonNode.class); + return reader.readValue(getClass().getResourceAsStream(path)); + } + + private List readTests(JsonNode jsonNode) { + List list = new ArrayList<>(); + for (JsonNode n : jsonNode) { + if (n.isObject()) { + JsonTest jsontest = new JsonTest(); + jsontest.input = get(n, "input"); + jsontest.base = get(n, "base"); + jsontest.href = get(n, "href"); + jsontest.origin = get(n, "origin"); + jsontest.protocol = get(n, "protocol"); + jsontest.username = get(n, "username"); + jsontest.password = get(n, "password"); + jsontest.host = get(n, "host"); + jsontest.hostname = get(n, "hostname"); + jsontest.port = get(n, "port"); + jsontest.pathname = get(n, "pathname"); + jsontest.search = get(n, "search"); + jsontest.hash = get(n, "hash"); + jsontest.failure = n.has("failure"); + jsontest.skip = n.has("skip"); + list.add(jsontest); + } + } + return list; + } + + private String get(JsonNode n, String key) { + return n.has(key) ? n.get(key).textValue() : null; + } + + static class JsonTest { + String input; + String base; + String href; + String origin; + String protocol; + String username; + String password; + String host; + String hostname; + String port; + String pathname; + String search; + String hash; + boolean failure; + boolean skip; + } + +} diff --git a/net/src/test/java/org/xbib/net/package-info.java b/net/src/test/java/org/xbib/net/package-info.java new file mode 100644 index 0000000..9103c39 --- /dev/null +++ b/net/src/test/java/org/xbib/net/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for testing URL building und parsing. + */ +package org.xbib.net; diff --git a/net/src/test/java/org/xbib/net/template/URITemplateTest.java b/net/src/test/java/org/xbib/net/template/URITemplateTest.java new file mode 100644 index 0000000..a1487de --- /dev/null +++ b/net/src/test/java/org/xbib/net/template/URITemplateTest.java @@ -0,0 +1,410 @@ +package org.xbib.net.template; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import org.junit.jupiter.api.Test; +import org.xbib.net.template.expression.ExpressionType; +import org.xbib.net.template.expression.TemplateExpression; +import org.xbib.net.template.expression.URITemplateExpression; +import org.xbib.net.template.parse.ExpressionParser; +import org.xbib.net.template.parse.URITemplateParser; +import org.xbib.net.template.parse.VariableSpecParser; +import org.xbib.net.template.vars.Variables; +import org.xbib.net.template.vars.specs.ExplodedVariable; +import org.xbib.net.template.vars.specs.PrefixVariable; +import org.xbib.net.template.vars.specs.SimpleVariable; +import org.xbib.net.template.vars.specs.VariableSpec; +import org.xbib.net.template.vars.specs.VariableSpecType; +import org.xbib.net.template.vars.values.ListValue; +import org.xbib.net.template.vars.values.MapValue; +import org.xbib.net.template.vars.values.NullValue; +import org.xbib.net.template.vars.values.ScalarValue; +import org.xbib.net.template.vars.values.VariableValue; + +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +class URITemplateTest { + + @Test + void simpleTest() { + String[] strings = new String[]{ + "foo", "%33foo", "foo%20", "foo_%20bar", "FoOb%02ZAZE287", "foo.bar", "foo_%20bar.baz%af.r" + }; + for (String s : strings) { + CharBuffer buffer = CharBuffer.wrap(s).asReadOnlyBuffer(); + VariableSpec varspec = VariableSpecParser.parse(buffer); + assertEquals(varspec.getName(), s); + assertSame(varspec.getType(), VariableSpecType.SIMPLE); + assertFalse(buffer.hasRemaining()); + } + } + + @Test + void invalidTest() { + String[] strings = new String[]{"", "%", "foo..bar", ".", "foo%ra", "foo%ar"}; + for (String s : strings) { + try { + CharBuffer buffer = CharBuffer.wrap(s).asReadOnlyBuffer(); + VariableSpecParser.parse(buffer); + fail("No exception thrown"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + } + } + + @Test + void literalTest() { + Variables vars = Variables.builder().build(); + String[] strings = new String[]{"foo", "%23foo", "%23foo%24", "foo%24", "f%c4oo", "http://slashdot.org", + "x?y=e", "urn:d:ze:/oize#/e/e", "ftp://ftp.foo.com/ee/z?a=b#e/dz", + "http://z.t/hello%20world"}; + for (String s : strings) { + CharBuffer buffer = CharBuffer.wrap(s).asReadOnlyBuffer(); + List list = URITemplateParser.parse(buffer); + assertEquals(list.get(0).expand(vars), s); + assertFalse(buffer.hasRemaining()); + } + } + + @Test + void parsingEmptyInputGivesEmptyList() { + CharBuffer buffer = CharBuffer.wrap("").asReadOnlyBuffer(); + List list = URITemplateParser.parse(buffer); + assertTrue(list.isEmpty()); + assertFalse(buffer.hasRemaining()); + } + + @Test + @SuppressWarnings("unchecked") + void parseExpressions() { + List list = new ArrayList<>(); + String input; + ExpressionType type; + List varspecs; + + input = "{foo}"; + type = ExpressionType.SIMPLE; + varspecs = Collections.singletonList(new SimpleVariable("foo")); + list.add(new Object[]{input, type, varspecs}); + + input = "{foo,bar}"; + type = ExpressionType.SIMPLE; + varspecs = Arrays.asList(new SimpleVariable("foo"), new SimpleVariable("bar")); + list.add(new Object[]{input, type, varspecs}); + + input = "{+foo}"; + type = ExpressionType.RESERVED; + varspecs = Collections.singletonList(new SimpleVariable("foo")); + list.add(new Object[]{input, type, varspecs}); + + input = "{.foo:10,bar*}"; + type = ExpressionType.NAME_LABELS; + varspecs = Arrays.asList(new PrefixVariable("foo", 10), new ExplodedVariable("bar")); + list.add(new Object[]{input, type, varspecs}); + + for (Object[] o : list) { + CharBuffer buffer = CharBuffer.wrap((CharSequence) o[0]).asReadOnlyBuffer(); + URITemplateExpression actual = new ExpressionParser().parse(buffer); + assertFalse(buffer.hasRemaining()); + URITemplateExpression expected = new TemplateExpression((ExpressionType) o[1], (List) o[2]); + assertEquals(actual, expected); + } + } + + @Test + void parseInvalidExpressions() { + try { + CharBuffer buffer = CharBuffer.wrap("{foo").asReadOnlyBuffer(); + new ExpressionParser().parse(buffer); + fail("No exception thrown"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + try { + CharBuffer buffer = CharBuffer.wrap("{foo#bar}").asReadOnlyBuffer(); + new ExpressionParser().parse(buffer); + fail("No exception thrown"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + } + + @Test + void parsePrefixes() { + String[] strings = new String[]{"foo:323", "%33foo:323", "foo%20:323", "foo_%20bar:323", "FoOb%02ZAZE287:323", + "foo.bar:323", "foo_%20bar.baz%af.r:323"}; + for (String s : strings) { + CharBuffer buffer = CharBuffer.wrap(s).asReadOnlyBuffer(); + VariableSpec varspec = VariableSpecParser.parse(buffer); + assertEquals(varspec.getName(), s.substring(0, s.indexOf(':'))); + assertSame(varspec.getType(), VariableSpecType.PREFIX); + assertFalse(buffer.hasRemaining()); + } + } + + @Test + void parseInvalidPrefixes() { + String[] strings = new String[]{"foo:", "foo:-1", "foo:a", "foo:10001", "foo:2147483648"}; + for (String s : strings) { + try { + VariableSpecParser.parse(CharBuffer.wrap(s).asReadOnlyBuffer()); + fail("No exception thrown!!"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + } + } + + @Test + void parseExploded() { + String[] strings = new String[]{"foo*", "%33foo*", "foo%20*", "foo_%20bar*", "FoOb%02ZAZE287*", "foo.bar*", + "foo_%20bar.baz%af.r*"}; + for (String s : strings) { + CharBuffer buffer = CharBuffer.wrap(s).asReadOnlyBuffer(); + VariableSpec varspec = VariableSpecParser.parse(buffer); + assertEquals(varspec.getName(), s.substring(0, s.length() - 1)); + assertSame(varspec.getType(), VariableSpecType.EXPLODED); + assertFalse(buffer.hasRemaining()); + } + } + + @Test + void parseExceptions() { + String[] strings = new String[]{"foo%", "foo%r", "foo%ra", "foo%ar", "foo<", "foo{"}; + for (String s : strings) { + try { + URITemplateParser.parse(s); + fail("No exception thrown!!"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + } + } + + @Test + void testExamples() throws Exception { + JsonNode data = fromResource("/spec-examples.json"); + List> list = new ArrayList<>(); + for (JsonNode node : data) { + Variables.Builder builder = Variables.builder(); + Iterator> it = node.get("variables").fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + builder.add(entry.getKey(), fromJson(entry.getValue())); + } + for (JsonNode n : node.get("testcases")) { + Map m = new HashMap<>(); + m.put("tmpl", n.get(0).textValue()); + m.put("vars", builder.build()); + m.put("resultNode", n.get(1)); + list.add(m); + } + } + for (Map e : list) { + URITemplate template = new URITemplate((String) e.get("tmpl")); + String actual = template.toString((Variables) e.get("vars")); + JsonNode resultNode = (JsonNode) e.get("resultNode"); + if (resultNode.isTextual()) { + assertEquals(resultNode.textValue(), actual); + } else { + if (!resultNode.isArray()) { + throw new IllegalArgumentException("didn't expect that"); + } + boolean found = false; + for (JsonNode node : resultNode) { + if (node.textValue().equals(actual)) { + found = true; + } + } + assertTrue(found); + } + } + } + + @Test + void testExamplesBySection() throws Exception { + JsonNode data = fromResource("/spec-examples-by-section.json"); + List> list = new ArrayList<>(); + for (JsonNode node : data) { + Variables.Builder builder = Variables.builder(); + Iterator> it = node.get("variables").fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + builder.add(entry.getKey(), fromJson(entry.getValue())); + } + for (JsonNode n : node.get("testcases")) { + Map m = new HashMap<>(); + m.put("tmpl", n.get(0).textValue()); + m.put("vars", builder.build()); + m.put("resultNode", n.get(1)); + list.add(m); + } + } + for (Map e : list) { + URITemplate template = new URITemplate((String) e.get("tmpl")); + String actual = template.toString((Variables) e.get("vars")); + JsonNode resultNode = (JsonNode) e.get("resultNode"); + if (resultNode.isTextual()) { + assertEquals(resultNode.textValue(), actual); + } else { + if (!resultNode.isArray()) { + throw new IllegalArgumentException("didn't expect that"); + } + boolean found = false; + for (JsonNode node : resultNode) { + if (node.textValue().equals(actual)) { + found = true; + } + } + assertTrue(found); + } + } + } + + @Test + void extendedTests() throws Exception { + JsonNode data = fromResource("/extended-tests.json"); + List> list = new ArrayList<>(); + for (JsonNode node : data) { + Variables.Builder builder = Variables.builder(); + Iterator> it = node.get("variables").fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + builder.add(entry.getKey(), fromJson(entry.getValue())); + } + for (JsonNode n : node.get("testcases")) { + Map m = new HashMap<>(); + m.put("tmpl", n.get(0).textValue()); + m.put("vars", builder.build()); + m.put("resultNode", n.get(1)); + list.add(m); + } + } + for (Map e : list) { + URITemplate template = new URITemplate((String) e.get("tmpl")); + String actual = template.toString((Variables) e.get("vars")); + JsonNode resultNode = (JsonNode) e.get("resultNode"); + if (resultNode.isTextual()) { + assertEquals(resultNode.textValue(), actual); + } else { + if (!resultNode.isArray()) { + throw new IllegalArgumentException("didn't expect that"); + } + boolean found = false; + for (JsonNode node : resultNode) { + if (node.textValue().equals(actual)) { + found = true; + } + } + assertTrue(found); + } + } + } + + @Test + void negativeTests() throws Exception { + JsonNode data = fromResource("/negative-tests.json"); + JsonNode node = data.get("Failure Tests").get("variables"); + Variables.Builder builder = Variables.builder(); + Iterator> it = node.fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + builder.add(entry.getKey(), fromJson(entry.getValue())); + } + List> list = new ArrayList<>(); + for (JsonNode n : data.get("Failure Tests").get("testcases")) { + Map m = new HashMap<>(); + m.put("tmpl", n.get(0).textValue()); + m.put("vars", builder.build()); + list.add(m); + } + for (Map e : list) { + try { + new URITemplate((String) e.get("tmpl")).toString((Variables) e.get("vars")); + fail("no exception thrown"); + } catch (Exception ex) { + assertTrue(ex instanceof IllegalArgumentException); + } + } + } + + @Test + void expansionTest() throws Exception { + String[] strings = new String[]{"/rfcExamples.json", "/strings.json", "/multipleStrings.json", + "/lists.json", "/multipleLists.json"}; + for (String s : strings) { + JsonNode data = fromResource(s); + Variables.Builder builder = Variables.builder(); + Iterator> it = data.get("vars").fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (!entry.getValue().isNull()) { + builder.add(entry.getKey(), fromJson(entry.getValue())); + } + } + List> list = new ArrayList<>(); + it = data.get("tests").fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + Map m = new HashMap<>(); + m.put("tmpl", entry.getKey()); + m.put("vars", builder.build()); + m.put("expected", entry.getValue().textValue()); + list.add(m); + } + for (Map e : list) { + String actual = new URITemplate((String) e.get("tmpl")).toString((Variables) e.get("vars")); + assertEquals(e.get("expected"), actual); + } + } + } + + private JsonNode fromResource(String path) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + ObjectReader reader = mapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true) + .readerFor(JsonNode.class); + return reader.readValue(getClass().getResourceAsStream(path)); + } + + private static VariableValue fromJson(JsonNode node) { + if (node.isTextual()) { + return new ScalarValue(node.textValue()); + } + if (node.isArray()) { + ListValue.Builder builder = ListValue.builder(); + for (JsonNode n : node) { + builder.add(n.textValue()); + } + return builder.build(); + } + if (node.isObject()) { + MapValue.Builder builder = MapValue.builder(); + Iterator> it = node.fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + builder.put(entry.getKey(), entry.getValue().textValue()); + } + return builder.build(); + } + if (node.isNull()) { + return new NullValue(); + } + throw new IllegalArgumentException("cannot bind JSON to variable value: " + node + " class " + node.getClass()); + } +} diff --git a/net/src/test/java/org/xbib/net/template/package-info.java b/net/src/test/java/org/xbib/net/template/package-info.java new file mode 100644 index 0000000..eb5045e --- /dev/null +++ b/net/src/test/java/org/xbib/net/template/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for testing URL templates. + */ +package org.xbib.net.template; diff --git a/net/src/test/resources/extended-tests.json b/net/src/test/resources/extended-tests.json new file mode 100644 index 0000000..dcd71bb --- /dev/null +++ b/net/src/test/resources/extended-tests.json @@ -0,0 +1,119 @@ +{ + "Additional Examples 1":{ + "level":4, + "variables":{ + "id" : "person", + "token" : "12345", + "fields" : ["id", "name", "picture"], + "format" : "json", + "q" : "URI Templates", + "page" : "5", + "lang" : "en", + "geocode" : ["37.76","-122.427"], + "first_name" : "John", + "last.name" : "Doe", + "Some%20Thing" : "foo", + "number" : "6", + "long" : "37.76", + "lat" : "-122.427", + "group_id" : "12345", + "query" : "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }", + "uri" : "http://example.org/?uri=http%3A%2F%2Fexample.org%2F", + "word" : "drücken", + "Stra%C3%9Fe" : "Grüner Weg", + "random" : "šö䟜ñꀣ¥‡ÑÒÓÔÕÖ×ØÙÚàáâãäåæçÿ", + "assoc_special_chars" : + { "šö䟜ñꀣ¥‡ÑÒÓÔÕ" : "Ö×ØÙÚàáâãäåæçÿ" } + }, + "testcases":[ + + [ "{/id*}" , "/person" ], + [ "{/id*}{?fields,first_name,last.name,token}" , [ + "/person?fields=id,name,picture&first_name=John&last.name=Doe&token=12345", + "/person?fields=id,picture,name&first_name=John&last.name=Doe&token=12345", + "/person?fields=picture,name,id&first_name=John&last.name=Doe&token=12345", + "/person?fields=picture,id,name&first_name=John&last.name=Doe&token=12345", + "/person?fields=name,picture,id&first_name=John&last.name=Doe&token=12345", + "/person?fields=name,id,picture&first_name=John&last.name=Doe&token=12345"] + ], + ["/search.{format}{?q,geocode,lang,locale,page,result_type}", + [ "/search.json?q=URI%20Templates&geocode=37.76,-122.427&lang=en&page=5", + "/search.json?q=URI%20Templates&geocode=-122.427,37.76&lang=en&page=5"] + ], + ["/test{/Some%20Thing}", "/test/foo" ], + ["/set{?number}", "/set?number=6"], + ["/loc{?long,lat}" , "/loc?long=37.76&lat=-122.427"], + ["/base{/group_id,first_name}/pages{/page,lang}{?format,q}","/base/12345/John/pages/5/en?format=json&q=URI%20Templates"], + ["/sparql{?query}", "/sparql?query=PREFIX%20dc%3A%20%3Chttp%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%3E%20SELECT%20%3Fbook%20%3Fwho%20WHERE%20%7B%20%3Fbook%20dc%3Acreator%20%3Fwho%20%7D"], + ["/go{?uri}", "/go?uri=http%3A%2F%2Fexample.org%2F%3Furi%3Dhttp%253A%252F%252Fexample.org%252F"], + ["/service{?word}", "/service?word=dr%C3%BCcken"], + ["/lookup{?Stra%C3%9Fe}", "/lookup?Stra%C3%9Fe=Gr%C3%BCner%20Weg"], + ["{random}" , "%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF"], + ["{?assoc_special_chars*}", "?%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95=%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF"] + ] + }, + "Additional Examples 2":{ + "level":4, + "variables":{ + "id" : ["person","albums"], + "token" : "12345", + "fields" : ["id", "name", "picture"], + "format" : "atom", + "q" : "URI Templates", + "page" : "10", + "start" : "5", + "lang" : "en", + "geocode" : ["37.76","-122.427"] + }, + "testcases":[ + + [ "{/id*}" , ["/person/albums","/albums/person"] ], + [ "{/id*}{?fields,token}" , [ + "/person/albums?fields=id,name,picture&token=12345", + "/person/albums?fields=id,picture,name&token=12345", + "/person/albums?fields=picture,name,id&token=12345", + "/person/albums?fields=picture,id,name&token=12345", + "/person/albums?fields=name,picture,id&token=12345", + "/person/albums?fields=name,id,picture&token=12345", + "/albums/person?fields=id,name,picture&token=12345", + "/albums/person?fields=id,picture,name&token=12345", + "/albums/person?fields=picture,name,id&token=12345", + "/albums/person?fields=picture,id,name&token=12345", + "/albums/person?fields=name,picture,id&token=12345", + "/albums/person?fields=name,id,picture&token=12345"] + ] + ] + }, + "Additional Examples 3: Empty Variables":{ + "disabled": true, + "variables" : { + "empty_list" : [], + "empty_assoc" : {} + }, + "testcases":[ + [ "{/empty_list}", [ "" ] ], + [ "{/empty_list*}", [ "" ] ], + [ "{?empty_list}", [ "?empty_list="] ], + [ "{?empty_list*}", [ "" ] ], + [ "{?empty_assoc}", [ "?empty_assoc=" ] ], + [ "{?empty_assoc*}", [ "" ] ] + ] + }, + "Additional Examples 4: Numeric Keys":{ + "variables" : { + "42" : "The Answer to the Ultimate Question of Life, the Universe, and Everything", + "1337" : ["leet", "as","it", "can","be"], + "german" : { + "11": "elf", + "12": "zwölf" + } + }, + "testcases":[ + [ "{42}", "The%20Answer%20to%20the%20Ultimate%20Question%20of%20Life%2C%20the%20Universe%2C%20and%20Everything"], + [ "{?42}", "?42=The%20Answer%20to%20the%20Ultimate%20Question%20of%20Life%2C%20the%20Universe%2C%20and%20Everything"], + [ "{1337}", "leet,as,it,can,be"], + [ "{?1337*}", "?1337=leet&1337=as&1337=it&1337=can&1337=be"], + [ "{?german*}", [ "?11=elf&12=zw%C3%B6lf", "?12=zw%C3%B6lf&11=elf"] ] + ] + } +} diff --git a/net/src/test/resources/lists.json b/net/src/test/resources/lists.json new file mode 100644 index 0000000..503bbe4 --- /dev/null +++ b/net/src/test/resources/lists.json @@ -0,0 +1,74 @@ +{ + "vars": { + "list1": [ "one", "two", "three" ], + "list2": [ "Hello", "World!" ], + "list3": [ "one", "", "three" ], + "empty": [] + }, + "tests": { + "{list1}": "one,two,three", + "{+list1}": "one,two,three", + "{.list1}": ".one,two,three", + "{/list1}": "/one,two,three", + "{;list1}": ";list1=one,two,three", + "{?list1}": "?list1=one,two,three", + "{&list1}": "&list1=one,two,three", + "{#list1}": "#one,two,three", + "{list2}": "Hello,World%21", + "{+list2}": "Hello,World!", + "{.list2}": ".Hello,World%21", + "{/list2}": "/Hello,World%21", + "{;list2}": ";list2=Hello,World%21", + "{?list2}": "?list2=Hello,World%21", + "{&list2}": "&list2=Hello,World%21", + "{#list2}": "#Hello,World!", + "{list3}": "one,,three", + "{+list3}": "one,,three", + "{.list3}": ".one,,three", + "{/list3}": "/one,,three", + "{;list3}": ";list3=one,,three", + "{?list3}": "?list3=one,,three", + "{&list3}": "&list3=one,,three", + "{#list3}": "#one,,three", + "{empty}": "", + "{+empty}": "", + "{.empty}": "", + "{/empty}": "", + "{;empty}": ";empty", + "{?empty}": "?empty=", + "{&empty}": "&empty=", + "{#empty}": "", + "{list1*}": "one,two,three", + "{+list1*}": "one,two,three", + "{.list1*}": ".one.two.three", + "{/list1*}": "/one/two/three", + "{;list1*}": ";list1=one;list1=two;list1=three", + "{?list1*}": "?list1=one&list1=two&list1=three", + "{&list1*}": "&list1=one&list1=two&list1=three", + "{#list1*}": "#one,two,three", + "{list2*}": "Hello,World%21", + "{+list2*}": "Hello,World!", + "{.list2*}": ".Hello.World%21", + "{/list2*}": "/Hello/World%21", + "{;list2*}": ";list2=Hello;list2=World%21", + "{?list2*}": "?list2=Hello&list2=World%21", + "{&list2*}": "&list2=Hello&list2=World%21", + "{#list2*}": "#Hello,World!", + "{list3*}": "one,,three", + "{+list3*}": "one,,three", + "{.list3*}": ".one..three", + "{/list3*}": "/one//three", + "{;list3*}": ";list3=one;list3;list3=three", + "{?list3*}": "?list3=one&list3=&list3=three", + "{&list3*}": "&list3=one&list3=&list3=three", + "{#list3*}": "#one,,three", + "{empty*}": "", + "{+empty*}": "", + "{.empty*}": "", + "{/empty*}": "", + "{;empty*}": "", + "{?empty*}": "", + "{&empty*}": "", + "{#empty*}": "" + } +} \ No newline at end of file diff --git a/net/src/test/resources/logging.properties b/net/src/test/resources/logging.properties new file mode 100644 index 0000000..d9913d4 --- /dev/null +++ b/net/src/test/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter +jdk.event.security.level=INFO diff --git a/net/src/test/resources/multipleLists.json b/net/src/test/resources/multipleLists.json new file mode 100644 index 0000000..8c4ec21 --- /dev/null +++ b/net/src/test/resources/multipleLists.json @@ -0,0 +1,41 @@ +{ + "vars": { + "list1": [ "one", "two", "three" ], + "list2": [ "Hello", "World!" ], + "empty": [] + }, + "tests": { + "{list1,list2}": "one,two,three,Hello,World%21", + "{+list1,list2}": "one,two,three,Hello,World!", + "{.list1,list2}": ".one,two,three.Hello,World%21", + "{/list1,list2}": "/one,two,three/Hello,World%21", + "{;list1,list2}": ";list1=one,two,three;list2=Hello,World%21", + "{?list1,list2}": "?list1=one,two,three&list2=Hello,World%21", + "{&list1,list2}": "&list1=one,two,three&list2=Hello,World%21", + "{#list1,list2}": "#one,two,three,Hello,World!", + "{list1*,list2}": "one,two,three,Hello,World%21", + "{+list1*,list2}": "one,two,three,Hello,World!", + "{.list1*,list2}": ".one.two.three.Hello,World%21", + "{/list1*,list2}": "/one/two/three/Hello,World%21", + "{;list1*,list2}": ";list1=one;list1=two;list1=three;list2=Hello,World%21", + "{?list1*,list2}": "?list1=one&list1=two&list1=three&list2=Hello,World%21", + "{&list1*,list2}": "&list1=one&list1=two&list1=three&list2=Hello,World%21", + "{#list1*,list2}": "#one,two,three,Hello,World!", + "{list1,empty}": "one,two,three", + "{+list1,empty}": "one,two,three", + "{.list1,empty}": ".one,two,three", + "{/list1,empty}": "/one,two,three", + "{;list1,empty}": ";list1=one,two,three;empty", + "{?list1,empty}": "?list1=one,two,three&empty=", + "{&list1,empty}": "&list1=one,two,three&empty=", + "{#list1,empty}": "#one,two,three", + "{list1,empty*}": "one,two,three", + "{+list1,empty*}": "one,two,three", + "{.list1,empty*}": ".one,two,three", + "{/list1,empty*}": "/one,two,three", + "{;list1,empty*}": ";list1=one,two,three", + "{?list1,empty*}": "?list1=one,two,three", + "{&list1,empty*}": "&list1=one,two,three", + "{#list1,empty*}": "#one,two,three" + } +} \ No newline at end of file diff --git a/net/src/test/resources/multipleStrings.json b/net/src/test/resources/multipleStrings.json new file mode 100644 index 0000000..9add677 --- /dev/null +++ b/net/src/test/resources/multipleStrings.json @@ -0,0 +1,65 @@ +{ + "vars": { + "var": "value", + "hello": "Hello World!", + "empty": "" + }, + "tests": { + "{var,undef}": "value", + "{+var,undef}": "value", + "{.var,undef}": ".value", + "{/var,undef}": "/value", + "{;var,undef}": ";var=value", + "{?var,undef}": "?var=value", + "{&var,undef}": "&var=value", + "{#var,undef}": "#value", + "{var,empty}": "value,", + "{+var,empty}": "value,", + "{.var,empty}": ".value.", + "{/var,empty}": "/value/", + "{;var,empty}": ";var=value;empty", + "{?var,empty}": "?var=value&empty=", + "{&var,empty}": "&var=value&empty=", + "{#var,empty}": "#value,", + "{var,hello}": "value,Hello%20World%21", + "{+var,hello}": "value,Hello%20World!", + "{.var,hello}": ".value.Hello%20World%21", + "{/var,hello}": "/value/Hello%20World%21", + "{;var,hello}": ";var=value;hello=Hello%20World%21", + "{?var,hello}": "?var=value&hello=Hello%20World%21", + "{&var,hello}": "&var=value&hello=Hello%20World%21", + "{#var,hello}": "#value,Hello%20World!", + "{var,undef,empty}": "value,", + "{+var,undef,empty}": "value,", + "{.var,undef,empty}": ".value.", + "{/var,undef,empty}": "/value/", + "{;var,undef,empty}": ";var=value;empty", + "{?var,undef,empty}": "?var=value&empty=", + "{&var,undef,empty}": "&var=value&empty=", + "{#var,undef,empty}": "#value,", + "{var,undef,hello}": "value,Hello%20World%21", + "{+var,undef,hello}": "value,Hello%20World!", + "{.var,undef,hello}": ".value.Hello%20World%21", + "{/var,undef,hello}": "/value/Hello%20World%21", + "{;var,undef,hello}": ";var=value;hello=Hello%20World%21", + "{?var,undef,hello}": "?var=value&hello=Hello%20World%21", + "{&var,undef,hello}": "&var=value&hello=Hello%20World%21", + "{#var,undef,hello}": "#value,Hello%20World!", + "{var,empty,undef}": "value,", + "{+var,empty,undef}": "value,", + "{.var,empty,undef}": ".value.", + "{/var,empty,undef}": "/value/", + "{;var,empty,undef}": ";var=value;empty", + "{?var,empty,undef}": "?var=value&empty=", + "{&var,empty,undef}": "&var=value&empty=", + "{#var,empty,undef}": "#value,", + "{var,hello,undef}": "value,Hello%20World%21", + "{+var,hello,undef}": "value,Hello%20World!", + "{.var,hello,undef}": ".value.Hello%20World%21", + "{/var,hello,undef}": "/value/Hello%20World%21", + "{;var,hello,undef}": ";var=value;hello=Hello%20World%21", + "{?var,hello,undef}": "?var=value&hello=Hello%20World%21", + "{&var,hello,undef}": "&var=value&hello=Hello%20World%21", + "{#var,hello,undef}": "#value,Hello%20World!" + } +} \ No newline at end of file diff --git a/net/src/test/resources/negative-tests.json b/net/src/test/resources/negative-tests.json new file mode 100644 index 0000000..e302640 --- /dev/null +++ b/net/src/test/resources/negative-tests.json @@ -0,0 +1,49 @@ +{ + "Failure Tests":{ + "level":4, + "variables":{ + "id" : "thing", + "var" : "value", + "hello" : "Hello World!", + "empty" : "", + "path" : "/foo/bar", + "x" : "1024", + "y" : "768", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "example" : "red", + "searchTerms" : "uri templates", + "~thing" : "some-user", + "default-graph-uri" : ["http://www.example/book/","http://www.example/papers/"], + "query" : "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }" + + }, + "testcases":[ + [ "{/id*", false ], + [ "/id*}", false ], + [ "{/?id}", false ], + [ "{var:prefix}", false ], + [ "{hello:2*}", false ] , + [ "{??hello}", false ] , + [ "{!hello}", false ] , + [ "{=path}", false ] , + [ "{$var}", false ], + [ "{|var*}", false ], + [ "{*keys?}", false ], + [ "{?empty=default,var}", false ], + [ "{var}{-prefix|/-/|var}" , false ], + [ "?q={searchTerms}&c={example:color?}" , false ], + [ "x{?empty|foo=none}" , false ], + [ "/h{#hello+}" , false ], + [ "/h#{hello+}" , false ], + [ "{keys:1}", false ], + [ "{+keys:1}", false ], + [ "{;keys:1*}", false ], + [ "?{-join|&|var,list}" , false ], + [ "/people/{~thing}", false], + [ "/sparql{?query){&default-graph-uri*}", false ], + [ "/resolution{?x, y}" , false ] + + ] + } +} \ No newline at end of file diff --git a/net/src/test/resources/rfcExamples.json b/net/src/test/resources/rfcExamples.json new file mode 100644 index 0000000..1a13d5c --- /dev/null +++ b/net/src/test/resources/rfcExamples.json @@ -0,0 +1,80 @@ +{ + "vars": { + "var": "value", + "hello": "Hello World!", + "path": "/foo/bar", + "empty": "", + "x": "1024", + "y": "768", + "list": [ "red", "green", "blue" ], + "keys": { "semi": ";", "dot": ".", "comma": "," } + }, + "tests": { + "{var}": "value", + "{hello}": "Hello%20World%21", + "{+var}": "value", + "{+hello}": "Hello%20World!", + "{+path}/here": "/foo/bar/here", + "here?ref={+path}": "here?ref=/foo/bar", + "X{#var}": "X#value", + "X{#hello}": "X#Hello%20World!", + "map?{x,y}": "map?1024,768", + "{x,hello,y}": "1024,Hello%20World%21,768", + "{+x,hello,y}": "1024,Hello%20World!,768", + "{+path,x}/here": "/foo/bar,1024/here", + "{#x,hello,y}": "#1024,Hello%20World!,768", + "{#path,x}/here": "#/foo/bar,1024/here", + "X{.var}": "X.value", + "X{.x,y}": "X.1024.768", + "{/var}": "/value", + "{/var,x}/here": "/value/1024/here", + "{;x,y}": ";x=1024;y=768", + "{;x,y,empty}": ";x=1024;y=768;empty", + "{?x,y}": "?x=1024&y=768", + "{?x,y,empty}": "?x=1024&y=768&empty=", + "?fixed=yes{&x}": "?fixed=yes&x=1024", + "{&x,y,empty}": "&x=1024&y=768&empty=", + "{var:3}": "val", + "{var:30}": "value", + "{list}": "red,green,blue", + "{list*}": "red,green,blue", + "{keys}": "semi,%3B,dot,.,comma,%2C", + "{keys*}": "semi=%3B,dot=.,comma=%2C", + "{+path:6}/here": "/foo/b/here", + "{+list}": "red,green,blue", + "{+list*}": "red,green,blue", + "{+keys}": "semi,;,dot,.,comma,,", + "{+keys*}": "semi=;,dot=.,comma=,", + "{#path:6}/here": "#/foo/b/here", + "{#list}": "#red,green,blue", + "{#list*}": "#red,green,blue", + "{#keys}": "#semi,;,dot,.,comma,,", + "{#keys*}": "#semi=;,dot=.,comma=,", + "X{.var:3}": "X.val", + "X{.list}": "X.red,green,blue", + "X{.list*}": "X.red.green.blue", + "X{.keys}": "X.semi,%3B,dot,.,comma,%2C", + "X{.keys*}": "X.semi=%3B.dot=..comma=%2C", + "{/var:1,var}": "/v/value", + "{/list}": "/red,green,blue", + "{/list*}": "/red/green/blue", + "{/list*,path:4}": "/red/green/blue/%2Ffoo", + "{/keys}": "/semi,%3B,dot,.,comma,%2C", + "{/keys*}": "/semi=%3B/dot=./comma=%2C", + "{;hello:5}": ";hello=Hello", + "{;list}": ";list=red,green,blue", + "{;list*}": ";list=red;list=green;list=blue", + "{;keys}": ";keys=semi,%3B,dot,.,comma,%2C", + "{;keys*}": ";semi=%3B;dot=.;comma=%2C", + "{?var:3}": "?var=val", + "{?list}": "?list=red,green,blue", + "{?list*}": "?list=red&list=green&list=blue", + "{?keys}": "?keys=semi,%3B,dot,.,comma,%2C", + "{?keys*}": "?semi=%3B&dot=.&comma=%2C", + "{&var:3}": "&var=val", + "{&list}": "&list=red,green,blue", + "{&list*}": "&list=red&list=green&list=blue", + "{&keys}": "&keys=semi,%3B,dot,.,comma,%2C", + "{&keys*}": "&semi=%3B&dot=.&comma=%2C" + } +} \ No newline at end of file diff --git a/net/src/test/resources/spec-examples-by-section.json b/net/src/test/resources/spec-examples-by-section.json new file mode 100644 index 0000000..cd54d9a --- /dev/null +++ b/net/src/test/resources/spec-examples-by-section.json @@ -0,0 +1,437 @@ +{ + "3.2.1 Variable Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{count}", "one,two,three"], + ["{count*}", "one,two,three"], + ["{/count}", "/one,two,three"], + ["{/count*}", "/one/two/three"], + ["{;count}", ";count=one,two,three"], + ["{;count*}", ";count=one;count=two;count=three"], + ["{?count}", "?count=one,two,three"], + ["{?count*}", "?count=one&count=two&count=three"], + ["{&count*}", "&count=one&count=two&count=three"] + ] + }, + "3.2.2 Simple String Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{var}", "value"], + ["{hello}", "Hello%20World%21"], + ["{half}", "50%25"], + ["O{empty}X", "OX"], + ["O{undef}X", "OX"], + ["{x,y}", "1024,768"], + ["{x,hello,y}", "1024,Hello%20World%21,768"], + ["?{x,empty}", "?1024,"], + ["?{x,undef}", "?1024"], + ["?{undef,y}", "?768"], + ["{var:3}", "val"], + ["{var:30}", "value"], + ["{list}", "red,green,blue"], + ["{list*}", "red,green,blue"], + ["{keys}", [ + "comma,%2C,dot,.,semi,%3B", + "comma,%2C,semi,%3B,dot,.", + "dot,.,comma,%2C,semi,%3B", + "dot,.,semi,%3B,comma,%2C", + "semi,%3B,comma,%2C,dot,.", + "semi,%3B,dot,.,comma,%2C" + ]], + ["{keys*}", [ + "comma=%2C,dot=.,semi=%3B", + "comma=%2C,semi=%3B,dot=.", + "dot=.,comma=%2C,semi=%3B", + "dot=.,semi=%3B,comma=%2C", + "semi=%3B,comma=%2C,dot=.", + "semi=%3B,dot=.,comma=%2C" + ]] + ] + }, + "3.2.3 Reserved Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{+var}", "value"], + ["{+hello}", "Hello%20World!"], + ["{+half}", "50%25"], + ["{base}index", "http%3A%2F%2Fexample.com%2Fhome%2Findex"], + ["{+base}index", "http://example.com/home/index"], + ["O{+empty}X", "OX"], + ["O{+undef}X", "OX"], + ["{+path}/here", "/foo/bar/here"], + ["{+path:6}/here", "/foo/b/here"], + ["here?ref={+path}", "here?ref=/foo/bar"], + ["up{+path}{var}/here", "up/foo/barvalue/here"], + ["{+x,hello,y}", "1024,Hello%20World!,768"], + ["{+path,x}/here", "/foo/bar,1024/here"], + ["{+list}", "red,green,blue"], + ["{+list*}", "red,green,blue"], + ["{+keys}", [ + "comma,,,dot,.,semi,;", + "comma,,,semi,;,dot,.", + "dot,.,comma,,,semi,;", + "dot,.,semi,;,comma,,", + "semi,;,comma,,,dot,.", + "semi,;,dot,.,comma,," + ]], + ["{+keys*}", [ + "comma=,,dot=.,semi=;", + "comma=,,semi=;,dot=.", + "dot=.,comma=,,semi=;", + "dot=.,semi=;,comma=,", + "semi=;,comma=,,dot=.", + "semi=;,dot=.,comma=," + ]] + ] + }, + "3.2.4 Fragment Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{#var}", "#value"], + ["{#hello}", "#Hello%20World!"], + ["{#half}", "#50%25"], + ["foo{#empty}", "foo#"], + ["foo{#undef}", "foo"], + ["{#x,hello,y}", "#1024,Hello%20World!,768"], + ["{#path,x}/here", "#/foo/bar,1024/here"], + ["{#path:6}/here", "#/foo/b/here"], + ["{#list}", "#red,green,blue"], + ["{#list*}", "#red,green,blue"], + ["{#keys}", [ + "#comma,,,dot,.,semi,;", + "#comma,,,semi,;,dot,.", + "#dot,.,comma,,,semi,;", + "#dot,.,semi,;,comma,,", + "#semi,;,comma,,,dot,.", + "#semi,;,dot,.,comma,," + ]] + ] + }, + "3.2.5 Label Expansion with Dot-Prefix" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{.who}", ".fred"], + ["{.who,who}", ".fred.fred"], + ["{.half,who}", ".50%25.fred"], + ["www{.dom*}", "www.example.com"], + ["X{.var}", "X.value"], + ["X{.var:3}", "X.val"], + ["X{.empty}", "X."], + ["X{.undef}", "X"], + ["X{.list}", "X.red,green,blue"], + ["X{.list*}", "X.red.green.blue"], + ["{#keys}", [ + "#comma,,,dot,.,semi,;", + "#comma,,,semi,;,dot,.", + "#dot,.,comma,,,semi,;", + "#dot,.,semi,;,comma,,", + "#semi,;,comma,,,dot,.", + "#semi,;,dot,.,comma,," + ]], + ["{#keys*}", [ + "#comma=,,dot=.,semi=;", + "#comma=,,semi=;,dot=.", + "#dot=.,comma=,,semi=;", + "#dot=.,semi=;,comma=,", + "#semi=;,comma=,,dot=.", + "#semi=;,dot=.,comma=," + ]], + ["X{.empty_keys}", "X"], + ["X{.empty_keys*}", "X"] + ] + }, + "3.2.6 Path Segment Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{/who}", "/fred"], + ["{/who,who}", "/fred/fred"], + ["{/half,who}", "/50%25/fred"], + ["{/who,dub}", "/fred/me%2Ftoo"], + ["{/var}", "/value"], + ["{/var,empty}", "/value/"], + ["{/var,undef}", "/value"], + ["{/var,x}/here", "/value/1024/here"], + ["{/var:1,var}", "/v/value"], + ["{/list}", "/red,green,blue"], + ["{/list*}", "/red/green/blue"], + ["{/list*,path:4}", "/red/green/blue/%2Ffoo"], + ["{/keys}", [ + "/comma,%2C,dot,.,semi,%3B", + "/comma,%2C,semi,%3B,dot,.", + "/dot,.,comma,%2C,semi,%3B", + "/dot,.,semi,%3B,comma,%2C", + "/semi,%3B,comma,%2C,dot,.", + "/semi,%3B,dot,.,comma,%2C" + ]], + ["{/keys*}", [ + "/comma=%2C/dot=./semi=%3B", + "/comma=%2C/semi=%3B/dot=.", + "/dot=./comma=%2C/semi=%3B", + "/dot=./semi=%3B/comma=%2C", + "/semi=%3B/comma=%2C/dot=.", + "/semi=%3B/dot=./comma=%2C" + ]] + ] + }, + "3.2.7 Path-Style Parameter Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{;who}", ";who=fred"], + ["{;half}", ";half=50%25"], + ["{;empty}", ";empty"], + ["{;hello:5}", ";hello=Hello"], + ["{;v,empty,who}", ";v=6;empty;who=fred"], + ["{;v,bar,who}", ";v=6;who=fred"], + ["{;x,y}", ";x=1024;y=768"], + ["{;x,y,empty}", ";x=1024;y=768;empty"], + ["{;x,y,undef}", ";x=1024;y=768"], + ["{;list}", ";list=red,green,blue"], + ["{;list*}", ";list=red;list=green;list=blue"], + ["{;keys}", [ + ";keys=comma,%2C,dot,.,semi,%3B", + ";keys=comma,%2C,semi,%3B,dot,.", + ";keys=dot,.,comma,%2C,semi,%3B", + ";keys=dot,.,semi,%3B,comma,%2C", + ";keys=semi,%3B,comma,%2C,dot,.", + ";keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{;keys*}", [ + ";comma=%2C;dot=.;semi=%3B", + ";comma=%2C;semi=%3B;dot=.", + ";dot=.;comma=%2C;semi=%3B", + ";dot=.;semi=%3B;comma=%2C", + ";semi=%3B;comma=%2C;dot=.", + ";semi=%3B;dot=.;comma=%2C" + ]] + ] + }, + "3.2.8 Form-Style Query Expansion" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{?who}", "?who=fred"], + ["{?half}", "?half=50%25"], + ["{?x,y}", "?x=1024&y=768"], + ["{?x,y,empty}", "?x=1024&y=768&empty="], + ["{?x,y,undef}", "?x=1024&y=768"], + ["{?var:3}", "?var=val"], + ["{?list}", "?list=red,green,blue"], + ["{?list*}", "?list=red&list=green&list=blue"], + ["{?keys}", [ + "?keys=comma,%2C,dot,.,semi,%3B", + "?keys=comma,%2C,semi,%3B,dot,.", + "?keys=dot,.,comma,%2C,semi,%3B", + "?keys=dot,.,semi,%3B,comma,%2C", + "?keys=semi,%3B,comma,%2C,dot,.", + "?keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{?keys*}", [ + "?comma=%2C&dot=.&semi=%3B", + "?comma=%2C&semi=%3B&dot=.", + "?dot=.&comma=%2C&semi=%3B", + "?dot=.&semi=%3B&comma=%2C", + "?semi=%3B&comma=%2C&dot=.", + "?semi=%3B&dot=.&comma=%2C" + ]] + ] + }, + "3.2.9 Form-Style Query Continuation" : + { + "variables": { + "count" : ["one", "two", "three"], + "dom" : ["example", "com"], + "dub" : "me/too", + "hello" : "Hello World!", + "half" : "50%", + "var" : "value", + "who" : "fred", + "base" : "http://example.com/home/", + "path" : "/foo/bar", + "list" : ["red", "green", "blue"], + "keys" : { "semi" : ";", "dot" : ".", "comma" : ","}, + "v" : "6", + "x" : "1024", + "y" : "768", + "empty" : "", + "empty_keys" : {}, + "undef" : null + }, + "testcases" : [ + ["{&who}", "&who=fred"], + ["{&half}", "&half=50%25"], + ["?fixed=yes{&x}", "?fixed=yes&x=1024"], + ["{&var:3}", "&var=val"], + ["{&x,y,empty}", "&x=1024&y=768&empty="], + ["{&x,y,undef}", "&x=1024&y=768"], + ["{&list}", "&list=red,green,blue"], + ["{&list*}", "&list=red&list=green&list=blue"], + ["{&keys}", [ + "&keys=comma,%2C,dot,.,semi,%3B", + "&keys=comma,%2C,semi,%3B,dot,.", + "&keys=dot,.,comma,%2C,semi,%3B", + "&keys=dot,.,semi,%3B,comma,%2C", + "&keys=semi,%3B,comma,%2C,dot,.", + "&keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{&keys*}", [ + "&comma=%2C&dot=.&semi=%3B", + "&comma=%2C&semi=%3B&dot=.", + "&dot=.&comma=%2C&semi=%3B", + "&dot=.&semi=%3B&comma=%2C", + "&semi=%3B&comma=%2C&dot=.", + "&semi=%3B&dot=.&comma=%2C" + ]] + ] + } +} diff --git a/net/src/test/resources/spec-examples.json b/net/src/test/resources/spec-examples.json new file mode 100644 index 0000000..07437de --- /dev/null +++ b/net/src/test/resources/spec-examples.json @@ -0,0 +1,218 @@ +{ + "Level 1 Examples" : + { + "level": 1, + "variables": { + "var" : "value", + "hello" : "Hello World!" + }, + "testcases" : [ + ["{var}", "value"], + ["{hello}", "Hello%20World%21"] + ] + }, + "Level 2 Examples" : + { + "level": 2, + "variables": { + "var" : "value", + "hello" : "Hello World!", + "path" : "/foo/bar" + }, + "testcases" : [ + ["{+var}", "value"], + ["{+hello}", "Hello%20World!"], + ["{+path}/here", "/foo/bar/here"], + ["here?ref={+path}", "here?ref=/foo/bar"] + ] + }, + "Level 3 Examples" : + { + "level": 3, + "variables": { + "var" : "value", + "hello" : "Hello World!", + "empty" : "", + "path" : "/foo/bar", + "x" : "1024", + "y" : "768" + }, + "testcases" : [ + ["map?{x,y}", "map?1024,768"], + ["{x,hello,y}", "1024,Hello%20World%21,768"], + ["{+x,hello,y}", "1024,Hello%20World!,768"], + ["{+path,x}/here", "/foo/bar,1024/here"], + ["{#x,hello,y}", "#1024,Hello%20World!,768"], + ["{#path,x}/here", "#/foo/bar,1024/here"], + ["X{.var}", "X.value"], + ["X{.x,y}", "X.1024.768"], + ["{/var}", "/value"], + ["{/var,x}/here", "/value/1024/here"], + ["{;x,y}", ";x=1024;y=768"], + ["{;x,y,empty}", ";x=1024;y=768;empty"], + ["{?x,y}", "?x=1024&y=768"], + ["{?x,y,empty}", "?x=1024&y=768&empty="], + ["?fixed=yes{&x}", "?fixed=yes&x=1024"], + ["{&x,y,empty}", "&x=1024&y=768&empty="] + ] + }, + "Level 4 Examples" : + { + "level": 4, + "variables": { + "var": "value", + "hello": "Hello World!", + "path": "/foo/bar", + "list": ["red", "green", "blue"], + "keys": {"semi": ";", "dot": ".", "comma":","} + }, + "testcases": [ + ["{var:3}", "val"], + ["{var:30}", "value"], + ["{list}", "red,green,blue"], + ["{list*}", "red,green,blue"], + ["{keys}", [ + "comma,%2C,dot,.,semi,%3B", + "comma,%2C,semi,%3B,dot,.", + "dot,.,comma,%2C,semi,%3B", + "dot,.,semi,%3B,comma,%2C", + "semi,%3B,comma,%2C,dot,.", + "semi,%3B,dot,.,comma,%2C" + ]], + ["{keys*}", [ + "comma=%2C,dot=.,semi=%3B", + "comma=%2C,semi=%3B,dot=.", + "dot=.,comma=%2C,semi=%3B", + "dot=.,semi=%3B,comma=%2C", + "semi=%3B,comma=%2C,dot=.", + "semi=%3B,dot=.,comma=%2C" + ]], + ["{+path:6}/here", "/foo/b/here"], + ["{+list}", "red,green,blue"], + ["{+list*}", "red,green,blue"], + ["{+keys}", [ + "comma,,,dot,.,semi,;", + "comma,,,semi,;,dot,.", + "dot,.,comma,,,semi,;", + "dot,.,semi,;,comma,,", + "semi,;,comma,,,dot,.", + "semi,;,dot,.,comma,," + ]], + ["{+keys*}", [ + "comma=,,dot=.,semi=;", + "comma=,,semi=;,dot=.", + "dot=.,comma=,,semi=;", + "dot=.,semi=;,comma=,", + "semi=;,comma=,,dot=.", + "semi=;,dot=.,comma=," + ]], + ["{#path:6}/here", "#/foo/b/here"], + ["{#list}", "#red,green,blue"], + ["{#list*}", "#red,green,blue"], + ["{#keys}", [ + "#comma,,,dot,.,semi,;", + "#comma,,,semi,;,dot,.", + "#dot,.,comma,,,semi,;", + "#dot,.,semi,;,comma,,", + "#semi,;,comma,,,dot,.", + "#semi,;,dot,.,comma,," + ]], + ["{#keys*}", [ + "#comma=,,dot=.,semi=;", + "#comma=,,semi=;,dot=.", + "#dot=.,comma=,,semi=;", + "#dot=.,semi=;,comma=,", + "#semi=;,comma=,,dot=.", + "#semi=;,dot=.,comma=," + ]], + ["X{.var:3}", "X.val"], + ["X{.list}", "X.red,green,blue"], + ["X{.list*}", "X.red.green.blue"], + ["X{.keys}", [ + "X.comma,%2C,dot,.,semi,%3B", + "X.comma,%2C,semi,%3B,dot,.", + "X.dot,.,comma,%2C,semi,%3B", + "X.dot,.,semi,%3B,comma,%2C", + "X.semi,%3B,comma,%2C,dot,.", + "X.semi,%3B,dot,.,comma,%2C" + ]], + ["{/var:1,var}", "/v/value"], + ["{/list}", "/red,green,blue"], + ["{/list*}", "/red/green/blue"], + ["{/list*,path:4}", "/red/green/blue/%2Ffoo"], + ["{/keys}", [ + "/comma,%2C,dot,.,semi,%3B", + "/comma,%2C,semi,%3B,dot,.", + "/dot,.,comma,%2C,semi,%3B", + "/dot,.,semi,%3B,comma,%2C", + "/semi,%3B,comma,%2C,dot,.", + "/semi,%3B,dot,.,comma,%2C" + ]], + ["{/keys*}", [ + "/comma=%2C/dot=./semi=%3B", + "/comma=%2C/semi=%3B/dot=.", + "/dot=./comma=%2C/semi=%3B", + "/dot=./semi=%3B/comma=%2C", + "/semi=%3B/comma=%2C/dot=.", + "/semi=%3B/dot=./comma=%2C" + ]], + ["{;hello:5}", ";hello=Hello"], + ["{;list}", ";list=red,green,blue"], + ["{;list*}", ";list=red;list=green;list=blue"], + ["{;keys}", [ + ";keys=comma,%2C,dot,.,semi,%3B", + ";keys=comma,%2C,semi,%3B,dot,.", + ";keys=dot,.,comma,%2C,semi,%3B", + ";keys=dot,.,semi,%3B,comma,%2C", + ";keys=semi,%3B,comma,%2C,dot,.", + ";keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{;keys*}", [ + ";comma=%2C;dot=.;semi=%3B", + ";comma=%2C;semi=%3B;dot=.", + ";dot=.;comma=%2C;semi=%3B", + ";dot=.;semi=%3B;comma=%2C", + ";semi=%3B;comma=%2C;dot=.", + ";semi=%3B;dot=.;comma=%2C" + ]], + ["{?var:3}", "?var=val"], + ["{?list}", "?list=red,green,blue"], + ["{?list*}", "?list=red&list=green&list=blue"], + ["{?keys}", [ + "?keys=comma,%2C,dot,.,semi,%3B", + "?keys=comma,%2C,semi,%3B,dot,.", + "?keys=dot,.,comma,%2C,semi,%3B", + "?keys=dot,.,semi,%3B,comma,%2C", + "?keys=semi,%3B,comma,%2C,dot,.", + "?keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{?keys*}", [ + "?comma=%2C&dot=.&semi=%3B", + "?comma=%2C&semi=%3B&dot=.", + "?dot=.&comma=%2C&semi=%3B", + "?dot=.&semi=%3B&comma=%2C", + "?semi=%3B&comma=%2C&dot=.", + "?semi=%3B&dot=.&comma=%2C" + ]], + ["{&var:3}", "&var=val"], + ["{&list}", "&list=red,green,blue"], + ["{&list*}", "&list=red&list=green&list=blue"], + ["{&keys}", [ + "&keys=comma,%2C,dot,.,semi,%3B", + "&keys=comma,%2C,semi,%3B,dot,.", + "&keys=dot,.,comma,%2C,semi,%3B", + "&keys=dot,.,semi,%3B,comma,%2C", + "&keys=semi,%3B,comma,%2C,dot,.", + "&keys=semi,%3B,dot,.,comma,%2C" + ]], + ["{&keys*}", [ + "&comma=%2C&dot=.&semi=%3B", + "&comma=%2C&semi=%3B&dot=.", + "&dot=.&comma=%2C&semi=%3B", + "&dot=.&semi=%3B&comma=%2C", + "&semi=%3B&comma=%2C&dot=.", + "&semi=%3B&dot=.&comma=%2C" + ]] + ] + } +} diff --git a/net/src/test/resources/strings.json b/net/src/test/resources/strings.json new file mode 100644 index 0000000..341a587 --- /dev/null +++ b/net/src/test/resources/strings.json @@ -0,0 +1,43 @@ +{ + "vars": { + "var": "value", + "hello": "Hello World!", + "empty": "", + "poo": "\ud83d\udca9 is a pile of poo" + }, + "tests": { + "{poo:4}": "%F0%9F%92%A9%20is", + "{var}": "value", + "{+var}": "value", + "{.var}": ".value", + "{/var}": "/value", + "{;var}": ";var=value", + "{?var}": "?var=value", + "{&var}": "&var=value", + "{#var}": "#value", + "{hello}": "Hello%20World%21", + "{+hello}": "Hello%20World!", + "{.hello}": ".Hello%20World%21", + "{/hello}": "/Hello%20World%21", + "{;hello}": ";hello=Hello%20World%21", + "{?hello}": "?hello=Hello%20World%21", + "{&hello}": "&hello=Hello%20World%21", + "{#hello}": "#Hello%20World!", + "{empty}": "", + "{+empty}": "", + "{.empty}": ".", + "{/empty}": "/", + "{;empty}": ";empty", + "{?empty}": "?empty=", + "{&empty}": "&empty=", + "{#empty}": "#", + "{undef}": "", + "{+undef}": "", + "{.undef}": "", + "{/undef}": "", + "{;undef}": "", + "{?undef}": "", + "{&undef}": "", + "{#undef}": "" + } +} \ No newline at end of file diff --git a/net/src/test/resources/urltestdata.json b/net/src/test/resources/urltestdata.json new file mode 100644 index 0000000..56bb67f --- /dev/null +++ b/net/src/test/resources/urltestdata.json @@ -0,0 +1,6488 @@ +[ + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/segments.js", + { + "input": "http://example\t.\norg", + "base": "http://example.org/foo/bar", + "href": "http://example.org/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "failure": "true" + }, + { + "input": "http://user:pass@foo:21/bar;par?b#c", + "base": "http://example.org/foo/bar", + "href": "http://user:pass@foo:21/bar;par?b#c", + "origin": "http://foo:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "foo:21", + "hostname": "foo", + "port": "21", + "pathname": "/bar;par", + "search": "?b", + "hash": "#c" + }, + { + "input": "https://test:@test", + "base": "about:blank", + "href": "https://test@test/", + "origin": "https://test", + "protocol": "https:", + "username": "test", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "https://:@test", + "base": "about:blank", + "href": "https://test/", + "origin": "https://test", + "protocol": "https:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "non-special://test:@test/x", + "base": "about:blank", + "href": "non-special://test@test/x", + "origin": "null", + "protocol": "non-special:", + "username": "test", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/x", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "non-special://:@test/x", + "base": "about:blank", + "href": "non-special://test/x", + "origin": "null", + "protocol": "non-special:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/x", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:foo.com", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/foo/foo.com", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "\t :foo.com \n", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com", + "search": "", + "hash": "", + "failure": "true" + }, + { + "input": " foo.com ", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/foo.com", + "search": "", + "hash": "" + }, + { + "input": "a:\t foo.com", + "base": "http://example.org/foo/bar", + "href": "a: foo.com", + "origin": "null", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": " foo.com", + "search": "", + "hash": "", + "failure": "true" + }, + { + "input": "http://f:21/ b ? d # e ", + "base": "http://example.org/foo/bar", + "href": "http://f:21/%20b%20?%20d%20# e", + "origin": "http://f:21", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:21", + "hostname": "f", + "port": "21", + "pathname": "/%20b%20", + "search": "?%20d%20", + "hash": "# e" + }, + { + "input": "lolscheme:x x#x x", + "base": "about:blank", + "href": "lolscheme:x x#x x", + "protocol": "lolscheme:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "x x", + "search": "", + "hash": "#x x" + }, + { + "input": "http://f:/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:0/c", + "base": "http://example.org/foo/bar", + "href": "http://f:0/c", + "origin": "http://f:0", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:0", + "hostname": "f", + "port": "0", + "pathname": "/c", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://f:00000000000000/c", + "base": "http://example.org/foo/bar", + "href": "http://f:0/c", + "origin": "http://f:0", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:0", + "hostname": "f", + "port": "0", + "pathname": "/c", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://f:00000000000000000000080/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:b/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f: /c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f:\n/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://f:fifty-two/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f:999999/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "non-special://f:999999/c", + "base": "http://example.org/foo/bar" + }, + { + "input": "http://f: 21 / b ? d # e ", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": " \t", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "", + "failure": true + }, + { + "input": ":foo.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com/", + "search": "", + "hash": "" + }, + { + "input": ":foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com/", + "search": "", + "hash": "" + }, + { + "input": ":", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:", + "search": "", + "hash": "" + }, + { + "input": ":a", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:a", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:a", + "search": "", + "hash": "" + }, + { + "input": ":/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:/", + "search": "", + "hash": "" + }, + { + "input": ":\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:/", + "search": "", + "hash": "" + }, + { + "input": ":#", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:#", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:", + "search": "", + "hash": "" + }, + { + "input": "#", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "#/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#/" + }, + { + "input": "#\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#\\", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#\\" + }, + { + "input": "#;?", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#;?", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#;?" + }, + { + "input": "?", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar?", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": ":23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:23", + "search": "", + "hash": "" + }, + { + "input": "/:23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/:23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/:23", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "::", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/::", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/::", + "search": "", + "hash": "" + }, + { + "input": "::23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/::23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/::23", + "search": "", + "hash": "" + }, + { + "input": "foo://", + "base": "http://example.org/foo/bar", + "href": "foo://", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "http://a:b@c:29/d", + "base": "http://example.org/foo/bar", + "href": "http://a:b@c:29/d", + "origin": "http://c:29", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "c:29", + "hostname": "c", + "port": "29", + "pathname": "/d", + "search": "", + "hash": "" + }, + { + "input": "http::@c:29", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:@c:29", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:@c:29", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://&a:foo(b]c@d:2/", + "base": "http://example.org/foo/bar", + "href": "http://&a:foo(b%5Dc@d:2/", + "origin": "http://d:2", + "protocol": "http:", + "username": "&a", + "password": "foo(b%5Dc", + "host": "d:2", + "hostname": "d", + "port": "2", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://::@c@d:2", + "base": "http://example.org/foo/bar", + "href": "http://:%3A%40c@d:2/", + "origin": "http://d:2", + "protocol": "http:", + "username": "", + "password": "%3A%40c", + "host": "d:2", + "hostname": "d", + "port": "2", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo.com:b@d/", + "base": "http://example.org/foo/bar", + "href": "http://foo.com:b@d/", + "origin": "http://d", + "protocol": "http:", + "username": "foo.com", + "password": "b", + "host": "d", + "hostname": "d", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo.com/\\@", + "base": "http://example.org/foo/bar", + "href": "http://foo.com//@", + "origin": "http://foo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.com", + "hostname": "foo.com", + "port": "", + "pathname": "//@", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://foo.com/", + "origin": "http://foo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.com", + "hostname": "foo.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\a\\b:c\\d@foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://a/b:c/d@foo.com/", + "origin": "http://a", + "protocol": "http:", + "username": "", + "password": "", + "host": "a", + "hostname": "a", + "port": "", + "pathname": "/b:c/d@foo.com/", + "search": "", + "hash": "" + }, + { + "input": "foo:/", + "base": "http://example.org/foo/bar", + "href": "foo:/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "foo:/bar.com/", + "base": "http://example.org/foo/bar", + "href": "foo:/bar.com/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/bar.com/", + "search": "", + "hash": "" + }, + { + "input": "foo://///////", + "base": "http://example.org/foo/bar", + "href": "foo://///////", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "///////", + "search": "", + "hash": "" + }, + { + "input": "foo://///////bar.com/", + "base": "http://example.org/foo/bar", + "href": "foo://///////bar.com/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "///////bar.com/", + "search": "", + "hash": "" + }, + { + "input": "foo:////://///", + "base": "http://example.org/foo/bar", + "href": "foo:////://///", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "//://///", + "search": "", + "hash": "" + }, + { + "input": "c:/foo", + "base": "http://example.org/foo/bar", + "href": "c:/foo", + "origin": "null", + "protocol": "c:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "//foo/bar", + "base": "http://example.org/foo/bar", + "href": "http://foo/bar", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/bar", + "search": "", + "hash": "" + }, + { + "input": "http://foo/path;a??e#f#g", + "base": "http://example.org/foo/bar", + "href": "http://foo/path;a??e#f#g", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/path;a", + "search": "??e", + "hash": "#f#g" + }, + { + "input": "http://foo/abcd?efgh?ijkl", + "base": "http://example.org/foo/bar", + "href": "http://foo/abcd?efgh?ijkl", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/abcd", + "search": "?efgh?ijkl", + "hash": "" + }, + { + "input": "http://foo/abcd#foo?bar", + "base": "http://example.org/foo/bar", + "href": "http://foo/abcd#foo?bar", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/abcd", + "search": "", + "hash": "#foo?bar" + }, + { + "input": "[61:24:74]:98", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/[61:24:74]:98", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/[61:24:74]:98", + "search": "", + "hash": "", + "failure": true + + }, + { + "input": "http:[61:27]/:foo", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/[61:27]/:foo", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/[61:27]/:foo", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://[1::2]:3:4", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1]", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1]:80", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://[2001::1]", + "base": "http://example.org/foo/bar", + "href": "http://[2001::1]/", + "origin": "http://[2001::1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[2001::1]", + "hostname": "2001:0:0:0:0:0:0:1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[::127.0.0.1]", + "base": "http://example.org/foo/bar", + "href": "http://[::7f00:1]/", + "origin": "http://[::7f00:1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[::7f00:1]", + "hostname": "0:0:0:0:0:0:7f00:1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[0:0:0:0:0:0:13.1.68.3]", + "base": "http://example.org/foo/bar", + "href": "http://[::d01:4403]/", + "origin": "http://[::d01:4403]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[::d01:4403]", + "hostname": "0:0:0:0:0:0:d01:4403", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[2001::1]:80", + "base": "http://example.org/foo/bar", + "href": "http://[2001::1]/", + "origin": "http://[2001::1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[2001::1]", + "hostname": "2001:0:0:0:0:0:0:1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/example.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/example.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ftp:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "https:/example.com/", + "base": "http://example.org/foo/bar", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "madeupscheme:/example.com/", + "base": "http://example.org/foo/bar", + "href": "madeupscheme:/example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file:/example.com/", + "base": "http://example.org/foo/bar", + "href": "file:///example.com/", + "protocol": "file:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file://example:1/", + "base": "about:blank" + }, + { + "input": "file://example:test/", + "base": "about:blank" + }, + { + "input": "file://example%/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://[example]/", + "base": "about:blank" + }, + { + "input": "ftps:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ftps:/example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:/example.com/", + "base": "http://example.org/foo/bar", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ws:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "wss:/example.com/", + "base": "http://example.org/foo/bar", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "data:/example.com/", + "base": "http://example.org/foo/bar", + "href": "data:/example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:/example.com/", + "base": "http://example.org/foo/bar", + "href": "javascript:/example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "mailto:/example.com/", + "base": "http://example.org/foo/bar", + "href": "mailto:/example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:example.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/example.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ftp:example.com/", + "base": "http://example.org/foo/bar", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "https:example.com/", + "base": "http://example.org/foo/bar", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "madeupscheme:example.com/", + "base": "http://example.org/foo/bar", + "href": "madeupscheme:example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ftps:example.com/", + "base": "http://example.org/foo/bar", + "href": "ftps:example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "gopher:example.com/", + "base": "http://example.org/foo/bar", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ws:example.com/", + "base": "http://example.org/foo/bar", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "wss:example.com/", + "base": "http://example.org/foo/bar", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "data:example.com/", + "base": "http://example.org/foo/bar", + "href": "data:example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:example.com/", + "base": "http://example.org/foo/bar", + "href": "javascript:example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "mailto:example.com/", + "base": "http://example.org/foo/bar", + "href": "mailto:example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "/a/b/c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/b/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/b/c", + "search": "", + "hash": "" + }, + { + "input": "/a/ /c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/%20/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/%20/c", + "search": "", + "hash": "" + }, + { + "input": "/a%2fc", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a%2fc", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a%2fc", + "search": "", + "hash": "" + }, + { + "input": "/a/%2f/c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/%2f/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/%2f/c", + "search": "", + "hash": "" + }, + { + "input": "#β", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#%CE%B2", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#%CE%B2" + }, + { + "input": "data:text/html,test#test", + "base": "http://example.org/foo/bar", + "href": "data:text/html,test#test", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "text/html,test", + "search": "", + "hash": "#test" + }, + { + "input": "tel:1234567890", + "base": "http://example.org/foo/bar", + "href": "tel:1234567890", + "origin": "null", + "protocol": "tel:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "1234567890", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/file.html", + { + "input": "file:c:\\foo\\bar.html", + "base": "file:///tmp/mock/path", + "href": "file:///c:/foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/c:/foo/bar.html", + "search": "", + "hash": "" + }, + { + "input": " File:c|////foo\\bar.html", + "base": "file:///tmp/mock/path", + "href": "file:///c:////foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:////foo/bar.html", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "C|/foo/bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/C|\\foo\\bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "//C|/foo/bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "//server/file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "\\\\server\\file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "/\\server/file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "file:///foo/bar.txt", + "base": "file:///tmp/mock/path", + "href": "file:///foo/bar.txt", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/foo/bar.txt", + "search": "", + "hash": "" + }, + { + "input": "file:///home/me", + "base": "file:///tmp/mock/path", + "href": "file:///home/me", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/home/me", + "search": "", + "hash": "" + }, + { + "input": "//", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "///", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "///test", + "base": "file:///tmp/mock/path", + "href": "file:///test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "file://test", + "base": "file:///tmp/mock/path", + "href": "file://test/", + "protocol": "file:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost/", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost/test", + "base": "file:///tmp/mock/path", + "href": "file:///test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "test", + "base": "file:///tmp/mock/path", + "href": "file:///tmp/mock/test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/tmp/mock/test", + "search": "", + "hash": "" + }, + { + "input": "file:test", + "base": "file:///tmp/mock/path", + "href": "file:///tmp/mock/test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/tmp/mock/test", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/path.js", + { + "input": "http://example.com/././foo", + "base": "about:blank", + "href": "http://example.com/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/./.foo", + "base": "about:blank", + "href": "http://example.com/.foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/.foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/.", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/./", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/..", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/..bar", + "base": "about:blank", + "href": "http://example.com/foo/..bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/..bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../ton", + "base": "about:blank", + "href": "http://example.com/foo/ton", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/ton", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../ton/../../a", + "base": "about:blank", + "href": "http://example.com/a", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/a", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/../../..", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/../../../ton", + "base": "about:blank", + "href": "http://example.com/ton", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/ton", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e%2", + "base": "about:blank", + "href": "http://example.com/foo/%2e%2", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/%2e%2", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar", + "base": "about:blank", + "href": "http://example.com/%2e.bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%2e.bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com////../..", + "base": "about:blank", + "href": "http://example.com//", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "//", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar//../..", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar//..", + "base": "about:blank", + "href": "http://example.com/foo/bar/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/bar/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo", + "base": "about:blank", + "href": "http://example.com/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%20foo", + "base": "about:blank", + "href": "http://example.com/%20foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%20foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%", + "base": "about:blank", + "href": "http://example.com/foo%", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com/foo%2", + "base": "about:blank", + "href": "http://example.com/foo%2", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com/foo%2zbar", + "base": "about:blank", + "href": "http://example.com/foo%2zbar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2zbar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%2©zbar", + "base": "about:blank", + "href": "http://example.com/foo%2%C3%82%C2%A9zbar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2%C3%82%C2%A9zbar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%41%7a", + "base": "about:blank", + "href": "http://example.com/foo%41%7a", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%41%7a", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo\t\u0091%91", + "base": "about:blank", + "href": "http://example.com/foo%C2%91%91", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%C2%91%91", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com/foo%00%51", + "base": "about:blank", + "href": "http://example.com/foo%00%51", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%00%51", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/(%28:%3A%29)", + "base": "about:blank", + "href": "http://example.com/(%28:%3A%29)", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/(%28:%3A%29)", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%3A%3a%3C%3c", + "base": "about:blank", + "href": "http://example.com/%3A%3a%3C%3c", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%3A%3a%3C%3c", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo\tbar", + "base": "about:blank", + "href": "http://example.com/foobar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foobar", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://example.com\\\\foo\\\\bar", + "base": "about:blank", + "href": "http://example.com//foo//bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "//foo//bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd", + "base": "about:blank", + "href": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%7Ffp3%3Eju%3Dduvgw%3Dd", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/@asdf%40", + "base": "about:blank", + "href": "http://example.com/@asdf%40", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/@asdf%40", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/你好你好", + "base": "about:blank", + "href": "http://example.com/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/‥/foo", + "base": "about:blank", + "href": "http://example.com/%E2%80%A5/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E2%80%A5/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com//foo", + "base": "about:blank", + "href": "http://example.com/%EF%BB%BF/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%EF%BB%BF/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/‮/foo/‭/bar", + "base": "about:blank", + "href": "http://example.com/%E2%80%AE/foo/%E2%80%AD/bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E2%80%AE/foo/%E2%80%AD/bar", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/relative.js", + { + "input": "http://www.google.com/foo?bar=baz#", + "base": "about:blank", + "href": "http://www.google.com/foo?bar=baz#", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "?bar=baz", + "hash": "" + }, + { + "input": "http://www.google.com/foo?bar=baz# »", + "base": "about:blank", + "href": "http://www.google.com/foo?bar=baz# %C2%BB", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "?bar=baz", + "hash": "# %C2%BB" + }, + { + "input": "data:test# »", + "base": "about:blank", + "href": "data:test# %C2%BB", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "test", + "search": "", + "hash": "# %C2%BB" + }, + { + "input": "http://www.google.com", + "base": "about:blank", + "href": "http://www.google.com/", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.0x00A80001", + "base": "about:blank", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.0x00a80001", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://www/foo%2Ehtml", + "base": "about:blank", + "href": "http://www/foo%2Ehtml", + "origin": "http://www", + "protocol": "http:", + "username": "", + "password": "", + "host": "www", + "hostname": "www", + "port": "", + "pathname": "/foo%2Ehtml", + "search": "", + "hash": "" + }, + { + "input": "http://www/foo/%2E/html", + "base": "about:blank", + "href": "http://www/foo/html", + "origin": "http://www", + "protocol": "http:", + "username": "", + "password": "", + "host": "www", + "hostname": "www", + "port": "", + "pathname": "/foo/html", + "search": "", + "hash": "" + }, + { + "input": "http://user:pass@/", + "base": "about:blank" + }, + { + "input": "http://%25DOMAIN:foobar@foodomain.com/", + "base": "about:blank", + "href": "http://%25DOMAIN:foobar@foodomain.com/", + "origin": "http://foodomain.com", + "protocol": "http:", + "username": "%25DOMAIN", + "password": "foobar", + "host": "foodomain.com", + "hostname": "foodomain.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\www.google.com\\foo", + "base": "about:blank", + "href": "http://www.google.com/foo", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://foo:80/", + "base": "about:blank", + "href": "http://foo/", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo:81/", + "base": "about:blank", + "href": "http://foo:81/", + "origin": "http://foo:81", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "httpa://foo:80/", + "base": "about:blank", + "href": "httpa://foo:80/", + "origin": "null", + "protocol": "httpa:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo:-80/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://foo:443/", + "base": "about:blank", + "href": "https://foo/", + "origin": "https://foo", + "protocol": "https:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://foo:80/", + "base": "about:blank", + "href": "https://foo:80/", + "origin": "https://foo:80", + "protocol": "https:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp://foo:21/", + "base": "about:blank", + "href": "ftp://foo/", + "origin": "ftp://foo", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp://foo:80/", + "base": "about:blank", + "href": "ftp://foo:80/", + "origin": "ftp://foo:80", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "gopher://foo:70/", + "base": "about:blank", + "href": "gopher://foo/", + "origin": "gopher://foo", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "gopher://foo:443/", + "base": "about:blank", + "href": "gopher://foo:443/", + "origin": "gopher://foo:443", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "foo:443", + "hostname": "foo", + "port": "443", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:80/", + "base": "about:blank", + "href": "ws://foo/", + "origin": "ws://foo", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:81/", + "base": "about:blank", + "href": "ws://foo:81/", + "origin": "ws://foo:81", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:443/", + "base": "about:blank", + "href": "ws://foo:443/", + "origin": "ws://foo:443", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:443", + "hostname": "foo", + "port": "443", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:815/", + "base": "about:blank", + "href": "ws://foo:815/", + "origin": "ws://foo:815", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:815", + "hostname": "foo", + "port": "815", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:80/", + "base": "about:blank", + "href": "wss://foo:80/", + "origin": "wss://foo:80", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:81/", + "base": "about:blank", + "href": "wss://foo:81/", + "origin": "wss://foo:81", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:443/", + "base": "about:blank", + "href": "wss://foo/", + "origin": "wss://foo", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:815/", + "base": "about:blank", + "href": "wss://foo:815/", + "origin": "wss://foo:815", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:815", + "hostname": "foo", + "port": "815", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/example.com/", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ftp:/example.com/", + "base": "about:blank", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "https:/example.com/", + "base": "about:blank", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "madeupscheme:/example.com/", + "base": "about:blank", + "href": "madeupscheme:/example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file:/example.com/", + "base": "about:blank", + "href": "file:///example.com/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:/example.com/", + "base": "about:blank", + "href": "ftps:/example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:/example.com/", + "base": "about:blank", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ws:/example.com/", + "base": "about:blank", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "wss:/example.com/", + "base": "about:blank", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "data:/example.com/", + "base": "about:blank", + "href": "data:/example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "javascript:/example.com/", + "base": "about:blank", + "href": "javascript:/example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/example.com/", + "base": "about:blank", + "href": "mailto:/example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "http:example.com/", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ftp:example.com/", + "base": "about:blank", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "https:example.com/", + "base": "about:blank", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "madeupscheme:example.com/", + "base": "about:blank", + "href": "madeupscheme:example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:example.com/", + "base": "about:blank", + "href": "ftps:example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:example.com/", + "base": "about:blank", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "ws:example.com/", + "base": "about:blank", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "wss:example.com/", + "base": "about:blank", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "data:example.com/", + "base": "about:blank", + "href": "data:example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:example.com/", + "base": "about:blank", + "href": "javascript:example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "mailto:example.com/", + "base": "about:blank", + "href": "mailto:example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/segments-userinfo-vs-host.html", + { + "input": "http:@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:/@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:/a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://@pple.com", + "base": "about:blank", + "href": "http://pple.com/", + "origin": "http://pple.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "pple.com", + "hostname": "pple.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http::b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:/:b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://:b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/:@/www.example.com", + "base": "about:blank" + }, + { + "input": "http://user@/www.example.com", + "base": "about:blank" + }, + { + "input": "http:@/www.example.com", + "base": "about:blank" + }, + { + "input": "http:/@/www.example.com", + "base": "about:blank" + }, + { + "input": "http://@/www.example.com", + "base": "about:blank" + }, + { + "input": "https:@/www.example.com", + "base": "about:blank" + }, + { + "input": "http:a:b@/www.example.com", + "base": "about:blank" + }, + { + "input": "http:/a:b@/www.example.com", + "base": "about:blank" + }, + { + "input": "http://a:b@/www.example.com", + "base": "about:blank" + }, + { + "input": "http::@/www.example.com", + "base": "about:blank" + }, + { + "input": "http:a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:/a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http://a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://www.@pple.com", + "base": "about:blank", + "href": "http://www.@pple.com/", + "origin": "http://pple.com", + "protocol": "http:", + "username": "www.", + "password": "", + "host": "pple.com", + "hostname": "pple.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:@:www.example.com", + "base": "about:blank" + }, + { + "input": "http:/@:www.example.com", + "base": "about:blank" + }, + { + "input": "http://@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://:@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# Others", + { + "input": "/", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": ".", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "./test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../aaa/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/aaa/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/aaa/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../../test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "中/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/%E4%B8%AD/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/%E4%B8%AD/test.txt", + "search": "", + "hash": "" + }, + { + "input": "http://www.example2.com", + "base": "http://www.example.com/test", + "href": "http://www.example2.com/", + "origin": "http://www.example2.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example2.com", + "hostname": "www.example2.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "//www.example2.com", + "base": "http://www.example.com/test", + "href": "http://www.example2.com/", + "origin": "http://www.example2.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:...", + "base": "http://www.example.com/test", + "href": "file:///...", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/...", + "search": "", + "hash": "" + }, + { + "input": "file:..", + "base": "http://www.example.com/test", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:a", + "base": "http://www.example.com/test", + "href": "file:///a", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/a", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/host.html", + "Basic canonicalization, uppercase should be converted to lowercase", + { + "input": "http://ExAmPlE.CoM", + "base": "http://other.com/", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://example example.com", + "base": "http://other.com/" + }, + { + "input": "http://Goo%20 goo%7C|.com", + "base": "http://other.com/" + }, + { + "input": "http://[]", + "base": "http://other.com/" + }, + { + "input": "http://[:]", + "base": "http://other.com/" + }, + "U+3000 is mapped to U+0020 (space) which is disallowed", + { + "input": "http://GOO\u00a0\u3000goo.com", + "base": "http://other.com/" + }, + "Other types of space (no-break, zero-width, zero-width-no-break) are name-prepped away to nothing. U+200B, U+2060, and U+FEFF, are ignored", + { + "input": "http://GOO\u200b\u2060\ufeffgoo.com", + "base": "http://other.com/", + "href": "http://googoo.com/", + "origin": "http://googoo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "googoo.com", + "hostname": "googoo.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + "Leading and trailing C0 control or space", + { + "input": "\u0000\u001b\u0004\u0012 http://example.com/\u001f \u000d ", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "failure": true + }, + "Ideographic full stop (full-width period for Chinese, etc.) should be treated as a dot. U+3002 is mapped to U+002E (dot)", + { + "input": "http://www.foo。bar.com", + "base": "http://other.com/", + "href": "http://www.foo.bar.com/", + "origin": "http://www.foo.bar.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.foo.bar.com", + "hostname": "www.foo.bar.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + "Invalid unicode characters should fail... U+FDD0 is disallowed; %ef%b7%90 is U+FDD0", + { + "input": "http://\ufdd0zyx.com", + "base": "http://other.com/", + "failure": true + }, + "This is the same as previous but escaped", + { + "input": "http://%ef%b7%90zyx.com", + "base": "http://other.com/", + "failure": true + }, + "U+FFFD", + { + "input": "https://\ufffd", + "base": "about:blank", + "failure": true + }, + { + "input": "https://%EF%BF%BD", + "base": "about:blank", + "failure": true + }, + { + "input": "https://x/\ufffd?\ufffd#\ufffd", + "base": "about:blank", + "href": "https://x/%EF%BF%BD?%EF%BF%BD#%EF%BF%BD", + "origin": "https://x", + "protocol": "https:", + "username": "", + "password": "", + "host": "x", + "hostname": "x", + "port": "", + "pathname": "/%EF%BF%BD", + "search": "?%EF%BF%BD", + "hash": "#%EF%BF%BD" + }, + "Test name prepping, fullwidth input should be converted to ASCII and NOT IDN-ized. This is 'Go' in fullwidth UTF-8/UTF-16.", + { + "input": "http://Go.com", + "base": "http://other.com/", + "href": "http://go.com/", + "origin": "http://go.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "go.com", + "hostname": "go.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + "URL spec forbids the following. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24257", + { + "input": "http://%41.com", + "base": "http://other.com/" + }, + { + "input": "http://%ef%bc%85%ef%bc%94%ef%bc%91.com", + "base": "http://other.com/" + }, + "...%00 in fullwidth should fail (also as escaped UTF-8 input)", + { + "input": "http://%00.com", + "base": "http://other.com/" + }, + { + "input": "http://%ef%bc%85%ef%bc%90%ef%bc%90.com", + "base": "http://other.com/" + }, + "Basic IDN support, UTF-8 and UTF-16 input should be converted to IDN", + { + "input": "http://你好你好", + "base": "http://other.com/", + "href": "http://xn--6qqa088eba/", + "origin": "http://xn--6qqa088eba", + "protocol": "http:", + "username": "", + "password": "", + "host": "xn--6qqa088eba", + "hostname": "xn--6qqa088eba", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "https://faß.ExAmPlE/", + "base": "about:blank", + "href": "https://xn--fa-hia.example/", + "origin": "https://xn--fa-hia.example", + "protocol": "https:", + "username": "", + "password": "", + "host": "xn--fa-hia.example", + "hostname": "fass.example", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://faß.ExAmPlE/", + "base": "about:blank", + "href": "sc://fa%C3%9F.ExAmPlE/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "fa%C3%9F.ExAmPlE", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid escaped characters should fail and the percents should be escaped. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24191", + { + "input": "http://%zz%66%a.com", + "base": "http://other.com/", + "failure": true + }, + "If we get an invalid character that has been escaped.", + { + "input": "http://%25", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://hello%00", + "base": "http://other.com/" + }, + "Escaped numbers should be treated like IP addresses if they are.", + { + "input": "http://%30%78%63%30%2e%30%32%35%30.01", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "0xc0.0250.01", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://%30%78%63%30%2e%30%32%35%30.01%2e", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "0xc0.0250.01.", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.168.0.257", + "base": "http://other.com/" + }, + "Invalid escaping in hosts causes failure", + { + "input": "http://%3g%78%63%30%2e%30%32%35%30%2E.01", + "base": "http://other.com/", + "failure": true + }, + "A space in a host causes failure", + { + "input": "http://192.168.0.1 hello", + "base": "http://other.com/" + }, + { + "input": "https://x x:12", + "base": "about:blank" + }, + "Fullwidth and escaped UTF-8 fullwidth should still be treated as IP", + { + "input": "http://0Xc0.0250.01", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "0xc0.0250.01", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Domains with empty labels", + { + "input": "http://./", + "base": "about:blank", + "href": "http://./", + "origin": "http://.", + "protocol": "http:", + "username": "", + "password": "", + "host": ".", + "hostname": ".", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://../", + "base": "about:blank", + "href": "http://../", + "origin": "http://..", + "protocol": "http:", + "username": "", + "password": "", + "host": "..", + "hostname": "..", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "http://0..0x300/", + "base": "about:blank", + "href": "http://0..0x300/", + "origin": "http://0..0x300", + "protocol": "http:", + "username": "", + "password": "", + "host": "0..0x300", + "hostname": "0..0x300", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "failure": true + }, + "Broken IPv6", + { + "input": "http://[www.google.com]/", + "base": "about:blank" + }, + { + "input": "http://[google.com]", + "base": "http://other.com/" + }, + { + "input": "http://[::1.2.3.4x]", + "base": "http://other.com/" + }, + { + "input": "http://[::1.2.3.]", + "base": "http://other.com/" + }, + { + "input": "http://[::1.2.]", + "base": "http://other.com/" + }, + { + "input": "http://[::1.]", + "base": "http://other.com/" + }, + "Misc Unicode", + { + "input": "http://foo:💩@example.com/bar", + "base": "http://other.com/", + "href": "http://foo:%F0%9F%92%A9@example.com/bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "foo", + "password": "%F0%9F%92%A9", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/bar", + "search": "", + "hash": "" + }, + "# resolving a fragment against any scheme succeeds", + { + "input": "#", + "base": "test:test", + "href": "test:test#", + "origin": "null", + "protocol": "test:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "test", + "search": "", + "hash": "" + }, + { + "input": "#x", + "base": "mailto:x@x.com", + "href": "mailto:x@x.com#x", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "x@x.com", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "data:,", + "href": "data:,#x", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": ",", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "about:blank", + "href": "about:blank#x", + "origin": "null", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "blank", + "search": "", + "hash": "#x" + }, + { + "input": "#", + "base": "test:test?test", + "href": "test:test?test#", + "origin": "null", + "protocol": "test:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "test", + "search": "?test", + "hash": "" + }, + "# multiple @ in authority state", + { + "input": "https://@test@test@example:800/", + "base": "http://doesnotmatter/", + "href": "https://%40test%40test@example:800/", + "origin": "https://example:800", + "protocol": "https:", + "username": "%40test%40test", + "password": "", + "host": "example:800", + "hostname": "example", + "port": "800", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://@@@example", + "base": "http://doesnotmatter/", + "href": "https://%40%40@example/", + "origin": "https://example", + "protocol": "https:", + "username": "%40%40", + "password": "", + "host": "example", + "hostname": "example", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "non-az-09 characters", + { + "input": "http://`{}:`{}@h/`{}?`{}", + "base": "http://doesnotmatter/", + "href": "http://%60%7B%7D:%60%7B%7D@h/%60%7B%7D?`{}", + "origin": "http://h", + "protocol": "http:", + "username": "%60%7B%7D", + "password": "%60%7B%7D", + "host": "h", + "hostname": "h", + "port": "", + "pathname": "/%60%7B%7D", + "search": "?`{}", + "hash": "" + }, + "# Credentials in base", + { + "input": "/some/path", + "base": "http://user@example.org/smth", + "href": "http://user@example.org/some/path", + "origin": "http://example.org", + "protocol": "http:", + "username": "user", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/some/path", + "search": "", + "hash": "" + }, + { + "input": "", + "base": "http://user:pass@example.org:21/smth", + "href": "http://user:pass@example.org:21/smth", + "origin": "http://example.org:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "example.org:21", + "hostname": "example.org", + "port": "21", + "pathname": "/smth", + "search": "", + "hash": "" + }, + { + "input": "/some/path", + "base": "http://user:pass@example.org:21/smth", + "href": "http://user:pass@example.org:21/some/path", + "origin": "http://example.org:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "example.org:21", + "hostname": "example.org", + "port": "21", + "pathname": "/some/path", + "search": "", + "hash": "" + }, + "# a set of tests designed by zcorpan for relative URLs with unknown schemes", + { + "input": "i", + "base": "sc:sd" + }, + { + "input": "i", + "base": "sc:sd/sd" + }, + { + "input": "i", + "base": "sc:/pa/pa", + "href": "sc:/pa/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pa/i", + "search": "", + "hash": "" + }, + { + "input": "i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "i", + "base": "sc:///pa/pa", + "href": "sc:///pa/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/pa/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc:sd" + }, + { + "input": "../i", + "base": "sc:sd/sd" + }, + { + "input": "../i", + "base": "sc:/pa/pa", + "href": "sc:/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc:///pa/pa", + "href": "sc:///i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc:sd" + }, + { + "input": "/i", + "base": "sc:sd/sd" + }, + { + "input": "/i", + "base": "sc:/pa/pa", + "href": "sc:/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc:///pa/pa", + "href": "sc:///i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "?i", + "base": "sc:sd" + }, + { + "input": "?i", + "base": "sc:sd/sd" + }, + { + "input": "?i", + "base": "sc:/pa/pa", + "href": "sc:/pa/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pa/pa", + "search": "?i", + "hash": "" + }, + { + "input": "?i", + "base": "sc://ho/pa", + "href": "sc://ho/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/pa", + "search": "?i", + "hash": "" + }, + { + "input": "?i", + "base": "sc:///pa/pa", + "href": "sc:///pa/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/pa/pa", + "search": "?i", + "hash": "" + }, + { + "input": "#i", + "base": "sc:sd", + "href": "sc:sd#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "sd", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:sd/sd", + "href": "sc:sd/sd#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "sd/sd", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:/pa/pa", + "href": "sc:/pa/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pa/pa", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc://ho/pa", + "href": "sc://ho/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/pa", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:///pa/pa", + "href": "sc:///pa/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pa/pa", + "search": "", + "hash": "#i" + }, + "# make sure that relative URL logic works on known typically non-relative schemes too", + { + "input": "about:/../", + "base": "about:blank", + "href": "about:/", + "origin": "null", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:/../", + "base": "about:blank", + "href": "data:/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "javascript:/../", + "base": "about:blank", + "href": "javascript:/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/../", + "base": "about:blank", + "href": "mailto:/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# unknown schemes and their hosts", + { + "input": "sc://ñ.test/", + "base": "about:blank", + "href": "sc://%C3%B1.test/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://\u001F!\"$&'()*+,-.;<=>^_`{|}~/", + "base": "about:blank", + "href": "sc://%1F!\"$&'()*+,-.;<=>^_`{|}~/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://\u0000/", + "base": "about:blank" + }, + { + "input": "sc:// /", + "base": "about:blank" + }, + { + "input": "sc://%/", + "base": "about:blank", + "href": "sc://%/", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%", + "hostname": "%", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "sc://@/", + "base": "about:blank" + }, + { + "input": "sc://te@s:t@/", + "base": "about:blank" + }, + { + "input": "sc://:/", + "base": "about:blank" + }, + { + "input": "sc://:12/", + "base": "about:blank" + }, + { + "input": "sc://[/", + "base": "about:blank" + }, + { + "input": "sc://\\/", + "base": "about:blank" + }, + { + "input": "sc://]/", + "base": "about:blank" + }, + { + "input": "x", + "base": "sc://ñ", + "href": "sc://%C3%B1/x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/x", + "search": "", + "hash": "" + }, + "# unknown schemes and backslashes", + { + "input": "sc:\\../", + "base": "about:blank", + "href": "sc:\\../", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "\\../", + "search": "", + "hash": "" + }, + "# unknown scheme with path looking like a password", + { + "input": "sc::a@example.net", + "base": "about:blank", + "href": "sc::a@example.net", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": ":a@example.net", + "search": "", + "hash": "" + }, + "# unknown scheme with bogus percent-encoding", + { + "input": "wow:%NBD", + "base": "about:blank", + "href": "wow:%NBD", + "origin": "null", + "protocol": "wow:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "%NBD", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "wow:%1G", + "base": "about:blank", + "href": "wow:%1G", + "origin": "null", + "protocol": "wow:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "%1G", + "search": "", + "hash": "" + }, + "# Hosts and percent-encoding", + { + "input": "ftp://example.com%80/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftp://example.com%A0/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://example.com%80/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://example.com%A0/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftp://%e2%98%83", + "base": "about:blank", + "href": "ftp://xn--n3h/", + "origin": "ftp://xn--n3h", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "xn--n3h", + "hostname": "xn--n3h", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://%e2%98%83", + "base": "about:blank", + "href": "https://xn--n3h/", + "origin": "https://xn--n3h", + "protocol": "https:", + "username": "", + "password": "", + "host": "xn--n3h", + "hostname": "xn--n3h", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# tests from jsdom/whatwg-url designed for code coverage", + { + "input": "http://127.0.0.1:10100/relative_import.html", + "base": "about:blank", + "href": "http://127.0.0.1:10100/relative_import.html", + "origin": "http://127.0.0.1:10100", + "protocol": "http:", + "username": "", + "password": "", + "host": "127.0.0.1:10100", + "hostname": "localhost", + "port": "10100", + "pathname": "/relative_import.html", + "search": "", + "hash": "" + }, + { + "input": "http://facebook.com/?foo=%7B%22abc%22", + "base": "about:blank", + "href": "http://facebook.com/?foo=%7B%22abc%22", + "origin": "http://facebook.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "facebook.com", + "hostname": "facebook.com", + "port": "", + "pathname": "/", + "search": "?foo=%7B%22abc%22", + "hash": "" + }, + { + "input": "https://localhost:3000/jqueryui@1.2.3", + "base": "about:blank", + "href": "https://localhost:3000/jqueryui@1.2.3", + "origin": "https://localhost:3000", + "protocol": "https:", + "username": "", + "password": "", + "host": "localhost:3000", + "hostname": "localhost", + "port": "3000", + "pathname": "/jqueryui@1.2.3", + "search": "", + "hash": "" + }, + "# tab/LF/CR", + { + "input": "h\tt\nt\rp://h\to\ns\rt:9\t0\n0\r0/p\ta\nt\rh?q\tu\ne\rry#f\tr\na\rg", + "base": "about:blank", + "href": "http://host:9000/path?query#frag", + "origin": "http://host:9000", + "protocol": "http:", + "username": "", + "password": "", + "host": "host:9000", + "hostname": "host", + "port": "9000", + "pathname": "/path", + "search": "?query", + "hash": "#frag" + }, + "# Stringification of URL.searchParams", + { + "input": "?a=b&c=d", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar?a=b&c=d", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "?a=b&c=d", + "searchParams": "a=b&c=d", + "hash": "" + }, + { + "input": "??a=b&c=d", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar??a=b&c=d", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "??a=b&c=d", + "searchParams": "%3Fa=b&c=d", + "hash": "" + }, + "# Scheme only", + { + "input": "http:", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "searchParams": "", + "hash": "", + "skip": true + }, + { + "input": "http:", + "base": "https://example.org/foo/bar" + }, + { + "input": "sc:", + "base": "https://example.org/foo/bar", + "href": "sc:", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "searchParams": "", + "hash": "" + }, + "# Percent encoding of fragments", + { + "input": "http://foo.bar/baz?qux#foo\bbar", + "base": "about:blank", + "href": "http://foo.bar/baz?qux#foo%08bar", + "origin": "http://foo.bar", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.bar", + "hostname": "foo.bar", + "port": "", + "pathname": "/baz", + "search": "?qux", + "searchParams": "qux=", + "hash": "#foo%08bar" + }, + "# IPv4 parsing (via https://github.com/nodejs/node/pull/10317)", + { + "input": "http://192.168.257", + "base": "http://other.com/", + "href": "http://192.168.1.1/", + "origin": "http://192.168.1.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.1.1", + "hostname": "192.168.1.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.168.257.com", + "base": "http://other.com/", + "href": "http://192.168.257.com/", + "origin": "http://192.168.257.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.257.com", + "hostname": "192.168.257.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://256", + "base": "http://other.com/", + "href": "http://0.0.1.0/", + "origin": "http://0.0.1.0", + "protocol": "http:", + "username": "", + "password": "", + "host": "0.0.1.0", + "hostname": "0.0.1.0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://256.com", + "base": "http://other.com/", + "href": "http://256.com/", + "origin": "http://256.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "256.com", + "hostname": "256.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://999999999", + "base": "http://other.com/", + "href": "http://59.154.201.255/", + "origin": "http://59.154.201.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "59.154.201.255", + "hostname": "59.154.201.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://999999999.com", + "base": "http://other.com/", + "href": "http://999999999.com/", + "origin": "http://999999999.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "999999999.com", + "hostname": "999999999.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://10000000000", + "base": "http://other.com/" + }, + { + "input": "http://10000000000.com", + "base": "http://other.com/", + "href": "http://10000000000.com/", + "origin": "http://10000000000.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "10000000000.com", + "hostname": "10000000000.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://4294967295", + "base": "http://other.com/", + "href": "http://255.255.255.255/", + "origin": "http://255.255.255.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "255.255.255.255", + "hostname": "255.255.255.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://4294967296", + "base": "http://other.com/" + }, + { + "input": "http://0xffffffff", + "base": "http://other.com/", + "href": "http://255.255.255.255/", + "origin": "http://255.255.255.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "255.255.255.255", + "hostname": "0xffffffff", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0xffffffff1", + "base": "http://other.com/" + }, + { + "input": "http://256.256.256.256", + "base": "http://other.com/" + }, + { + "input": "http://256.256.256.256.256", + "base": "http://other.com/", + "href": "http://256.256.256.256.256/", + "origin": "http://256.256.256.256.256", + "protocol": "http:", + "username": "", + "password": "", + "host": "256.256.256.256.256", + "hostname": "256.256.256.256.256", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://0x.0x.0", + "base": "about:blank", + "href": "https://0.0.0.0/", + "origin": "https://0.0.0.0", + "protocol": "https:", + "username": "", + "password": "", + "host": "0.0.0.0", + "hostname": "0x.0x.0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "More IPv4 parsing (via https://github.com/jsdom/whatwg-url/issues/92)", + { + "input": "https://0x100000000/test", + "base": "about:blank" + }, + { + "input": "https://256.0.0.1/test", + "base": "about:blank" + }, + "# file URLs containing percent-encoded Windows drive letters (shouldn't work)", + { + "input": "file:///C%3A/", + "base": "about:blank", + "href": "file:///C%3A/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C%3A/", + "search": "", + "hash": "" + }, + { + "input": "file:///C%7C/", + "base": "about:blank", + "href": "file:///C%7C/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C%7C/", + "search": "", + "hash": "" + }, + "# file URLs relative to other file URLs (via https://github.com/jsdom/whatwg-url/pull/60)", + { + "input": "pix/submit.gif", + "base": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/anchor.html", + "href": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///C:/", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# More file URL tests by zcorpan and annevk", + { + "input": "/", + "base": "file:///C:/a/b", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "//d:", + "base": "file:///C:/a/b", + "href": "file:///d:", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/d:", + "search": "", + "hash": "", + "failure" : true + }, + { + "input": "//d:/..", + "base": "file:///C:/a/b", + "href": "file:///d:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/d:/", + "search": "", + "hash": "", + "failure": true + }, + { + "input": "..", + "base": "file:///ab:/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///1:/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "", + "base": "file:///test?test#test", + "href": "file:///test?test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "" + }, + { + "input": "file:", + "base": "file:///test?test#test", + "href": "file:///test?test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "" + }, + { + "input": "?x", + "base": "file:///test?test#test", + "href": "file:///test?x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?x", + "hash": "" + }, + { + "input": "file:?x", + "base": "file:///test?test#test", + "href": "file:///test?x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?x", + "hash": "" + }, + { + "input": "#x", + "base": "file:///test?test#test", + "href": "file:///test?test#x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "#x" + }, + { + "input": "file:#x", + "base": "file:///test?test#test", + "href": "file:///test?test#x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "#x" + }, + "# File URLs and many (back)slashes", + { + "input": "file:\\\\//", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:\\\\\\\\", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:\\\\\\\\?fox", + "base": "about:blank", + "href": "file:///?fox", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "?fox", + "hash": "" + }, + { + "input": "file:\\\\\\\\#guppy", + "base": "about:blank", + "href": "file:///#guppy", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "#guppy" + }, + { + "input": "file://spider///", + "base": "about:blank", + "href": "file://spider/", + "protocol": "file:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:\\\\localhost//", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:///localhost//cat", + "base": "about:blank", + "href": "file:///localhost//cat", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/localhost//cat", + "search": "", + "hash": "" + }, + { + "input": "file://\\/localhost//cat", + "base": "about:blank", + "href": "file:///localhost//cat", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/localhost//cat", + "search": "", + "hash": "" + }, + { + "input": "file://localhost//a//../..//", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "/////mouse", + "base": "file:///elephant", + "href": "file:///mouse", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/mouse", + "search": "", + "hash": "" + }, + { + "input": "\\//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "\\/localhost//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "//localhost//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "/..//localhost//pig", + "base": "file://lion/", + "href": "file://lion/localhost//pig", + "protocol": "file:", + "username": "", + "password": "", + "host": null, + "hostname": null, + "port": "", + "pathname": "/localhost//pig", + "search": "", + "hash": "" + }, + { + "input": "file://", + "base": "file://ape/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# File URLs with non-empty hosts", + { + "input": "/rooibos", + "base": "file://tea/", + "href": "file://tea/rooibos", + "protocol": "file:", + "username": "", + "password": "", + "host": "tea", + "hostname": null, + "port": "", + "pathname": "/rooibos", + "search": "", + "hash": "" + }, + { + "input": "/?chai", + "base": "file://tea/", + "href": "file://tea/?chai", + "protocol": "file:", + "username": "", + "password": "", + "host": "tea", + "hostname": null, + "port": "", + "pathname": "/", + "search": "?chai", + "hash": "" + }, + "# Windows drive letter handling with the 'file:' base URL", + { + "input": "C|", + "base": "file://host/dir/file", + "href": "file:///C:", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|#", + "base": "file://host/dir/file", + "href": "file:///C:#", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|?", + "base": "file://host/dir/file", + "href": "file:///C:?", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|/", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C|\n/", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C|\\", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C", + "base": "file://host/dir/file", + "href": "file://host/dir/C", + "protocol": "file:", + "username": "", + "password": "", + "host": "host", + "hostname": null, + "port": "", + "pathname": "/dir/C", + "search": "", + "hash": "" + }, + { + "input": "C|a", + "base": "file://host/dir/file", + "href": "file://host/dir/C|a", + "protocol": "file:", + "username": "", + "password": "", + "host": "host", + "hostname": null, + "port": "", + "pathname": "/dir/C|a", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk with not empty host", + { + "input": "file://example.net/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://1.2.3.4/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://[1::8]/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk (no host)", + { + "input": "file:/C|/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://C|/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + "# file URLs without base URL by Rimas Misevičius", + { + "input": "file:", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:?q=v", + "base": "about:blank", + "href": "file:///?q=v", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "?q=v", + "hash": "" + }, + { + "input": "file:#frag", + "base": "about:blank", + "href": "file:///#frag", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "#frag" + }, + "# IPv6 tests", + { + "input": "http://[1:0::]", + "base": "http://example.net/", + "href": "http://[1::]/", + "origin": "http://[1::]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[1::]", + "hostname": "1:0:0:0:0:0:0:0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[0:1:2:3:4:5:6:7:8]", + "base": "http://example.net/" + }, + { + "input": "https://[0::0::0]", + "base": "about:blank" + }, + { + "input": "https://[0:.0]", + "base": "about:blank" + }, + { + "input": "https://[0:0:]", + "base": "about:blank" + }, + { + "input": "https://[0:1:2:3:4:5:6:7.0.0.0.1]", + "base": "about:blank" + }, + { + "input": "https://[0:1.00.0.0.0]", + "base": "about:blank" + }, + { + "input": "https://[0:1.290.0.0.0]", + "base": "about:blank" + }, + { + "input": "https://[0:1.23.23]", + "base": "about:blank" + }, + "# Empty host", + { + "input": "http://?", + "base": "about:blank" + }, + { + "input": "http://#", + "base": "about:blank" + }, + "# Non-special-URL path tests", + { + "input": "sc://ñ", + "base": "about:blank", + "href": "sc://%C3%B1", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "sc://ñ?x", + "base": "about:blank", + "href": "sc://%C3%B1?x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": null, + "port": "", + "pathname": "", + "search": "?x", + "hash": "" + }, + { + "input": "sc://ñ#x", + "base": "about:blank", + "href": "sc://%C3%B1#x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "sc://ñ", + "href": "sc://%C3%B1#x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "#x" + }, + { + "input": "?x", + "base": "sc://ñ", + "href": "sc://%C3%B1?x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": null, + "port": "", + "pathname": "", + "search": "?x", + "hash": "" + }, + { + "input": "sc://?", + "base": "about:blank", + "href": "sc://?", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "sc://#", + "base": "about:blank", + "href": "sc://#", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "", + "search": "", + "hash": "" + }, + { + "input": "///", + "base": "sc://x/", + "href": "sc:///", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "////", + "base": "sc://x/", + "href": "sc:////", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "//", + "search": "", + "hash": "" + }, + { + "input": "////x/", + "base": "sc://x/", + "href": "sc:////x/", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "//x/", + "search": "", + "hash": "" + }, + { + "input": "tftp://foobar.com/someconfig;mode=netascii", + "base": "about:blank", + "href": "tftp://foobar.com/someconfig;mode=netascii", + "origin": "null", + "protocol": "tftp:", + "username": "", + "password": "", + "host": "foobar.com", + "hostname": "foobar.com", + "port": "", + "pathname": "/someconfig;mode=netascii", + "search": "", + "hash": "" + }, + { + "input": "telnet://user:pass@foobar.com:23/", + "base": "about:blank", + "href": "telnet://user:pass@foobar.com:23/", + "origin": "null", + "protocol": "telnet:", + "username": "user", + "password": "pass", + "host": "foobar.com:23", + "hostname": "foobar.com", + "port": "23", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ut2004://10.10.10.10:7777/Index.ut2", + "base": "about:blank", + "href": "ut2004://10.10.10.10:7777/Index.ut2", + "origin": "null", + "protocol": "ut2004:", + "username": "", + "password": "", + "host": "10.10.10.10:7777", + "hostname": null, + "port": "7777", + "pathname": "/Index.ut2", + "search": "", + "hash": "" + }, + { + "input": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz", + "base": "about:blank", + "href": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz", + "origin": "null", + "protocol": "redis:", + "username": "foo", + "password": "bar", + "host": "somehost:6379", + "hostname": "somehost", + "port": "6379", + "pathname": "/0", + "search": "?baz=bam&qux=baz", + "hash": "" + }, + { + "input": "rsync://foo@host:911/sup", + "base": "about:blank", + "href": "rsync://foo@host:911/sup", + "origin": "null", + "protocol": "rsync:", + "username": "foo", + "password": "", + "host": "host:911", + "hostname": "host", + "port": "911", + "pathname": "/sup", + "search": "", + "hash": "" + }, + { + "input": "git://github.com/foo/bar.git", + "base": "about:blank", + "href": "git://github.com/foo/bar.git", + "origin": "null", + "protocol": "git:", + "username": "", + "password": "", + "host": "github.com", + "hostname": "github.com", + "port": "", + "pathname": "/foo/bar.git", + "search": "", + "hash": "" + }, + { + "input": "irc://myserver.com:6999/channel?passwd", + "base": "about:blank", + "href": "irc://myserver.com:6999/channel?passwd", + "origin": "null", + "protocol": "irc:", + "username": "", + "password": "", + "host": "myserver.com:6999", + "hostname": "myserver.com", + "port": "6999", + "pathname": "/channel", + "search": "?passwd", + "hash": "" + }, + { + "input": "dns://fw.example.org:9999/foo.bar.org?type=TXT", + "base": "about:blank", + "href": "dns://fw.example.org:9999/foo.bar.org?type=TXT", + "origin": "null", + "protocol": "dns:", + "username": "", + "password": "", + "host": "fw.example.org:9999", + "hostname": "fw.example.org", + "port": "9999", + "pathname": "/foo.bar.org", + "search": "?type=TXT", + "hash": "" + }, + { + "input": "ldap://localhost:389/ou=People,o=JNDITutorial", + "base": "about:blank", + "href": "ldap://localhost:389/ou=People,o=JNDITutorial", + "origin": "null", + "protocol": "ldap:", + "username": "", + "password": "", + "host": "localhost:389", + "hostname": "localhost", + "port": "389", + "pathname": "/ou=People,o=JNDITutorial", + "search": "", + "hash": "" + }, + { + "input": "git+https://github.com/foo/bar", + "base": "about:blank", + "href": "git+https://github.com/foo/bar", + "origin": "null", + "protocol": "git+https:", + "username": "", + "password": "", + "host": "github.com", + "hostname": "github.com", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "urn:ietf:rfc:2648", + "base": "about:blank", + "href": "urn:ietf:rfc:2648", + "origin": "null", + "protocol": "urn:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "ietf:rfc:2648", + "search": "", + "hash": "" + }, + { + "input": "tag:joe@example.org,2001:foo/bar", + "base": "about:blank", + "href": "tag:joe@example.org,2001:foo/bar", + "origin": "null", + "protocol": "tag:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "joe@example.org,2001:foo/bar", + "search": "", + "hash": "" + }, + "# percent encoded hosts in non-special-URLs", + { + "input": "non-special://%E2%80%A0/", + "base": "about:blank", + "href": "non-special://%E2%80%A0/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "%E2%80%A0", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://H%4fSt/path", + "base": "about:blank", + "href": "non-special://H%4fSt/path", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "H%4fSt", + "hostname": null, + "port": "", + "pathname": "/path", + "search": "", + "hash": "" + }, + "# IPv6 in non-special-URLs", + { + "input": "non-special://[1:2:0:0:5:0:0:0]/", + "base": "about:blank", + "href": "non-special://[1:2:0:0:5::]/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2:0:0:5::]", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[1:2:0:0:0:0:0:3]/", + "base": "about:blank", + "href": "non-special://[1:2::3]/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2::3]", + "hostname": null, + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[1:2::3]:80/", + "base": "about:blank", + "href": "non-special://[1:2::3]:80/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2::3]:80", + "hostname": null, + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[:80/", + "base": "about:blank" + }, + { + "input": "blob:https://example.com:443/", + "base": "about:blank", + "href": "blob:https://example.com:443/", + "protocol": "blob:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "https://example.com:443/", + "search": "", + "hash": "" + }, + { + "input": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf", + "base": "about:blank", + "href": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf", + "protocol": "blob:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "d3958f5c-0777-0845-9dcf-2cb28783acaf", + "search": "", + "hash": "" + }, + "Invalid IPv4 radix digits", + { + "input": "http://0177.0.0.0189", + "base": "about:blank", + "href": "http://0177.0.0.0189/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0177.0.0.0189", + "hostname": "177-0-0-189.cbace701.dsl.brasiltelecom.net.br", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0x7f.0.0.0x7g", + "base": "about:blank", + "href": "http://0x7f.0.0.0x7g/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0x7f.0.0.0x7g", + "hostname": "0x7f.0.0.0x7g", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0X7F.0.0.0X7G", + "base": "about:blank", + "href": "http://0x7f.0.0.0x7g/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0x7f.0.0.0x7g", + "hostname": "0x7f.0.0.0x7g", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid IPv4 portion of IPv6 address", + { + "input": "http://[::127.0.0.0.1]", + "base": "about:blank" + }, + "Uncompressed IPv6 addresses with 0", + { + "input": "http://[0:1:0:1:0:1:0:1]", + "base": "about:blank", + "href": "http://[0:1:0:1:0:1:0:1]/", + "protocol": "http:", + "username": "", + "password": "", + "host": "[0:1:0:1:0:1:0:1]", + "hostname": "0:1:0:1:0:1:0:1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[1:0:1:0:1:0:1:0]", + "base": "about:blank", + "href": "http://[1:0:1:0:1:0:1:0]/", + "protocol": "http:", + "username": "", + "password": "", + "host": "[1:0:1:0:1:0:1:0]", + "hostname": "1:0:1:0:1:0:1:0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Percent-encoded query and fragment", + { + "input": "http://example.org/test?\u0022", + "base": "about:blank", + "href": "http://example.org/test?%22", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%22", + "hash": "" + }, + { + "input": "http://example.org/test?\u0023", + "base": "about:blank", + "href": "http://example.org/test?#", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "http://example.org/test?\u003C", + "base": "about:blank", + "href": "http://example.org/test?%3C", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%3C", + "hash": "" + }, + { + "input": "http://example.org/test?\u003E", + "base": "about:blank", + "href": "http://example.org/test?%3E", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%3E", + "hash": "" + }, + { + "input": "http://example.org/test?\u2323", + "base": "about:blank", + "href": "http://example.org/test?%E2%8C%A3", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%E2%8C%A3", + "hash": "" + }, + { + "input": "http://example.org/test?%23%23", + "base": "about:blank", + "href": "http://example.org/test?%23%23", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%23%23", + "hash": "" + }, + { + "input": "http://example.org/test?%GH", + "base": "about:blank", + "href": "http://example.org/test?%GH", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%GH", + "hash": "", + "failure": true + }, + { + "input": "http://example.org/test?a#%EF", + "base": "about:blank", + "href": "http://example.org/test?a#%EF", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#%EF", + "failure" : true + }, + { + "input": "http://example.org/test?a#%GH", + "base": "about:blank", + "href": "http://example.org/test?a#%GH", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#%GH", + "failure": true + }, + "Bad bases", + { + "input": "test-a.html", + "base": "a", + "failure": true + }, + { + "input": "test-a-slash.html", + "base": "a/", + "failure": true + }, + { + "input": "test-a-slash-slash.html", + "base": "a//", + "failure": true + }, + { + "input": "test-a-colon.html", + "base": "a:" + }, + { + "input": "test-a-colon-slash.html", + "base": "a:/", + "href": "a:/test-a-colon-slash.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test-a-colon-slash.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-slash-slash.html", + "base": "a://", + "href": "a:///test-a-colon-slash-slash.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test-a-colon-slash-slash.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-b.html", + "base": "a:b" + }, + { + "input": "test-a-colon-slash-b.html", + "base": "a:/b", + "href": "a:/test-a-colon-slash-b.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": null, + "port": "", + "pathname": "/test-a-colon-slash-b.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-slash-slash-b.html", + "base": "a://b", + "href": "a://b/test-a-colon-slash-slash-b.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "b", + "hostname": null, + "port": "", + "pathname": "/test-a-colon-slash-slash-b.html", + "search": "", + "hash": "" + }, + "Null code point in fragment", + { + "input": "http://example.org/test?a#b\u0000c", + "base": "about:blank", + "href": "http://example.org/test?a#bc", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#bc" + } +] \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..7984cf4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,28 @@ +dependencyResolutionManagement { + versionCatalogs { + libs { + version('gradle', '7.5.1') + version('junit', '5.9.1') + library('junit-jupiter-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit') + library('junit-jupiter-params', 'org.junit.jupiter', 'junit-jupiter-params').versionRef('junit') + library('junit-jupiter-engine', 'org.junit.jupiter', 'junit-jupiter-engine').versionRef('junit') + library('junit4', 'junit', 'junit').version('4.13.2') + library('hamcrest', 'org.hamcrest', 'hamcrest-library').version('2.2') + library('bouncycastle', 'org.bouncycastle', 'bcpkix-jdk18on').version('1.71') + library('conscrypt', 'org.conscrypt', 'conscrypt-openjdk-uber').version('2.5.2') + library('jackson', 'com.fasterxml.jackson.core', 'jackson-databind').version('2.12.6') + library('guice', 'org.xbib', 'guice').version('4.4.2') + library('jna', 'net.java.dev.jna', 'jna').version('5.10.0') + library('datastructures-common', 'org.xbib', 'datastructures-common').version('1.0.1') + plugin('publish', 'com.gradle.plugin-publish').version('0.18.0') + } + } +} + +include 'net' +include 'net-bouncycastle' +include 'net-mime' +include 'net-path' +include 'net-security' +include 'net-socket' +include 'benchmark'