origin
. */
31 | public Client(Origin origin) {
32 | this.origin = origin;
33 | this.groups = new HashMapReport-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(Listnull
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 HashMapurl
. */
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 ArrayListpriority
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(Listnow
. */
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 ArrayListReport-To
headers as defined by the Reporting spec.
34 | */
35 | public class EndpointGroupJsonAdapter extends TypeAdapternow
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 | ArrayListReport-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 TypeAdapterorigin
, 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 givenscheme
, 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 OriginMapgroup
.
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 | *
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 OriginMapcutoff
. */
50 | public void removeOldReports(Instant cutoff) {
51 | Iteratorinclude-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 | *
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