├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── nel │ │ ├── Client.java │ │ ├── Endpoint.java │ │ ├── EndpointGroup.java │ │ ├── EndpointGroupJsonAdapter.java │ │ ├── InvalidHeaderException.java │ │ ├── NelPolicy.java │ │ ├── NelPolicyJsonAdapter.java │ │ ├── Origin.java │ │ ├── OriginMap.java │ │ ├── QueuedReport.java │ │ ├── Report.java │ │ ├── ReportJsonAdapter.java │ │ ├── ReportingCache.java │ │ └── Type.java └── resources │ └── checkstyle.xml └── test └── java └── nel ├── ClientTest.java ├── EndpointGroupTest.java ├── NelPolicyTest.java ├── OriginMapTest.java ├── OriginTest.java ├── ReportTest.java └── ReportingCacheTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | eclipsebin 5 | 6 | bin 7 | gen 8 | build 9 | out 10 | lib 11 | 12 | target 13 | pom.xml.* 14 | release.properties 15 | 16 | .idea 17 | *.iml 18 | *.ipr 19 | *.iws 20 | classes 21 | 22 | obj 23 | 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | - oraclejdk9 5 | - openjdk7 6 | 7 | script: 8 | # Compile and run unit tests 9 | - mvn test -B 10 | # Validate code style 11 | - mvn validate -B 12 | 13 | # In addition to pull requests, always build these branches 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Network Error Logging reporter 2 | 3 | This library implements a reporter for the [Reporting][] and [Network Error 4 | Logging][] (NEL) specifications. These specs allow site owners to instruct 5 | browsers and other user agents to collect and report on reliability information 6 | about the site. This gives you the same information as you'd get from your 7 | server logs, but collected from your clients. This client-side data set will 8 | include information about failed requests that never made it to your serving 9 | infrastructure. 10 | 11 | [Reporting]: https://wicg.github.io/reporting/ 12 | [Network Error Logging]: https://wicg.github.io/network-error-logging/ 13 | 14 | This library provides a full working implementation of the specs, with one 15 | glaring omission: we don't handle any of the actual communication of sending and 16 | receiving HTTP requests. This lets you plug this library into *any* HTTP 17 | request library; we take of parsing and managing the reporting instructions for 18 | each origin, caching reports, and deciding which collector to send each report 19 | to. You provide an implementation of the `ReportDeliverer` interface to handle 20 | the actual HTTP communication using the library that you're integrating with. 21 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | 7 | org.sonatype.oss 8 | oss-parent 9 | 7 10 | 11 | 12 | com.google 13 | nel-reporter 14 | 1.0.0-SNAPSHOT 15 | jar 16 | 17 | Network Error Logging reporter 18 | 19 | Implementation of the client side of the Reporting and Network Error Logging 20 | W3C specs. Takes care of collecting reports and deciding when to upload 21 | them to the appropriate report collectors. All of the actual communication 22 | is handled by callbacks, allowing this library to be used with any HTTP 23 | request library. 24 | 25 | https://github.com/google/nel-reporter-java/ 26 | 27 | 28 | UTF-8 29 | 30 | 31 | 1.7 32 | 1.7 33 | 34 | 35 | 4.12 36 | 37 | 38 | 39 | 40 | Apache 2.0 41 | http://www.apache.org/licenses/LICENSE-2.0.txt 42 | 43 | 44 | 45 | 46 | 47 | com.google.code.gson 48 | gson 49 | 2.8.2 50 | 51 | 52 | joda-time 53 | joda-time 54 | 2.9.9 55 | 56 | 57 | junit 58 | junit 59 | ${junit.version} 60 | test 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-javadoc-plugin 70 | 3.0.0 71 | 72 | 73 | 74 | 75 | 76 | 77 | java8 78 | 79 | 1.8 80 | 81 | 82 | 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-checkstyle-plugin 87 | 3.0.0 88 | 89 | 90 | com.puppycrawl.tools 91 | checkstyle 92 | 8.8 93 | 94 | 95 | 96 | 97 | validate 98 | validate 99 | 100 | src/main/resources/checkstyle.xml 101 | true 102 | false 103 | true 104 | warning 105 | false 106 | 107 | 108 | check 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/main/java/nel/Client.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import java.util.HashMap; 19 | import java.util.List; 20 | 21 | import com.google.gson.Gson; 22 | import com.google.gson.GsonBuilder; 23 | import com.google.gson.JsonSyntaxException; 24 | import org.joda.time.Instant; 25 | 26 | /** 27 | * A particular {@link Origin}'s relationship to a set of {@link Endpoint}s. 28 | */ 29 | public class Client { 30 | /** Creates a new client for a given origin. */ 31 | public Client(Origin origin) { 32 | this.origin = origin; 33 | this.groups = new HashMap(); 34 | } 35 | 36 | /** 37 | * Parses a client from the contents of a Report-To header. 38 | * 39 | * @param headers A list of all values for the Report-To header from the response. 40 | * @param origin The origin of the response. 41 | * @param now The current timestamp. Will be used to calculate expiry times for any endpoint 42 | * groups in the new client. 43 | */ 44 | public static Client parseFromReportToHeader(List headers, Origin origin, Instant now) 45 | throws InvalidHeaderException { 46 | GsonBuilder builder = new GsonBuilder(); 47 | builder.registerTypeAdapter(EndpointGroup.class, new EndpointGroupJsonAdapter(now)); 48 | Gson gson = builder.create(); 49 | Client client = new Client(origin); 50 | for (String header : headers) { 51 | try { 52 | client.addGroup(gson.fromJson(header, EndpointGroup.class)); 53 | } catch (JsonSyntaxException e) { 54 | throw new InvalidHeaderException("Invalid \"Report-To\" header", e); 55 | } 56 | } 57 | return client; 58 | } 59 | 60 | public Origin getOrigin() { 61 | return origin; 62 | } 63 | 64 | /** Adds a new endpoint group to this client. */ 65 | public void addGroup(EndpointGroup group) { 66 | groups.put(group.getName(), group); 67 | } 68 | 69 | /** 70 | * Returns the endpoint group with the given name, or null if there is no such group. 71 | */ 72 | public EndpointGroup getGroup(String name) { 73 | return groups.get(name); 74 | } 75 | 76 | @Override 77 | public String toString() { 78 | return "Client(origin=" + origin + ", groups=" + groups + ")"; 79 | } 80 | 81 | @Override 82 | public boolean equals(Object obj) { 83 | if (!(obj instanceof Client)) { 84 | return false; 85 | } 86 | Client other = (Client) obj; 87 | return this.origin.equals(other.origin) 88 | && this.groups.equals(other.groups); 89 | } 90 | 91 | private Origin origin; 92 | private HashMap groups; 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/nel/Endpoint.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import java.net.URL; 19 | 20 | import org.joda.time.Instant; 21 | 22 | /** 23 | * A location to which reports for a particular origin may be sent. 24 | */ 25 | public class Endpoint { 26 | /** Creates a new endpoint that will upload reports to the given url. */ 27 | public Endpoint(URL url, int priority, int weight) { 28 | this.url = url; 29 | this.priority = priority; 30 | this.weight = weight; 31 | this.failures = 0; 32 | this.retryAfter = null; 33 | } 34 | 35 | /** Creates a new endpoint that will upload reports to the given url. */ 36 | public Endpoint(URL url) { 37 | this(url, 1, 1); 38 | } 39 | 40 | public int getPriority() { 41 | return priority; 42 | } 43 | 44 | public int getWeight() { 45 | return weight; 46 | } 47 | 48 | /** 49 | * Returns whether this endpoint is pending. A pending endpoint is one where we recently 50 | * encountered a failure trying to upload reports, and have not exceeded the retry delay. 51 | */ 52 | public boolean isPending(Instant now) { 53 | return retryAfter != null && retryAfter.isAfter(now); 54 | } 55 | 56 | /** 57 | * Records that we were able to successfully upload reports to this endpoint. This clears any 58 | * existing "pending" flag for the endpoint. 59 | */ 60 | public void recordSuccess() { 61 | this.failures = 0; 62 | this.retryAfter = null; 63 | } 64 | 65 | /** 66 | * Records that we were not able to upload reports to this endpoint. This sets the 67 | * pending flag, ensuring that we don't try to upload to this endpoint again until some point in 68 | * the future. 69 | */ 70 | public void recordFailure(Instant retryAfter) { 71 | this.failures++; 72 | this.retryAfter = retryAfter; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | return "<" + url.toString() + ", priority=" + Integer.toString(priority) 78 | + ", weight=" + Integer.toString(weight) + ">"; 79 | } 80 | 81 | @Override 82 | public boolean equals(Object obj) { 83 | if (!(obj instanceof Endpoint)) { 84 | return false; 85 | } 86 | Endpoint other = (Endpoint) obj; 87 | return this.url.equals(other.url) 88 | && this.priority == other.priority 89 | && this.weight == other.weight; 90 | } 91 | 92 | private URL url; 93 | private int priority; 94 | private int weight; 95 | private int failures; 96 | private Instant retryAfter; 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/nel/EndpointGroup.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import java.net.URL; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.Random; 22 | 23 | import org.joda.time.Duration; 24 | import org.joda.time.Instant; 25 | 26 | /** 27 | * A set of {@link Endpoint}s that will be used together for backup and failover purposes. 28 | */ 29 | public class EndpointGroup { 30 | /** 31 | * Creates a new endpoint group. The ttl and now parameters are used to 32 | * calculate an expiration time for this group; after that point, we will no longer use this group 33 | * to upload reports for its origin. (Presumably the configuration will be refreshed before then 34 | * by a newer successful response from the origin.) 35 | */ 36 | public EndpointGroup(String name, boolean subdomains, Duration ttl, Instant now) { 37 | this.name = name; 38 | this.endpoints = new ArrayList(); 39 | this.subdomains = subdomains; 40 | this.ttl = ttl; 41 | this.creation = now; 42 | this.expiry = now.plus(ttl); 43 | } 44 | 45 | public String getName() { 46 | return name; 47 | } 48 | 49 | public boolean includeSubdomains() { 50 | return subdomains; 51 | } 52 | 53 | /** 54 | * Returns the minimum priority value of all of the non-pending endpoints in this 55 | * group. Returns {@link Integer.MAX_VALUE} if all endpoints in the group are pending. 56 | */ 57 | public int getMinimumPriority(Instant now) { 58 | int minPriority = Integer.MAX_VALUE; 59 | for (Endpoint endpoint : endpoints) { 60 | if (endpoint.isPending(now)) { 61 | continue; 62 | } 63 | int thisPriority = endpoint.getPriority(); 64 | if (thisPriority < minPriority) { 65 | minPriority = thisPriority; 66 | } 67 | } 68 | return minPriority; 69 | } 70 | 71 | /** 72 | * Returns the total weight of all of the non-pending endpoints with the given 73 | * priority. 74 | */ 75 | public int getTotalWeightForPriority(Instant now, int priority) { 76 | int totalWeight = 0; 77 | for (Endpoint endpoint : endpoints) { 78 | if (endpoint.isPending(now)) { 79 | continue; 80 | } 81 | if (endpoint.getPriority() != priority) { 82 | continue; 83 | } 84 | totalWeight += endpoint.getWeight(); 85 | } 86 | return totalWeight; 87 | } 88 | 89 | /** Adds a new endpoint to this group. */ 90 | public void addEndpoint(Endpoint endpoint) { 91 | endpoints.add(endpoint); 92 | } 93 | 94 | /** Adds several new endpoints to this group. */ 95 | public void addEndpoints(List endpoints) { 96 | this.endpoints.addAll(endpoints); 97 | } 98 | 99 | /** Returns whether this endpoint is expired as of now. */ 100 | public boolean isExpired(Instant now) { 101 | return now.isAfter(expiry); 102 | } 103 | 104 | /** 105 | * Chooses an arbitrary endpoint from this group to upload reports to, using the "Choose an endpoint" algorithm 107 | * from the Reporting spec. 108 | */ 109 | public Endpoint chooseEndpoint(Instant now) { 110 | if (isExpired(now)) { 111 | return null; 112 | } 113 | 114 | int minPriority = getMinimumPriority(now); 115 | if (minPriority == Integer.MAX_VALUE) { 116 | return null; 117 | } 118 | 119 | int totalWeight = getTotalWeightForPriority(now, minPriority); 120 | if (totalWeight == 0) { 121 | return null; 122 | } 123 | 124 | int selectedWeight = RANDOM.nextInt(totalWeight); 125 | for (Endpoint endpoint : endpoints) { 126 | if (endpoint.isPending(now)) { 127 | continue; 128 | } 129 | if (endpoint.getPriority() != minPriority) { 130 | continue; 131 | } 132 | int thisWeight = endpoint.getWeight(); 133 | if (selectedWeight < thisWeight) { 134 | return endpoint; 135 | } 136 | selectedWeight -= thisWeight; 137 | } 138 | return null; 139 | } 140 | 141 | @Override 142 | public String toString() { 143 | return "EndpointGroup(name=" + name + ", include-subdomains=" + Boolean.toString(subdomains) 144 | + ", ttl=" + ttl + ", endpoints=[" + endpoints.toString() + "])"; 145 | } 146 | 147 | @Override 148 | public boolean equals(Object obj) { 149 | if (!(obj instanceof EndpointGroup)) { 150 | return false; 151 | } 152 | EndpointGroup other = (EndpointGroup) obj; 153 | return this.name.equals(other.name) 154 | && this.endpoints.equals(other.endpoints) 155 | && this.subdomains == other.subdomains 156 | && this.ttl.equals(other.ttl) 157 | && this.creation.equals(other.creation); 158 | } 159 | 160 | private static Random RANDOM = new Random(); 161 | 162 | private String name; 163 | private ArrayList endpoints; 164 | private boolean subdomains; 165 | private Duration ttl; 166 | private Instant creation; 167 | private Instant expiry; 168 | } 169 | -------------------------------------------------------------------------------- /src/main/java/nel/EndpointGroupJsonAdapter.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import java.io.IOException; 19 | import java.net.MalformedURLException; 20 | import java.net.URL; 21 | import java.util.ArrayList; 22 | 23 | import com.google.gson.TypeAdapter; 24 | import com.google.gson.stream.JsonReader; 25 | import com.google.gson.stream.JsonToken; 26 | import com.google.gson.stream.JsonWriter; 27 | import com.google.gson.stream.MalformedJsonException; 28 | import org.joda.time.Duration; 29 | import org.joda.time.Instant; 30 | 31 | /** 32 | * A GSON TypeAdapter that can parse Report-To headers as defined by the Reporting spec. 34 | */ 35 | public class EndpointGroupJsonAdapter extends TypeAdapter { 36 | /** 37 | * Creates a new adapter that can parse {@link EndpointGroup} instances, using now as 38 | * the creation time. 39 | */ 40 | public EndpointGroupJsonAdapter(Instant now) { 41 | this.now = now; 42 | } 43 | 44 | @Override 45 | public EndpointGroup read(JsonReader reader) throws IOException { 46 | String groupName = "default"; 47 | boolean subdomains = false; 48 | Duration ttl = null; 49 | ArrayList endpoints = null; 50 | 51 | reader.beginObject(); 52 | while (reader.hasNext()) { 53 | String name = reader.nextName(); 54 | if (name.equals("group")) { 55 | if (reader.peek() != JsonToken.STRING) { 56 | throw new MalformedJsonException("\"group\" must be a string in Report-To header"); 57 | } 58 | groupName = reader.nextString(); 59 | } else if (name.equals("include-subdomains")) { 60 | if (reader.peek() != JsonToken.BOOLEAN) { 61 | subdomains = false; 62 | continue; 63 | } 64 | subdomains = reader.nextBoolean(); 65 | } else if (name.equals("max-age")) { 66 | if (reader.peek() != JsonToken.NUMBER) { 67 | throw new MalformedJsonException("\"max-age\" must be a number in Report-To header"); 68 | } 69 | long maxAge = reader.nextLong(); 70 | if (maxAge < 0) { 71 | throw new MalformedJsonException("\"max-age\" must be non-negative in Report-To header"); 72 | } 73 | ttl = Duration.standardSeconds(maxAge); 74 | } else if (name.equals("endpoints")) { 75 | endpoints = readEndpoints(reader); 76 | } else { 77 | reader.skipValue(); 78 | } 79 | } 80 | reader.endObject(); 81 | 82 | if (ttl == null) { 83 | throw new MalformedJsonException("Missing \"max-age\" in Report-To header"); 84 | } 85 | 86 | if (endpoints == null) { 87 | throw new MalformedJsonException("Missing \"endpoints\" in Report-To header"); 88 | } 89 | 90 | if (endpoints.size() == 0) { 91 | throw new MalformedJsonException("Empty \"endpoints\" in Report-To header"); 92 | } 93 | 94 | EndpointGroup group = new EndpointGroup(groupName, subdomains, ttl, now); 95 | group.addEndpoints(endpoints); 96 | return group; 97 | } 98 | 99 | private ArrayList readEndpoints(JsonReader reader) throws IOException { 100 | ArrayList endpoints = new ArrayList(); 101 | reader.beginArray(); 102 | while (reader.hasNext()) { 103 | endpoints.add(readEndpoint(reader)); 104 | } 105 | reader.endArray(); 106 | return endpoints; 107 | } 108 | 109 | private Endpoint readEndpoint(JsonReader reader) throws IOException { 110 | URL url = null; 111 | int priority = 1; 112 | int weight = 1; 113 | 114 | reader.beginObject(); 115 | while (reader.hasNext()) { 116 | String name = reader.nextName(); 117 | if (name.equals("url")) { 118 | if (reader.peek() != JsonToken.STRING) { 119 | throw new MalformedJsonException("\"url\" must be a string in Report-To header"); 120 | } 121 | try { 122 | url = new URL(reader.nextString()); 123 | } catch (MalformedURLException e) { 124 | throw new MalformedJsonException("Invalid endpoint \"url\" in Report-To header", e); 125 | } 126 | if (!url.getProtocol().equals("https")) { 127 | throw new MalformedJsonException("\"url\" must be secure (HTTPS) in Report-To header"); 128 | } 129 | } else if (name.equals("priority")) { 130 | if (reader.peek() != JsonToken.NUMBER) { 131 | throw new MalformedJsonException("\"priority\" must be a string in Report-To header"); 132 | } 133 | priority = reader.nextInt(); 134 | if (priority < 0) { 135 | throw new MalformedJsonException("\"priority\" must be non-negative in Report-To header"); 136 | } 137 | } else if (name.equals("weight")) { 138 | if (reader.peek() != JsonToken.NUMBER) { 139 | throw new MalformedJsonException("\"weight\" must be a string in Report-To header"); 140 | } 141 | weight = reader.nextInt(); 142 | if (weight <= 0) { 143 | throw new MalformedJsonException("\"weight\" must be positive in Report-To header"); 144 | } 145 | } else { 146 | reader.skipValue(); 147 | } 148 | } 149 | reader.endObject(); 150 | 151 | if (url == null) { 152 | throw new MalformedJsonException("Missing endpoint \"url\" in Report-To header"); 153 | } 154 | 155 | return new Endpoint(url, priority, weight); 156 | } 157 | 158 | @Override 159 | public void write(JsonWriter writer, EndpointGroup group) throws IOException { 160 | throw new IllegalStateException("Cannot write EndpointGroups to JSON"); 161 | } 162 | 163 | private Instant now; 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/nel/InvalidHeaderException.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | /** 19 | * An invalid Report-To or NEL header value was found when processing a 20 | * response. 21 | */ 22 | public class InvalidHeaderException extends Exception { 23 | public InvalidHeaderException(String message, Throwable cause) { 24 | super(message, cause); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/nel/NelPolicy.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import com.google.gson.Gson; 19 | import com.google.gson.GsonBuilder; 20 | import com.google.gson.JsonSyntaxException; 21 | import org.joda.time.Duration; 22 | import org.joda.time.Instant; 23 | 24 | /** 25 | * Describes which Network Error Logging reports to collect for an origin. 26 | */ 27 | public class NelPolicy { 28 | /** Creates a new policy. */ 29 | public NelPolicy(Origin origin, String reportTo, boolean includeSubdomains, 30 | double successFraction, double failureFraction, Duration ttl, Instant now) { 31 | this.origin = origin; 32 | this.reportTo = reportTo; 33 | this.subdomains = includeSubdomains; 34 | this.successFraction = successFraction; 35 | this.failureFraction = failureFraction; 36 | this.ttl = ttl; 37 | this.creation = now; 38 | this.expiry = now.plus(ttl); 39 | } 40 | 41 | /** 42 | * Parses a NEL policy from the contents of a NEL header. 43 | * 44 | * @param header The value of the NEL header from the response. 45 | * @param origin The origin of the response. 46 | * @param now The current timestamp. Will be used to calculate expiry times for the new policy. 47 | */ 48 | public static NelPolicy parseFromNelHeader(String header, Origin origin, Instant now) 49 | throws InvalidHeaderException { 50 | GsonBuilder builder = new GsonBuilder(); 51 | builder.registerTypeAdapter(NelPolicy.class, new NelPolicyJsonAdapter(origin, now)); 52 | Gson gson = builder.create(); 53 | try { 54 | return gson.fromJson(header, NelPolicy.class); 55 | } catch (JsonSyntaxException e) { 56 | throw new InvalidHeaderException("Invalid \"NEL\" header", e); 57 | } 58 | } 59 | 60 | public boolean includeSubdomains() { 61 | return subdomains; 62 | } 63 | 64 | public String getReportTo() { 65 | return reportTo; 66 | } 67 | 68 | public double getSuccessFraction() { 69 | return successFraction; 70 | } 71 | 72 | public double getFailureFraction() { 73 | return failureFraction; 74 | } 75 | 76 | /** Returns whether this policy is expired as of now. */ 77 | public boolean isExpired(Instant now) { 78 | return now.isAfter(expiry); 79 | } 80 | 81 | @Override 82 | public String toString() { 83 | return "NelPolicy(origin=" + origin + ", reportTo=" + reportTo + ", includeSubdomains=" 84 | + Boolean.toString(subdomains) + ", successFraction=" + Double.toString(successFraction) 85 | + ", failureFraction=" + Double.toString(failureFraction) + ", ttl=" + ttl + ")"; 86 | } 87 | 88 | @Override 89 | public boolean equals(Object obj) { 90 | if (!(obj instanceof NelPolicy)) { 91 | return false; 92 | } 93 | NelPolicy other = (NelPolicy) obj; 94 | return this.origin.equals(other.origin) && reportTo.equals(other.reportTo) 95 | && subdomains == other.subdomains && successFraction == other.successFraction 96 | && failureFraction == other.failureFraction && this.ttl.equals(other.ttl) 97 | && this.creation.equals(other.creation); 98 | } 99 | 100 | private Origin origin; 101 | private String reportTo; 102 | private boolean subdomains; 103 | private double successFraction; 104 | private double failureFraction; 105 | private Duration ttl; 106 | private Instant creation; 107 | private Instant expiry; 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/nel/NelPolicyJsonAdapter.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import java.io.IOException; 19 | import java.util.ArrayList; 20 | 21 | import com.google.gson.TypeAdapter; 22 | import com.google.gson.stream.JsonReader; 23 | import com.google.gson.stream.JsonToken; 24 | import com.google.gson.stream.JsonWriter; 25 | import com.google.gson.stream.MalformedJsonException; 26 | import org.joda.time.Duration; 27 | import org.joda.time.Instant; 28 | 29 | /** 30 | * A GSON TypeAdapter that can parse NEL headers as defined by the Reporting spec. 32 | */ 33 | public class NelPolicyJsonAdapter extends TypeAdapter { 34 | /** 35 | * Creates a new adapter that can parse {@link NelPolicy} instances for a particular 36 | * origin, using now as the creation time. 37 | */ 38 | public NelPolicyJsonAdapter(Origin origin, Instant now) { 39 | this.origin = origin; 40 | this.now = now; 41 | } 42 | 43 | @Override 44 | public NelPolicy read(JsonReader reader) throws IOException { 45 | String reportTo = null; 46 | boolean subdomains = false; 47 | double successFraction = 0.0; 48 | double failureFraction = 1.0; 49 | Duration ttl = null; 50 | 51 | reader.beginObject(); 52 | while (reader.hasNext()) { 53 | String name = reader.nextName(); 54 | if (name.equals("report-to")) { 55 | if (reader.peek() != JsonToken.STRING) { 56 | throw new MalformedJsonException("\"report-to\" must be a string in NEL header"); 57 | } 58 | reportTo = reader.nextString(); 59 | } else if (name.equals("include-subdomains")) { 60 | if (reader.peek() != JsonToken.BOOLEAN) { 61 | throw new MalformedJsonException( 62 | "\"include-subdomains\" must be a boolean in NEL header"); 63 | } 64 | subdomains = reader.nextBoolean(); 65 | } else if (name.equals("max-age")) { 66 | if (reader.peek() != JsonToken.NUMBER) { 67 | throw new MalformedJsonException("\"max-age\" must be a number in NEL header"); 68 | } 69 | long maxAge = reader.nextLong(); 70 | if (maxAge < 0) { 71 | throw new MalformedJsonException("\"max-age\" must be non-negative in NEL header"); 72 | } 73 | ttl = Duration.standardSeconds(maxAge); 74 | } else if (name.equals("success-fraction")) { 75 | if (reader.peek() != JsonToken.NUMBER) { 76 | throw new MalformedJsonException("\"success-fraction\" must be a number in NEL header"); 77 | } 78 | successFraction = reader.nextDouble(); 79 | if (successFraction < 0.0) { 80 | throw new MalformedJsonException("\"success-fraction\" must be >= 0.0 in NEL header"); 81 | } 82 | if (successFraction > 1.0) { 83 | throw new MalformedJsonException("\"success-fraction\" must be <= 1.0 in NEL header"); 84 | } 85 | } else if (name.equals("failure-fraction")) { 86 | if (reader.peek() != JsonToken.NUMBER) { 87 | throw new MalformedJsonException("\"failure-fraction\" must be a number in NEL header"); 88 | } 89 | failureFraction = reader.nextDouble(); 90 | if (failureFraction < 0.0) { 91 | throw new MalformedJsonException("\"failure-fraction\" must be >= 0.0 in NEL header"); 92 | } 93 | if (failureFraction > 1.0) { 94 | throw new MalformedJsonException("\"failure-fraction\" must be <= 1.0 in NEL header"); 95 | } 96 | } else { 97 | reader.skipValue(); 98 | } 99 | } 100 | reader.endObject(); 101 | 102 | if (ttl == null) { 103 | throw new MalformedJsonException("Missing \"max-age\" in NEL header"); 104 | } 105 | 106 | if (reportTo == null && !ttl.equals(Duration.ZERO)) { 107 | throw new MalformedJsonException("Missing \"report-to\" in NEL header"); 108 | } 109 | 110 | return new NelPolicy(origin, reportTo, subdomains, successFraction, failureFraction, ttl, now); 111 | } 112 | 113 | @Override 114 | public void write(JsonWriter writer, NelPolicy policy) throws IOException { 115 | throw new IllegalStateException("Cannot write NelPolicies to JSON"); 116 | } 117 | 118 | private Origin origin; 119 | private Instant now; 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/nel/Origin.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import java.net.URI; 19 | import java.util.Objects; 20 | 21 | /** 22 | * The origin of an HTTP 23 | * request. Per the HTML spec: 24 | * 25 | *
26 | * Origins are the fundamental currency of the Web's security model. Two actors in the Web platform 27 | * that share an origin are assumed to trust each other and to have the same authority. Actors with 28 | * differing origins are considered potentially hostile versus each other, and are isolated from 29 | * each other to varying degrees. 30 | *
31 | * 32 | *

33 | * This class in particular represents a tuple origin — the combination of scheme, host, 34 | * and port from the original request. 35 | *

36 | */ 37 | public class Origin { 38 | /** 39 | * Creates a new origin with the given scheme, host, and 40 | * port. 41 | */ 42 | public Origin(String scheme, String host, int port) { 43 | this.scheme = scheme; 44 | this.host = host; 45 | this.port = port; 46 | } 47 | 48 | /** 49 | * Creates a new origin for the given {@link URI}. 50 | */ 51 | public Origin(URI uri) { 52 | this(uri.getScheme(), uri.getHost(), uri.getPort()); 53 | } 54 | 55 | /** 56 | * Creates a new origin whose host is the superdomain of this origin's host, 58 | * or null if this origin's host has no superdomain. 59 | */ 60 | public Origin getSuperdomainOrigin() { 61 | int index = host.indexOf('.'); 62 | if (index == -1) { 63 | return null; 64 | } 65 | return new Origin(scheme, host.substring(index + 1), port); 66 | } 67 | 68 | public String getScheme() { 69 | return scheme; 70 | } 71 | 72 | public String getHost() { 73 | return host; 74 | } 75 | 76 | public int getPort() { 77 | return port; 78 | } 79 | 80 | @Override 81 | public String toString() { 82 | return scheme + "://" + host + ":" + Integer.toString(port); 83 | } 84 | 85 | @Override 86 | public boolean equals(Object obj) { 87 | if (!(obj instanceof Origin)) { 88 | return false; 89 | } 90 | Origin other = (Origin) obj; 91 | return this.scheme.equals(other.scheme) && this.host.equals(other.host) 92 | && this.port == other.port; 93 | } 94 | 95 | @Override 96 | public int hashCode() { 97 | return Objects.hash(scheme, host, port); 98 | } 99 | 100 | private String scheme; 101 | private String host; 102 | private int port; 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/nel/OriginMap.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import java.util.ArrayList; 19 | import java.util.HashMap; 20 | import java.util.Iterator; 21 | import java.util.NoSuchElementException; 22 | 23 | /** 24 | * A {@link HashMap} specialization that only works with {@link Origin} as the key. Includes a 25 | * {@link #getAll} method that knows how to look up entries for all superdomains of an origin. 26 | */ 27 | public class OriginMap extends HashMap { 28 | /** Creates a new, empty map. */ 29 | public OriginMap() { 30 | super(); 31 | } 32 | 33 | /** 34 | * Returns all of the entries that cover a particular origin. This includes any entry for the 35 | * origin itself, as well as the entries for all of the origin's superdomains. The elements of 36 | * the list will be ordered, with more specific matches occurring first. 37 | */ 38 | public Iterable getAll(Origin origin) { 39 | return new AllIterable(origin); 40 | } 41 | 42 | private class AllIterable implements Iterable { 43 | private AllIterable(Origin origin) { 44 | this.origin = origin; 45 | } 46 | 47 | public Iterator iterator() { 48 | return new AllIterator(origin); 49 | } 50 | 51 | private Origin origin; 52 | } 53 | 54 | private class AllIterator implements Iterator { 55 | private AllIterator(Origin start) { 56 | this.origin = start; 57 | this.nextElement = null; 58 | advance(); 59 | } 60 | 61 | private void advance() { 62 | while (origin != null) { 63 | nextElement = get(origin); 64 | origin = origin.getSuperdomainOrigin(); 65 | if (nextElement != null) { 66 | return; 67 | } 68 | } 69 | } 70 | 71 | @Override 72 | public boolean hasNext() { 73 | return origin != null; 74 | } 75 | 76 | @Override 77 | public V next() { 78 | if (origin == null) { 79 | throw new NoSuchElementException(); 80 | } 81 | V result = nextElement; 82 | advance(); 83 | return result; 84 | } 85 | 86 | @Override 87 | public void remove() { 88 | throw new UnsupportedOperationException("Cannot remove from OriginMap#getAll"); 89 | } 90 | 91 | private Origin origin; 92 | private V nextElement; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/nel/QueuedReport.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | /** 19 | * A wrapper around a {@link Report} once it's been added to a {@link ReportingCache}. Keeps track 20 | * of some logistics about the report, such as how many times we've tried to upload it. 21 | */ 22 | public class QueuedReport { 23 | /** 24 | * Creates a new queued report that will be uploaded to an endpoint in the given 25 | * group. 26 | */ 27 | public QueuedReport(Report report, String group) { 28 | this.report = report; 29 | this.origin = new Origin(report.getUri()); 30 | this.group = group; 31 | } 32 | 33 | public Report getReport() { 34 | return report; 35 | } 36 | 37 | public Origin getOrigin() { 38 | return origin; 39 | } 40 | 41 | public String getGroup() { 42 | return group; 43 | } 44 | 45 | private Report report; 46 | private Origin origin; 47 | private String group; 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/nel/Report.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import java.net.InetAddress; 19 | import java.net.URI; 20 | import java.net.URISyntaxException; 21 | import java.net.UnknownHostException; 22 | 23 | import com.google.gson.GsonBuilder; 24 | import org.joda.time.Duration; 25 | import org.joda.time.Instant; 26 | 27 | /** 28 | * Contains a single NEL report, as defined by the Reporting and NEL specs. 31 | */ 32 | public class Report { 33 | /** Returns the timestamp when the report was created. */ 34 | public Instant getTimestamp() { 35 | return timestamp; 36 | } 37 | 38 | /** Returns the URI of the request that this report describes (the "original request"). */ 39 | public URI getUri() { 40 | return uri; 41 | } 42 | 43 | /** Returns the referrer information for the original request, if any. */ 44 | public URI getReferrer() { 45 | return referrer; 46 | } 47 | 48 | /** 49 | * Returns the sampling rate that was in effect when this report was captured. When doing any 50 | * follow-on analysis, you should assume that this report represents 1 / 51 | * samplingFraction requests. 52 | */ 53 | public double getSamplingFraction() { 54 | return samplingFraction; 55 | } 56 | 57 | /** 58 | * Returns the IP address of the server that the original request was sent to. This can be 59 | * null if DNS failed to resolve an IP address for the request. 60 | */ 61 | public InetAddress getServerIp() { 62 | return serverIp; 63 | } 64 | 65 | /** 66 | * Returns the ALPN protocol that was used for 67 | * the original request. 68 | */ 69 | public String getProtocol() { 70 | return protocol; 71 | } 72 | 73 | /** Returns the HTTP status code that was returned for the original request. */ 74 | public int getStatusCode() { 75 | return statusCode; 76 | } 77 | 78 | /** Returns the amount of time that it took to process the original request. */ 79 | public Duration getElapsedTime() { 80 | return elapsedTime; 81 | } 82 | 83 | /** 84 | * Returns the description of the error that occurred when processing the original request, or 85 | * ok if the original request was successful. 86 | */ 87 | public Type getType() { 88 | return type; 89 | } 90 | 91 | /** Sets the timestamp of this report. */ 92 | public Report setTimestamp(Instant timestamp) { 93 | this.timestamp = timestamp; 94 | return this; 95 | } 96 | 97 | /** 98 | * Sets the URI of this report. We will remove any fragment in uri as required by 99 | * the specs. 100 | */ 101 | public Report setUri(URI uri) { 102 | // Remove the fragment identifier from the URI, if any. 103 | try { 104 | this.uri = new URI(uri.getScheme(), uri.getSchemeSpecificPart(), null); 105 | } catch (URISyntaxException e) { 106 | // Rethrow this as unchecked; we started with a valid URI, so this should never occur. 107 | throw new IllegalArgumentException(e); 108 | } 109 | return this; 110 | } 111 | 112 | /** 113 | * Sets the URI of this report from a string. This should only be used in test cases. 114 | */ 115 | public Report setUri(String uri) { 116 | try { 117 | return setUri(new URI(uri)); 118 | } catch (URISyntaxException e) { 119 | // Rethrow this as unchecked; this should only be used in test cases, and you should only pass 120 | // in valid URIs. 121 | throw new IllegalArgumentException(e); 122 | } 123 | } 124 | 125 | /** Sets the referrer URI of this report. */ 126 | public Report setReferrer(URI referrer) { 127 | this.referrer = referrer; 128 | return this; 129 | } 130 | 131 | /** Sets the sampling fraction of this report. */ 132 | public Report setSamplingFraction(double samplingFraction) { 133 | this.samplingFraction = samplingFraction; 134 | return this; 135 | } 136 | 137 | /** Sets the server IP address of this report. */ 138 | public Report setServerIp(InetAddress serverIp) { 139 | this.serverIp = serverIp; 140 | return this; 141 | } 142 | 143 | /** 144 | * Sets the server IP address of this report. This should only be used in test cases, and you 145 | * must only pass in IP address literals for serverIp. 146 | */ 147 | public Report setServerIp(String serverIp) { 148 | try { 149 | return setServerIp(InetAddress.getByName(serverIp)); 150 | } catch (UnknownHostException e) { 151 | // Rethrow this as unchecked; this method is only for test cases, and should only be operating 152 | // on IP addresses. 153 | throw new IllegalArgumentException(e); 154 | } 155 | } 156 | 157 | /** Sets the protocol of this report. */ 158 | public Report setProtocol(String protocol) { 159 | this.protocol = protocol; 160 | return this; 161 | } 162 | 163 | /** Sets the HTTP status code of this report. */ 164 | public Report setStatusCode(int statusCode) { 165 | this.statusCode = statusCode; 166 | return this; 167 | } 168 | 169 | /** Sets the elapsed time of this report. */ 170 | public Report setElapsedTime(Duration elapsedTime) { 171 | this.elapsedTime = elapsedTime; 172 | return this; 173 | } 174 | 175 | /** Sets the error type of this report. */ 176 | public Report setType(Type type) { 177 | this.type = type; 178 | return this; 179 | } 180 | 181 | /** 182 | * Renders this report in JSON, using the current system time to calculate the age of 183 | * the report. 184 | */ 185 | public String toString() { 186 | return toString(new Instant()); 187 | } 188 | 189 | /** 190 | * Renders this report in JSON, using now to calculate the age of the 191 | * report. 192 | */ 193 | public String toString(Instant now) { 194 | GsonBuilder builder = new GsonBuilder(); 195 | builder.registerTypeAdapter(Report.class, new ReportJsonAdapter(now)); 196 | return builder.setPrettyPrinting().create().toJson(this); 197 | } 198 | 199 | private Instant timestamp; 200 | private URI uri; 201 | private URI referrer; 202 | private double samplingFraction; 203 | private InetAddress serverIp; 204 | private String protocol; 205 | private int statusCode; 206 | private Duration elapsedTime; 207 | private Type type; 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/nel/ReportJsonAdapter.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import java.io.IOException; 19 | 20 | import com.google.gson.TypeAdapter; 21 | import com.google.gson.stream.JsonReader; 22 | import com.google.gson.stream.JsonWriter; 23 | import org.joda.time.Duration; 24 | import org.joda.time.Instant; 25 | 26 | /** 27 | * A GSON TypeAdapter that can render a {@link Report} instance in JSON as defined by the Reporting and NEL specs. 30 | * 31 | *

32 | * Note that {@link Report}s include a timestamp (when the report was generated), while the specs 33 | * define an age field (the difference in time between when the report was generated 34 | * and when it was uploaded). When constructing a new adapter, you must pass in the current time 35 | * (or whatever time you wish to use as the "upload time"), which we will use to calculate the 36 | * age fields. 37 | *

38 | */ 39 | public class ReportJsonAdapter extends TypeAdapter { 40 | /** 41 | * Creates a new adapter that uses now as the base time for calculating the 42 | * age field of any JSON report payloads. 43 | */ 44 | public ReportJsonAdapter(Instant now) { 45 | this.now = now; 46 | } 47 | 48 | @Override 49 | public Report read(JsonReader reader) throws IOException { 50 | throw new IllegalStateException("Cannot parse Reports from JSON"); 51 | } 52 | 53 | @Override 54 | public void write(JsonWriter writer, Report report) throws IOException { 55 | writer.beginObject(); 56 | if (report.getTimestamp() != null) { 57 | writer.name("age").value( 58 | new Duration(report.getTimestamp(), now).getMillis()); 59 | } 60 | writer.name("type").value("network-error"); 61 | writer.name("url").value(report.getUri().toString()); 62 | writer.name("body").beginObject(); 63 | writer.name("uri").value(report.getUri().toString()); 64 | if (report.getReferrer() == null) { 65 | writer.name("referrer").nullValue(); 66 | } else { 67 | writer.name("referrer").value(report.getReferrer().toString()); 68 | } 69 | writer.name("sampling-fraction").value(report.getSamplingFraction()); 70 | writer.name("server-ip").value(report.getServerIp().getHostAddress()); 71 | writer.name("protocol").value(report.getProtocol()); 72 | if (report.getStatusCode() != 0) { 73 | writer.name("status-code").value(report.getStatusCode()); 74 | } 75 | writer.name("elapsed-time").value(report.getElapsedTime().getMillis()); 76 | writer.name("type").value(report.getType().toString()); 77 | writer.endObject(); 78 | writer.endObject(); 79 | } 80 | 81 | private Instant now; 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/nel/ReportingCache.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import java.util.HashSet; 19 | import java.util.Iterator; 20 | 21 | import org.joda.time.Instant; 22 | 23 | /** 24 | * A cache of all of all Reporting and NEL configurations that we have received, and of reports that 25 | * are queued for delivery. 26 | */ 27 | public class ReportingCache { 28 | /** Creates a new, empty cache. */ 29 | public ReportingCache() { 30 | this.clients = new OriginMap(); 31 | this.queuedReports = new HashSet(); 32 | } 33 | 34 | /** Adds a new client to the cache, replacing any existing client for the same origin. */ 35 | public void addClient(Client client) { 36 | clients.put(client.getOrigin(), client); 37 | } 38 | 39 | /** Returns the number of queued reports. */ 40 | public int getQueuedReportCount() { 41 | return queuedReports.size(); 42 | } 43 | 44 | /** Adds a new report to the cache. */ 45 | public void enqueueReport(Report report) { 46 | queuedReports.add(new QueuedReport(report, "nel")); 47 | } 48 | 49 | /** Removes all queued reports older than cutoff. */ 50 | public void removeOldReports(Instant cutoff) { 51 | Iterator iter = queuedReports.iterator(); 52 | while (iter.hasNext()) { 53 | QueuedReport queuedReport = iter.next(); 54 | if (queuedReport.getReport().getTimestamp().isBefore(cutoff)) { 55 | iter.remove(); 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Chooses an endpoint for an origin, taking into account any include-subdomains, 62 | * priority, and weight properties of the endpoints. 63 | * 64 | *

65 | * Returns null if we cannot find any appropriate endpoint for the origin. 66 | *

67 | * 68 | *

69 | * This implements step 3 of the "Send 70 | * reports" algorithm in the Reporting spec. 71 | *

72 | */ 73 | public Endpoint chooseEndpoint(Instant now, Origin origin, String groupName) { 74 | // Loop through all of the clients registered for origin, or any of its superdomains. 75 | for (Client client : clients.getAll(origin)) { 76 | EndpointGroup group = client.getGroup(groupName); 77 | if (group == null) { 78 | // This client has no group with the requested name. 79 | continue; 80 | } 81 | if (client.getOrigin() != origin && !group.includeSubdomains()) { 82 | // This client is for a superdomain of origin, and its group does not have 83 | // include-subdomains set to true; that means we can't use it for the subdomain. 84 | continue; 85 | } 86 | Endpoint endpoint = group.chooseEndpoint(now); 87 | if (endpoint != null) { 88 | return endpoint; 89 | } 90 | } 91 | 92 | // Couldn't find any suitable endpoints! 93 | return null; 94 | } 95 | 96 | private OriginMap clients; 97 | private HashSet queuedReports; 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/nel/Type.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | /** 19 | * Defines the type of 20 | * network error described by a NEL report. 21 | */ 22 | public class Type { 23 | /** The request did not result in a network error. */ 24 | public static Type OK = Type.other("ok"); 25 | 26 | /** DNS server was unreachable. */ 27 | public static Type DNS_UNREACHABLE = Type.other("dns.unreachable"); 28 | /** DNS server responded but was unable to resolve the address. */ 29 | public static Type DNS_NAME_NOT_RESOLVED = Type.other("dns.name_not_resolved"); 30 | /** Request to the DNS server failed due to reasons not covered by previous errors. */ 31 | public static Type DNS_FAILED = Type.other("dns.failed"); 32 | 33 | /** TCP connection to the server timed out. */ 34 | public static Type TCP_TIMED_OUT = Type.other("tcp.timed_out"); 35 | /** The TCP connection was closed by the server. */ 36 | public static Type TCP_CLOSED = Type.other("tcp.closed"); 37 | /** The TCP connection was reset. */ 38 | public static Type TCP_RESET = Type.other("tcp.reset"); 39 | /** The TCP connection was refused by the server. */ 40 | public static Type TCP_REFUSED = Type.other("tcp.refused"); 41 | /** The TCP connection was aborted. */ 42 | public static Type TCP_ABORTED = Type.other("tcp.aborted"); 43 | /** The IP address was invalid. */ 44 | public static Type TCP_ADDRESS_INVALID = Type.other("tcp.address_invalid"); 45 | /** The IP address was unreachable. */ 46 | public static Type TCP_ADDRESS_UNREACHABLE = Type.other("tcp.address_unreachable"); 47 | /** The TCP connection failed due to reasons not covered by previous errors. */ 48 | public static Type TCP_FAILED = Type.other("tcp.failed"); 49 | 50 | /** The TLS connection was aborted due to version or cipher mismatch. */ 51 | public static Type TLS_VERSION_OR_CIPHER_MISMATCH = Type.other("tls.version_or_cipher_mismatch"); 52 | /** The TLS connection was aborted due to invalid client certificate. */ 53 | public static Type TLS_BAD_CLIENT_AUTH_CERT = Type.other("tls.bad_client_auth_cert"); 54 | /** The TLS connection was aborted due to invalid name. */ 55 | public static Type TLS_CERT_NAME_INVALID = Type.other("tls.cert.name_invalid"); 56 | /** The TLS connection was aborted due to invalid certificate date. */ 57 | public static Type TLS_CERT_DATE_INVALID = Type.other("tls.cert.date_invalid"); 58 | /** The TLS connection was aborted due to invalid issuing authority. */ 59 | public static Type TLS_CERT_AUTHORITY_INVALID = Type.other("tls.cert.authority_invalid"); 60 | /** The TLS connection was aborted due to invalid certificate. */ 61 | public static Type TLS_CERT_INVALID = Type.other("tls.cert.invalid"); 62 | /** The TLS connection was aborted due to revoked server certificate. */ 63 | public static Type TLS_CERT_REVOKED = Type.other("tls.cert.revoked"); 64 | /** The TLS connection was aborted due to a key pinning error. */ 65 | public static Type TLS_CERT_PINNED_KEY_NOT_IN_CERT_CHAIN = 66 | Type.other("tls.cert.pinned_key_not_in_cert_chain"); 67 | /** The TLS connection was aborted due to a TLS protocol error. */ 68 | public static Type TLS_PROTOCOL_ERROR = Type.other("tls.protocol.error"); 69 | /** The TLS connection failed due to reasons not covered by previous errors. */ 70 | public static Type TLS_FAILED = Type.other("tls.failed"); 71 | 72 | /** The connection was aborted due to an HTTP protocol error. */ 73 | public static Type HTTP_PROTOCOL_ERROR = Type.other("http.protocol.error"); 74 | /** 75 | * Response was empty, had a content-length mismatch, had improper encoding, and/or other 76 | * conditions that prevented user agent from processing the response. 77 | */ 78 | public static Type HTTP_RESPONSE_INVALID = Type.other("http.response.invalid"); 79 | /** The request was aborted due to a detected redirect loop. */ 80 | public static Type HTTP_RESPONSE_REDIRECT_LOOP = Type.other("http.response.redirect_loop"); 81 | /** The connection failed due to errors in HTTP protocol not covered by previous errors. */ 82 | public static Type HTTP_FAILED = Type.other("http.failed"); 83 | 84 | /** User aborted the resource fetch before it was complete. */ 85 | public static Type ABANDONED = Type.other("abandoned"); 86 | /** Error type is unknown. */ 87 | public static Type UNKNOWN = Type.other("unknown"); 88 | 89 | /** An error not covered by any of the cases listed in the standard. */ 90 | public static Type other(String type) { 91 | return new Type(type); 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return type; 97 | } 98 | 99 | private Type(String type) { 100 | this.type = type; 101 | } 102 | 103 | private String type; 104 | } 105 | -------------------------------------------------------------------------------- /src/main/resources/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 120 | 121 | 122 | 124 | 125 | 126 | 127 | 129 | 130 | 131 | 132 | 134 | 135 | 136 | 137 | 139 | 140 | 141 | 142 | 143 | 145 | 146 | 147 | 148 | 150 | 151 | 152 | 153 | 155 | 156 | 157 | 158 | 160 | 161 | 162 | 163 | 165 | 167 | 169 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /src/test/java/nel/ClientTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertTrue; 20 | 21 | import java.net.MalformedURLException; 22 | import java.net.URL; 23 | import java.util.ArrayList; 24 | 25 | import org.joda.time.Duration; 26 | import org.joda.time.Instant; 27 | import org.junit.Test; 28 | 29 | public class ClientTest { 30 | @Test 31 | public void canParseEndpointGroup() throws InvalidHeaderException, MalformedURLException { 32 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 33 | final Origin origin = new Origin("https", "example.com", 443); 34 | ArrayList headers = new ArrayList(); 35 | // CHECKSTYLE.OFF: OperatorWrap 36 | headers.add( 37 | "{\n" + 38 | " \"group\": \"nel\",\n" + 39 | " \"max-age\":600\n," + 40 | " \"endpoints\": [\n" + 41 | " {\"url\":\"https://example.com/upload\",\"priority\":1,\"weight\":1},\n" + 42 | " {\"url\":\"https://example.com/upload2\",\"priority\":2,\"weight\":3}\n" + 43 | " ]\n" + 44 | "}"); 45 | // CHECKSTYLE.ON: OperatorWrap 46 | Client expected = new Client(origin); 47 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardSeconds(600), I_1300); 48 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload"), 1, 1)); 49 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload2"), 2, 3)); 50 | expected.addGroup(group); 51 | Client actual = Client.parseFromReportToHeader(headers, origin, I_1300); 52 | assertEquals(expected, actual); 53 | } 54 | 55 | private void checkInvalidHeader(String header) 56 | throws InvalidHeaderException, MalformedURLException { 57 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 58 | final Origin origin = new Origin("https", "example.com", 443); 59 | ArrayList headers = new ArrayList(); 60 | headers.add(header); 61 | // If `header` is invalid (either malformed JSON, or incorrect contents as defined by the 62 | // Reporting spec), this method should throw an InvalidHeaderException. 63 | Client.parseFromReportToHeader(headers, origin, I_1300); 64 | } 65 | 66 | @Test(expected = InvalidHeaderException.class) 67 | public void cannotParseMissingUrl() throws InvalidHeaderException, MalformedURLException { 68 | checkInvalidHeader("{\"max-age\":1, \"endpoints\": [{}]}"); 69 | } 70 | 71 | @Test(expected = InvalidHeaderException.class) 72 | public void cannotParseNonStringUrl() throws InvalidHeaderException, MalformedURLException { 73 | checkInvalidHeader("{\"max-age\":1, \"endpoints\": [{\"url\":0}]}"); 74 | } 75 | 76 | @Test(expected = InvalidHeaderException.class) 77 | public void cannotParseInsecureUrl() throws InvalidHeaderException, MalformedURLException { 78 | checkInvalidHeader("{\"max-age\":1, \"endpoints\": [{\"url\":\"http://insecure/\"}]}"); 79 | } 80 | 81 | @Test(expected = InvalidHeaderException.class) 82 | public void cannotParseMissingMaxAge() throws InvalidHeaderException, MalformedURLException { 83 | checkInvalidHeader("{\"endpoints\": [{\"url\":\"https://endpoint/\"}]}"); 84 | } 85 | 86 | @Test(expected = InvalidHeaderException.class) 87 | public void cannotParseNonIntegerMaxAge() throws InvalidHeaderException, MalformedURLException { 88 | checkInvalidHeader("{\"max-age\":\"\", \"endpoints\": [{\"url\":\"https://endpoint/\"}]}"); 89 | } 90 | 91 | @Test(expected = InvalidHeaderException.class) 92 | public void cannotParseNegativeMaxAge() throws InvalidHeaderException, MalformedURLException { 93 | checkInvalidHeader("{\"max-age\":-1, \"endpoints\": [{\"url\":\"https://endpoint/\"}]}"); 94 | } 95 | 96 | @Test(expected = InvalidHeaderException.class) 97 | public void cannotParseNonStringGroup() throws InvalidHeaderException, MalformedURLException { 98 | checkInvalidHeader( 99 | "{\"max-age\":1, \"group\":0, \"endpoints\": [{\"url\":\"https://endpoint/\"}]}"); 100 | } 101 | 102 | @Test(expected = InvalidHeaderException.class) 103 | public void cannotParseNonIntegerPriority() throws InvalidHeaderException, MalformedURLException { 104 | checkInvalidHeader( 105 | "{\"max-age\":1, \"endpoints\": [{\"url\":\"https://endpoint/\",\"priority\":\"\"}]}"); 106 | } 107 | 108 | @Test(expected = InvalidHeaderException.class) 109 | public void cannotParseNonIntegerWeight() throws InvalidHeaderException, MalformedURLException { 110 | checkInvalidHeader( 111 | "{\"max-age\":1, \"endpoints\": [{\"url\":\"https://endpoint/\",\"weight\":\"\"}]}"); 112 | } 113 | 114 | @Test(expected = InvalidHeaderException.class) 115 | public void cannotParseNegativeWeight() throws InvalidHeaderException, MalformedURLException { 116 | checkInvalidHeader( 117 | "{\"max-age\":1, \"endpoints\": [{\"url\":\"https://endpoint/\",\"weight\":-1}]}"); 118 | } 119 | 120 | @Test(expected = InvalidHeaderException.class) 121 | public void cannotParseZeroWeight() throws InvalidHeaderException, MalformedURLException { 122 | checkInvalidHeader( 123 | "{\"max-age\":1, \"endpoints\": [{\"url\":\"https://endpoint/\",\"weight\":0}]}"); 124 | } 125 | 126 | @Test(expected = InvalidHeaderException.class) 127 | public void cannotParseWrappedInList() throws InvalidHeaderException, MalformedURLException { 128 | checkInvalidHeader( 129 | "[{\"max-age\":1, \"endpoints\": [{\"url\":\"https://a/\"}]}," 130 | + "{\"max-age\":1, \"endpoints\": [{\"url\":\"https://b/\"}]}]"); 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/test/java/nel/EndpointGroupTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertTrue; 20 | 21 | import java.net.MalformedURLException; 22 | import java.net.URL; 23 | import java.util.HashMap; 24 | 25 | import org.joda.time.Duration; 26 | import org.joda.time.Instant; 27 | import org.junit.Test; 28 | 29 | public class EndpointGroupTest { 30 | @Test 31 | public void canGetMinPriority() throws MalformedURLException { 32 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 33 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 34 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 35 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/1"), 1, 1)); 36 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/2"), 2, 1)); 37 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/3"), 3, 1)); 38 | assertEquals(1, group.getMinimumPriority(I_1301)); 39 | } 40 | 41 | @Test 42 | public void cannotGetMinPriorityWhenNoEndpoints() throws MalformedURLException { 43 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 44 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 45 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 46 | assertEquals(Integer.MAX_VALUE, group.getMinimumPriority(I_1301)); 47 | } 48 | 49 | @Test 50 | public void minPriorityIgnoresPendingEndpoints() throws MalformedURLException { 51 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 52 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 53 | final Instant I_1401 = Instant.parse("2018-02-20T14:01:00.000Z"); 54 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 55 | Endpoint endpoint = new Endpoint(new URL("https://example.com/upload/1"), 1, 1); 56 | group.addEndpoint(endpoint); 57 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/2"), 2, 1)); 58 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/3"), 3, 1)); 59 | endpoint.recordFailure(I_1401); 60 | // Endpoint 1 is ignored since it's pending. 61 | assertEquals(2, group.getMinimumPriority(I_1301)); 62 | } 63 | 64 | @Test 65 | public void canGetTotalWeight() throws MalformedURLException { 66 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 67 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 68 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 69 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/1"), 1, 1)); 70 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/2"), 1, 2)); 71 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/3"), 1, 3)); 72 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/4"), 2, 4)); 73 | assertEquals(6, group.getTotalWeightForPriority(I_1301, 1)); 74 | assertEquals(4, group.getTotalWeightForPriority(I_1301, 2)); 75 | } 76 | 77 | @Test 78 | public void canGetTotalWeightWhenNoEndpoints() throws MalformedURLException { 79 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 80 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 81 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 82 | assertEquals(0, group.getTotalWeightForPriority(I_1301, 1)); 83 | } 84 | 85 | @Test 86 | public void totalWeightIgnoresPendingEndpoints() throws MalformedURLException { 87 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 88 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 89 | final Instant I_1401 = Instant.parse("2018-02-20T14:01:00.000Z"); 90 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 91 | Endpoint endpoint = new Endpoint(new URL("https://example.com/upload/1"), 1, 1); 92 | group.addEndpoint(endpoint); 93 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/2"), 1, 2)); 94 | group.addEndpoint(new Endpoint(new URL("https://example.com/upload/3"), 1, 3)); 95 | endpoint.recordFailure(I_1401); 96 | // Endpoint 1 is ignored since it's pending. 97 | assertEquals(5, group.getTotalWeightForPriority(I_1301, 1)); 98 | } 99 | 100 | @Test 101 | public void canChooseEndpoint() throws MalformedURLException { 102 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 103 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 104 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 105 | Endpoint endpoint = new Endpoint(new URL("https://example.com/upload")); 106 | group.addEndpoint(endpoint); 107 | // There's only one endpoint to choose from. 108 | assertEquals(endpoint, group.chooseEndpoint(I_1301)); 109 | } 110 | 111 | @Test 112 | public void cannotChooseWhenNoEndpoints() throws MalformedURLException { 113 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 114 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 115 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 116 | // There aren't any endpoints to choose from. 117 | assertEquals(null, group.chooseEndpoint(I_1301)); 118 | } 119 | 120 | @Test 121 | public void cannotChooseFromExpiredGroup() throws MalformedURLException { 122 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 123 | final Instant I_1401 = Instant.parse("2018-02-20T14:01:00.000Z"); 124 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 125 | Endpoint endpoint = new Endpoint(new URL("https://example.com/upload")); 126 | group.addEndpoint(endpoint); 127 | // Using a "now" that is after the group's TTL has expired. 128 | assertEquals(null, group.chooseEndpoint(I_1401)); 129 | } 130 | 131 | @Test 132 | public void chooseEndpointObeysPending() throws MalformedURLException { 133 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 134 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 135 | final Instant I_1401 = Instant.parse("2018-02-20T14:01:00.000Z"); 136 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 137 | Endpoint endpoint = new Endpoint(new URL("https://example.com/upload")); 138 | group.addEndpoint(endpoint); 139 | // Mark the endpoint as having failed, with a retryAfter time far into the future. 140 | endpoint.recordFailure(I_1401); 141 | // Using a "now" that is before the endpoint's retryAfter time means that endpoint isn't eligble 142 | // to be chosen. 143 | assertEquals(null, group.chooseEndpoint(I_1301)); 144 | } 145 | 146 | @Test 147 | public void chooseEndpointObeysPriority() throws MalformedURLException { 148 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 149 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 150 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 151 | Endpoint endpoint1 = new Endpoint(new URL("https://example.com/upload/1"), 1, 1); 152 | Endpoint endpoint2 = new Endpoint(new URL("https://example.com/upload/2"), 2, 1); 153 | group.addEndpoint(endpoint1); 154 | group.addEndpoint(endpoint2); 155 | // We should always pick the endpoint with the lowest priority value. 156 | assertEquals(endpoint1, group.chooseEndpoint(I_1301)); 157 | } 158 | 159 | @Test 160 | public void chooseEndpointObeysPriorityAndPending() throws MalformedURLException { 161 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 162 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 163 | final Instant I_1401 = Instant.parse("2018-02-20T14:01:00.000Z"); 164 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 165 | Endpoint endpoint1 = new Endpoint(new URL("https://example.com/upload/1"), 1, 1); 166 | Endpoint endpoint2 = new Endpoint(new URL("https://example.com/upload/2"), 2, 1); 167 | group.addEndpoint(endpoint1); 168 | group.addEndpoint(endpoint2); 169 | // Mark endpoint 1 as having failed, with a retryAfter time far into the future. 170 | endpoint1.recordFailure(I_1401); 171 | // We should always pick the endpoint 2, even though it has a higher priority value, since 172 | // endpoint 1 is pending. 173 | assertEquals(endpoint2, group.chooseEndpoint(I_1301)); 174 | } 175 | 176 | @Test 177 | public void chooseEndpointObeysWeight() throws MalformedURLException { 178 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 179 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 180 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 181 | Endpoint endpoint1 = new Endpoint(new URL("https://example.com/upload/1"), 1, 1); 182 | Endpoint endpoint2 = new Endpoint(new URL("https://example.com/upload/2"), 1, 2); 183 | group.addEndpoint(endpoint1); 184 | group.addEndpoint(endpoint2); 185 | // Pick an endpoint many many times to exercise the randomness. 186 | HashMap counts = new HashMap(); 187 | final int iterations = 1000; 188 | for (int i = 0; i < iterations; i++) { 189 | Endpoint result = group.chooseEndpoint(I_1301); 190 | Integer count = counts.get(result); 191 | if (count == null) { 192 | counts.put(result, 1); 193 | } else { 194 | counts.put(result, count + 1); 195 | } 196 | } 197 | // We should have gotten each endpoint at least once. 198 | assertTrue(counts.get(endpoint1) > 0); 199 | assertTrue(counts.get(endpoint2) > 0); 200 | // And we should have gotten endpoint 2 roughly twice as many times as endpoint 1, since its 201 | // weight is 2x. 202 | assertEquals(2.0, ((double) counts.get(endpoint2)) / ((double) counts.get(endpoint1)), 0.5); 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /src/test/java/nel/NelPolicyTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertTrue; 20 | 21 | import java.net.URL; 22 | 23 | import org.joda.time.Duration; 24 | import org.joda.time.Instant; 25 | import org.junit.Test; 26 | 27 | public class NelPolicyTest { 28 | @Test 29 | public void canParseNelPolicy() throws InvalidHeaderException { 30 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 31 | final Origin origin = new Origin("https", "example.com", 443); 32 | // CHECKSTYLE.OFF: OperatorWrap 33 | String header = 34 | "{\n" + 35 | " \"report-to\": \"nel\",\n" + 36 | " \"max-age\":600\n" + 37 | "}"; 38 | // CHECKSTYLE.ON: OperatorWrap 39 | NelPolicy expected = 40 | new NelPolicy(origin, "nel", false, 0.0, 1.0, Duration.standardSeconds(600), I_1300); 41 | NelPolicy actual = NelPolicy.parseFromNelHeader(header, origin, I_1300); 42 | assertEquals(expected, actual); 43 | } 44 | 45 | private void checkInvalidHeader(String header) 46 | throws InvalidHeaderException { 47 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 48 | final Origin origin = new Origin("https", "example.com", 443); 49 | // If `header` is invalid (either malformed JSON, or incorrect contents as defined by the 50 | // Reporting spec), this method should throw an InvalidHeaderException. 51 | NelPolicy.parseFromNelHeader(header, origin, I_1300); 52 | } 53 | 54 | @Test(expected = InvalidHeaderException.class) 55 | public void cannotParseMissingMaxAge() throws InvalidHeaderException { 56 | checkInvalidHeader("{\"report-to\": \"nel\"}"); 57 | } 58 | 59 | @Test(expected = InvalidHeaderException.class) 60 | public void cannotParseNonIntegerMaxAge() throws InvalidHeaderException { 61 | checkInvalidHeader("{\"max-age\":\"\", \"report-to\": \"nel\"}"); 62 | } 63 | 64 | @Test(expected = InvalidHeaderException.class) 65 | public void cannotParseNegativeMaxAge() throws InvalidHeaderException { 66 | checkInvalidHeader("{\"max-age\":-1, \"report-to\": \"nel\"}"); 67 | } 68 | 69 | @Test(expected = InvalidHeaderException.class) 70 | public void cannotParseMissingReportTo() throws InvalidHeaderException { 71 | checkInvalidHeader("{\"max-age\":1}"); 72 | } 73 | 74 | @Test(expected = InvalidHeaderException.class) 75 | public void cannotParseNonStringReportTo() throws InvalidHeaderException { 76 | checkInvalidHeader("{\"max-age\":1, \"report-to\":0}"); 77 | } 78 | 79 | @Test(expected = InvalidHeaderException.class) 80 | public void cannotParseNonBooleanIncludeSubdomains() throws InvalidHeaderException { 81 | checkInvalidHeader("{\"max-age\":1, \"report-to\":\"nel\", \"include-subdomains\":\"\"}"); 82 | } 83 | 84 | @Test(expected = InvalidHeaderException.class) 85 | public void cannotParseNonDoubleSuccessFraction() throws InvalidHeaderException { 86 | checkInvalidHeader("{\"max-age\":1, \"report-to\": \"nel\", \"success-fraction\":\"\"}"); 87 | } 88 | 89 | @Test(expected = InvalidHeaderException.class) 90 | public void cannotParseLowSuccessFraction() throws InvalidHeaderException { 91 | checkInvalidHeader("{\"max-age\":1, \"report-to\": \"nel\", \"success-fraction\":-0.5}"); 92 | } 93 | 94 | @Test(expected = InvalidHeaderException.class) 95 | public void cannotParseHighSuccessFraction() throws InvalidHeaderException { 96 | checkInvalidHeader("{\"max-age\":1, \"report-to\": \"nel\", \"success-fraction\":1.5}"); 97 | } 98 | 99 | @Test(expected = InvalidHeaderException.class) 100 | public void cannotParseNonDoubleFailureFraction() throws InvalidHeaderException { 101 | checkInvalidHeader("{\"max-age\":1, \"report-to\": \"nel\", \"failure-fraction\":\"\"}"); 102 | } 103 | 104 | @Test(expected = InvalidHeaderException.class) 105 | public void cannotParseLowFailureFraction() throws InvalidHeaderException { 106 | checkInvalidHeader("{\"max-age\":1, \"report-to\": \"nel\", \"failure-fraction\":-0.5}"); 107 | } 108 | 109 | @Test(expected = InvalidHeaderException.class) 110 | public void cannotParseHighFailureFraction() throws InvalidHeaderException { 111 | checkInvalidHeader("{\"max-age\":1, \"report-to\": \"nel\", \"failure-fraction\":1.5}"); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/test/java/nel/OriginMapTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.Iterator; 23 | 24 | import org.junit.Test; 25 | 26 | public class OriginMapTest { 27 | private ArrayList list(Iterable iterable) { 28 | ArrayList result = new ArrayList(); 29 | for (V element : iterable) { 30 | result.add(element); 31 | } 32 | return result; 33 | } 34 | 35 | @Test 36 | public void canGet() { 37 | final Origin origin = new Origin("https", "example.com", 443); 38 | OriginMap map = new OriginMap(); 39 | map.put(origin, "test"); 40 | assertEquals(Arrays.asList("test"), list(map.getAll(origin))); 41 | } 42 | 43 | @Test 44 | public void canGetForSubdomain() { 45 | final Origin origin = new Origin("https", "foo.example.com", 443); 46 | final Origin superdomainOrigin = new Origin("https", "example.com", 443); 47 | OriginMap map = new OriginMap(); 48 | map.put(superdomainOrigin, "test"); 49 | assertEquals(Arrays.asList("test"), list(map.getAll(origin))); 50 | } 51 | 52 | @Test 53 | public void canGetForSubsubdomain() { 54 | final Origin origin = new Origin("https", "bar.foo.example.com", 443); 55 | final Origin supersuperdomainOrigin = new Origin("https", "example.com", 443); 56 | OriginMap map = new OriginMap(); 57 | map.put(supersuperdomainOrigin, "test"); 58 | assertEquals(Arrays.asList("test"), list(map.getAll(origin))); 59 | } 60 | 61 | @Test 62 | public void canGetAllForSubdomain() { 63 | final Origin origin = new Origin("https", "foo.example.com", 443); 64 | final Origin superdomainOrigin = new Origin("https", "example.com", 443); 65 | OriginMap map = new OriginMap(); 66 | map.put(superdomainOrigin, "test"); 67 | map.put(origin, "test2"); 68 | assertEquals(Arrays.asList("test2", "test"), list(map.getAll(origin))); 69 | } 70 | 71 | @Test 72 | public void canGetAllForSubsubdomain() { 73 | final Origin origin = new Origin("https", "bar.foo.example.com", 443); 74 | final Origin superdomainOrigin = new Origin("https", "foo.example.com", 443); 75 | final Origin supersuperdomainOrigin = new Origin("https", "example.com", 443); 76 | OriginMap map = new OriginMap(); 77 | map.put(supersuperdomainOrigin, "test1"); 78 | map.put(superdomainOrigin, "test2"); 79 | map.put(origin, "test3"); 80 | assertEquals(Arrays.asList("test3", "test2", "test1"), list(map.getAll(origin))); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/nel/OriginTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | import org.junit.Test; 21 | 22 | public class OriginTest { 23 | @Test 24 | public void canGetSuperdomainOrigin() { 25 | assertEquals(new Origin("https", "example.com", 443), 26 | new Origin("https", "foo.example.com", 443).getSuperdomainOrigin()); 27 | assertEquals(new Origin("https", "com", 443), 28 | new Origin("https", "example.com", 443).getSuperdomainOrigin()); 29 | assertEquals(null, new Origin("https", "com", 443).getSuperdomainOrigin()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/nel/ReportTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | import java.net.URISyntaxException; 21 | 22 | import org.joda.time.Duration; 23 | import org.joda.time.Instant; 24 | import org.junit.Test; 25 | 26 | public class ReportTest { 27 | @Test 28 | public void canBuildReports() { 29 | Report report = new Report() 30 | .setTimestamp(Instant.parse("2018-02-20T13:00:00.000Z")) 31 | .setUri("https://example.com") 32 | .setSamplingFraction(0.5) 33 | .setServerIp("192.0.2.24") 34 | .setProtocol("h2") 35 | .setStatusCode(200) 36 | .setElapsedTime(Duration.millis(1000)) 37 | .setType(Type.OK); 38 | assertEquals( 39 | // CHECKSTYLE.OFF: OperatorWrap 40 | "{\n" + 41 | " \"age\": 200,\n" + 42 | " \"type\": \"network-error\",\n" + 43 | " \"url\": \"https://example.com\",\n" + 44 | " \"body\": {\n" + 45 | " \"uri\": \"https://example.com\",\n" + 46 | " \"sampling-fraction\": 0.5,\n" + 47 | " \"server-ip\": \"192.0.2.24\",\n" + 48 | " \"protocol\": \"h2\",\n" + 49 | " \"status-code\": 200,\n" + 50 | " \"elapsed-time\": 1000,\n" + 51 | " \"type\": \"ok\"\n" + 52 | " }\n" + 53 | "}", 54 | // CHECKSTYLE.ON: OperatorWrap 55 | report.toString(Instant.parse("2018-02-20T13:00:00.200Z"))); 56 | } 57 | 58 | @Test 59 | public void removesUriFragments() { 60 | Report report = new Report() 61 | .setTimestamp(Instant.parse("2018-02-20T13:00:00.000Z")) 62 | .setUri("https://example.com#fragment") 63 | .setSamplingFraction(0.5) 64 | .setServerIp("192.0.2.24") 65 | .setProtocol("h2") 66 | .setStatusCode(200) 67 | .setElapsedTime(Duration.millis(1000)) 68 | .setType(Type.OK); 69 | assertEquals( 70 | // CHECKSTYLE.OFF: OperatorWrap 71 | "{\n" + 72 | " \"age\": 200,\n" + 73 | " \"type\": \"network-error\",\n" + 74 | " \"url\": \"https://example.com\",\n" + 75 | " \"body\": {\n" + 76 | " \"uri\": \"https://example.com\",\n" + 77 | " \"sampling-fraction\": 0.5,\n" + 78 | " \"server-ip\": \"192.0.2.24\",\n" + 79 | " \"protocol\": \"h2\",\n" + 80 | " \"status-code\": 200,\n" + 81 | " \"elapsed-time\": 1000,\n" + 82 | " \"type\": \"ok\"\n" + 83 | " }\n" + 84 | "}", 85 | // CHECKSTYLE.ON: OperatorWrap 86 | report.toString(Instant.parse("2018-02-20T13:00:00.200Z"))); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/nel/ReportingCacheTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google LLC 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * https://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package nel; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | import java.net.MalformedURLException; 21 | import java.net.URL; 22 | 23 | import org.joda.time.Duration; 24 | import org.joda.time.Instant; 25 | import org.junit.Test; 26 | 27 | public class ReportingCacheTest { 28 | @Test 29 | public void canEnqueueReports() throws MalformedURLException { 30 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 31 | ReportingCache cache = new ReportingCache(); 32 | cache.enqueueReport(new Report() 33 | .setTimestamp(I_1300) 34 | .setUri("https://example.com") 35 | .setSamplingFraction(0.5) 36 | .setServerIp("192.0.2.24") 37 | .setProtocol("h2") 38 | .setStatusCode(200) 39 | .setElapsedTime(Duration.millis(1000)) 40 | .setType(Type.OK)); 41 | assertEquals(1, cache.getQueuedReportCount()); 42 | } 43 | 44 | @Test 45 | public void canRemoveOldReports() throws MalformedURLException { 46 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 47 | final Instant I_1330 = Instant.parse("2018-02-20T13:30:00.000Z"); 48 | final Instant I_1400 = Instant.parse("2018-02-20T14:00:00.000Z"); 49 | ReportingCache cache = new ReportingCache(); 50 | // This report will be removed 51 | cache.enqueueReport(new Report() 52 | .setTimestamp(I_1300) 53 | .setUri("https://example.com") 54 | .setSamplingFraction(0.5) 55 | .setServerIp("192.0.2.24") 56 | .setProtocol("h2") 57 | .setStatusCode(200) 58 | .setElapsedTime(Duration.millis(1000)) 59 | .setType(Type.OK)); 60 | // And this one won't 61 | cache.enqueueReport(new Report() 62 | .setTimestamp(I_1400) 63 | .setUri("https://example.com") 64 | .setSamplingFraction(0.5) 65 | .setServerIp("192.0.2.24") 66 | .setProtocol("h2") 67 | .setStatusCode(200) 68 | .setElapsedTime(Duration.millis(1000)) 69 | .setType(Type.OK)); 70 | cache.removeOldReports(I_1330); 71 | assertEquals(1, cache.getQueuedReportCount()); 72 | } 73 | 74 | @Test 75 | public void canChooseEndpoint() throws MalformedURLException { 76 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 77 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 78 | final Origin origin = new Origin("https", "example.com", 443); 79 | final Client client = new Client(origin); 80 | ReportingCache cache = new ReportingCache(); 81 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 82 | Endpoint endpoint = new Endpoint(new URL("https://example.com/upload")); 83 | group.addEndpoint(endpoint); 84 | client.addGroup(group); 85 | cache.addClient(client); 86 | // There's only one endpoint to choose from. 87 | assertEquals(endpoint, cache.chooseEndpoint(I_1301, origin, "nel")); 88 | } 89 | 90 | @Test 91 | public void canChooseSuperdomainEndpoint() throws MalformedURLException { 92 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 93 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 94 | final Origin origin = new Origin("https", "foo.example.com", 443); 95 | final Origin superdomainOrigin = new Origin("https", "example.com", 443); 96 | ReportingCache cache = new ReportingCache(); 97 | Client client = new Client(superdomainOrigin); 98 | EndpointGroup group = new EndpointGroup("nel", true, Duration.standardHours(1), I_1300); 99 | Endpoint endpoint = new Endpoint(new URL("https://example.com/upload")); 100 | group.addEndpoint(endpoint); 101 | client.addGroup(group); 102 | cache.addClient(client); 103 | // There's only one endpoint to choose from. 104 | assertEquals(endpoint, cache.chooseEndpoint(I_1301, origin, "nel")); 105 | } 106 | 107 | @Test 108 | public void chooseEndpointObeysIncludeSubdomains() throws MalformedURLException { 109 | final Instant I_1300 = Instant.parse("2018-02-20T13:00:00.000Z"); 110 | final Instant I_1301 = Instant.parse("2018-02-20T13:01:00.000Z"); 111 | final Origin origin = new Origin("https", "foo.example.com", 443); 112 | final Origin superdomainOrigin = new Origin("https", "example.com", 443); 113 | ReportingCache cache = new ReportingCache(); 114 | Client client = new Client(superdomainOrigin); 115 | EndpointGroup group = new EndpointGroup("nel", false, Duration.standardHours(1), I_1300); 116 | Endpoint endpoint = new Endpoint(new URL("https://example.com/upload")); 117 | group.addEndpoint(endpoint); 118 | client.addGroup(group); 119 | cache.addClient(client); 120 | // The superdomain endpoint group has include-subdomains = false, so it's not eligible. 121 | assertEquals(null, cache.chooseEndpoint(I_1301, origin, "nel")); 122 | } 123 | 124 | } 125 | --------------------------------------------------------------------------------