Skip to content

Commit 65a2db7

Browse files
Adrian Coleadriancole
Adrian Cole
authored andcommitted
Moves CORS processing to top of http stack
1 parent 18c04a5 commit 65a2db7

File tree

6 files changed

+378
-29
lines changed

6 files changed

+378
-29
lines changed

zipkin-server/pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
<dependency>
6464
<groupId>org.springframework.boot</groupId>
6565
<artifactId>spring-boot-starter-undertow</artifactId>
66-
<optional>true</optional>
6766
</dependency>
6867

6968
<!-- zipkin requires exporting /health endpoint -->
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Copyright 2015-2017 The OpenZipkin Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package zipkin.server;
15+
16+
import io.undertow.server.HandlerWrapper;
17+
import io.undertow.server.HttpHandler;
18+
import io.undertow.server.HttpServerExchange;
19+
import io.undertow.util.HeaderMap;
20+
import io.undertow.util.HttpString;
21+
import java.util.Arrays;
22+
import java.util.List;
23+
import java.util.logging.Level;
24+
import java.util.logging.Logger;
25+
26+
/**
27+
* Inspired by Netty's CorsHandler and driven by tests in https://github.com/rs/cors
28+
*
29+
* <p>This implementation simplified based on needs of the zipkin UI. For example, we don't need
30+
* sophisticated expressions, nor do we need to send back non-default response headers
31+
* (Access-Control-Expose-Headers). Finally, we don't have authorization
32+
* (Access-Control-Allow-Credentials) at the moment. When these assumptions change, we can complete
33+
* the implementation.
34+
*/
35+
36+
final class CorsHandler implements HttpHandler, HandlerWrapper {
37+
private static Logger logger = Logger.getLogger(CorsHandler.class.getName());
38+
39+
static final HttpString
40+
OPTIONS = HttpString.tryFromString("OPTIONS"),
41+
ORIGIN = HttpString.tryFromString("origin"),
42+
VARY = HttpString.tryFromString("vary"),
43+
ACCESS_CONTROL_ALLOW_METHODS = HttpString.tryFromString("access-control-allow-methods"),
44+
ACCESS_CONTROL_ALLOW_HEADERS = HttpString.tryFromString("access-control-allow-headers"),
45+
ACCESS_CONTROL_ALLOW_ORIGIN = HttpString.tryFromString("access-control-allow-origin"),
46+
ACCESS_CONTROL_REQUEST_METHOD = HttpString.tryFromString("access-control-request-method"),
47+
ACCESS_CONTROL_REQUEST_HEADERS = HttpString.tryFromString("access-control-request-headers");
48+
49+
final List<String> allowedOrigins;
50+
final List<String> allowedHeaders;
51+
final boolean wildcardOrigin;
52+
HttpHandler next;
53+
54+
CorsHandler(String allowedOrigins) {
55+
this.allowedOrigins = Arrays.asList(allowedOrigins.split(","));
56+
this.allowedHeaders = Arrays.asList(
57+
"accept",
58+
"content-type",
59+
"content-encoding",
60+
"origin"
61+
);
62+
this.wildcardOrigin = this.allowedOrigins.contains("*");
63+
}
64+
65+
@Override public void handleRequest(HttpServerExchange exchange) throws Exception {
66+
if (isPreflightRequest(exchange)) {
67+
handlePreflight(exchange);
68+
exchange.getResponseSender().close();
69+
return;
70+
}
71+
72+
if (!validateOrigin(exchange)) {
73+
exchange.setStatusCode(403).getResponseSender().send("CORS error\n");
74+
return;
75+
}
76+
77+
next.handleRequest(exchange);
78+
}
79+
80+
/** Statically allows headers used by the api */
81+
void handlePreflight(HttpServerExchange exchange) {
82+
HeaderMap requestHeaders = exchange.getRequestHeaders();
83+
String origin = requestHeaders.getFirst(ORIGIN);
84+
String method = requestHeaders.getFirst(ACCESS_CONTROL_REQUEST_METHOD);
85+
String requestedHeaders = requestHeaders.getFirst(ACCESS_CONTROL_REQUEST_HEADERS);
86+
HeaderMap responseHeaders = exchange.getResponseHeaders();
87+
88+
responseHeaders.put(VARY,
89+
"origin,access-control-request-method,access-control-request-headers");
90+
if (
91+
("POST".equals(method) || "GET".equals(method))
92+
&& requestedHeadersAllowed(requestedHeaders)
93+
&& setOrigin(origin, responseHeaders)
94+
) {
95+
responseHeaders.put(ACCESS_CONTROL_ALLOW_METHODS, method);
96+
if (requestedHeaders != null) {
97+
responseHeaders.put(ACCESS_CONTROL_ALLOW_HEADERS, requestedHeaders);
98+
}
99+
}
100+
}
101+
102+
boolean requestedHeadersAllowed(String requestedHeaders) {
103+
if (requestedHeaders == null) return true;
104+
StringBuilder next = new StringBuilder();
105+
for (int i = 0, length = requestedHeaders.length(); i < length; i++) {
106+
char c = requestedHeaders.charAt(i);
107+
if (c == ' ') continue;
108+
if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; // lowercase
109+
if (c != ',') next.append(c);
110+
if (c == ',' || i + 1 == length) {
111+
String toTest = next.toString();
112+
if (!allowedHeaders.contains(toTest)) {
113+
if (logger.isLoggable(Level.FINE)) {
114+
logger.fine(toTest + " is not an allowed header: " + allowedHeaders);
115+
}
116+
return false;
117+
}
118+
next.setLength(0);
119+
}
120+
}
121+
return true;
122+
}
123+
124+
boolean validateOrigin(HttpServerExchange exchange) {
125+
HeaderMap responseHeaders = exchange.getResponseHeaders();
126+
responseHeaders.put(VARY, "origin");
127+
String origin = exchange.getRequestHeaders().getFirst(ORIGIN);
128+
if (origin == null) return true; // just vary
129+
return setOrigin(origin, responseHeaders);
130+
}
131+
132+
private static boolean isPreflightRequest(HttpServerExchange exchange) {
133+
HeaderMap headers = exchange.getRequestHeaders();
134+
return exchange.getRequestMethod().equals(OPTIONS) &&
135+
headers.contains(ORIGIN) && headers.contains(ACCESS_CONTROL_REQUEST_METHOD);
136+
}
137+
138+
private boolean setOrigin(String origin, HeaderMap responseHeaders) {
139+
if ("null".equals(origin)) {
140+
responseHeaders.put(ACCESS_CONTROL_ALLOW_ORIGIN, "null");
141+
return true;
142+
}
143+
if (wildcardOrigin) {
144+
responseHeaders.put(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
145+
return true;
146+
} else if (allowedOrigins.contains(origin)) {
147+
responseHeaders.put(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
148+
return true;
149+
}
150+
if (logger.isLoggable(Level.FINE)) {
151+
logger.fine(origin + " is not an allowed origin: " + allowedOrigins);
152+
}
153+
return false;
154+
}
155+
156+
@Override public HttpHandler wrap(HttpHandler handler) {
157+
this.next = handler;
158+
return this;
159+
}
160+
}

zipkin-server/src/main/java/zipkin/server/ZipkinServerConfiguration.java

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
package zipkin.server;
1515

1616
import com.github.kristofa.brave.Brave;
17-
import java.util.Arrays;
1817
import java.util.Optional;
1918
import org.springframework.beans.factory.annotation.Autowired;
2019
import org.springframework.beans.factory.annotation.Value;
@@ -23,24 +22,20 @@
2322
import org.springframework.boot.actuate.metrics.buffer.CounterBuffers;
2423
import org.springframework.boot.actuate.metrics.buffer.GaugeBuffers;
2524
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
25+
import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory;
2626
import org.springframework.context.annotation.Bean;
2727
import org.springframework.context.annotation.Condition;
2828
import org.springframework.context.annotation.ConditionContext;
2929
import org.springframework.context.annotation.Conditional;
3030
import org.springframework.context.annotation.Configuration;
3131
import org.springframework.core.type.AnnotatedTypeMetadata;
32-
import org.springframework.web.cors.CorsConfiguration;
33-
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
34-
import org.springframework.web.filter.CorsFilter;
3532
import zipkin.collector.CollectorMetrics;
3633
import zipkin.collector.CollectorSampler;
3734
import zipkin.internal.V2StorageComponent;
3835
import zipkin.server.brave.TracedStorageComponent;
3936
import zipkin.storage.StorageComponent;
4037
import zipkin2.storage.InMemoryStorage;
4138

42-
import static java.util.Arrays.asList;
43-
4439
@Configuration
4540
public class ZipkinServerConfiguration {
4641

@@ -49,6 +44,17 @@ public class ZipkinServerConfiguration {
4944
return new ZipkinHealthIndicator(healthAggregator);
5045
}
5146

47+
@Bean public UndertowEmbeddedServletContainerFactory embeddedServletContainerFactory(
48+
@Value("${zipkin.query.allowed-origins:*}") String allowedOrigins
49+
) {
50+
UndertowEmbeddedServletContainerFactory factory = new UndertowEmbeddedServletContainerFactory();
51+
CorsHandler cors = new CorsHandler(allowedOrigins);
52+
factory.addDeploymentInfoCustomizers(
53+
info -> info.addInitialHandlerChainWrapper(cors)
54+
);
55+
return factory;
56+
}
57+
5258
@Bean
5359
@ConditionalOnMissingBean(CollectorSampler.class)
5460
CollectorSampler traceIdSampler(@Value("${zipkin.collector.sample-rate:1.0}") float rate) {
@@ -57,14 +63,15 @@ CollectorSampler traceIdSampler(@Value("${zipkin.collector.sample-rate:1.0}") fl
5763

5864
@Bean
5965
@ConditionalOnMissingBean(CollectorMetrics.class)
60-
CollectorMetrics metrics(Optional<CounterBuffers> counterBuffers, Optional<GaugeBuffers> gaugeBuffers) {
66+
CollectorMetrics metrics(Optional<CounterBuffers> counterBuffers,
67+
Optional<GaugeBuffers> gaugeBuffers) {
6168
// it is not guaranteed that BufferCounterService/CounterBuffers will be used,
6269
// for ex., com.datastax.cassandra:cassandra-driver-core brings com.codahale.metrics.MetricRegistry
6370
// and as result DropwizardMetricServices is getting instantiated instead of standard Java8 BufferCounterService.
6471
// On top of it Cassandra driver heavily relies on Dropwizard metrics and manually excluding it from pom.xml is not an option.
6572
// MetricsDropwizardAutoConfiguration can be manually excluded either, as Cassandra metrics won't be recorded.
6673
return new ActuateCollectorMetrics(counterBuffers.orElse(new CounterBuffers()),
67-
gaugeBuffers.orElse(new GaugeBuffers()));
74+
gaugeBuffers.orElse(new GaugeBuffers()));
6875
}
6976

7077
@Configuration
@@ -89,19 +96,6 @@ public Object postProcessAfterInitialization(Object bean, String beanName) {
8996
}
9097
}
9198

92-
@Bean
93-
@ConditionalOnMissingBean(CorsFilter.class)
94-
CorsFilter corsFilter(@Value("${zipkin.query.allowed-origins:*}") String allowedOrigins) {
95-
CorsConfiguration configuration = new CorsConfiguration();
96-
configuration.setAllowedOrigins(asList(allowedOrigins.split(",")));
97-
configuration.setAllowedMethods(asList("GET", "POST"));
98-
configuration.setAllowCredentials(false);
99-
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
100-
source.registerCorsConfiguration("/api/**", configuration);
101-
source.registerCorsConfiguration("/zipkin/api/**", configuration);
102-
return new CorsFilter(source);
103-
}
104-
10599
/**
106100
* This is a special-case configuration if there's no StorageComponent of any kind. In-Mem can
107101
* supply both read apis, so we add two beans here.
@@ -128,7 +122,7 @@ static final class StorageTypeMemAbsentOrEmpty implements Condition {
128122
@Override public boolean matches(ConditionContext condition, AnnotatedTypeMetadata ignored) {
129123
String storageType = condition.getEnvironment().getProperty("zipkin.storage.type");
130124
if (storageType == null) return true;
131-
storageType = storageType.trim();
125+
storageType = storageType.trim();
132126
if (storageType.isEmpty()) return true;
133127
return storageType.equals("mem");
134128
}

0 commit comments

Comments
 (0)