Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,33 @@ a relative path like `/my-app` cannot be used. If the application also sets
instead of prepending the context path or `X-Forwarded-Prefix`.

When `adk.web.backend-url` is unset, `/` and `/dev-ui` redirect to `/dev-ui/`
and the bundled `backendUrl` value is preserved. If you instead enable
`server.forward-headers-strategy=framework`, make sure your reverse proxy strips
or overwrites incoming `Forwarded` and `X-Forwarded-*` headers from untrusted
clients.
and the bundled `backendUrl` value is preserved.

The server applies `Forwarded` and `X-Forwarded-*` headers to the request URL by
default, as with `server.forward-headers-strategy=framework`. Make sure your
reverse proxy strips or overwrites these headers from untrusted clients. Set
`server.forward-headers-strategy=none` to ignore them, or `native` to let the
web server handle them instead. Behind a proxy that terminates TLS, keep them
on: with `none`, a page served over plain HTTP under the same host name passes
the same-origin check.

## Cross-origin requests

By default, the REST API and the `/run_live` WebSocket accept only same-origin
requests. The dev UI served by the server itself is same-origin and needs no
configuration.

To allow other origins, for example a UI served by a separate development
server, set `adk.web.cors.origins` to a comma-separated list of origins:

```properties
adk.web.cors.origins=http://localhost:4200
```

Behind a reverse proxy, the same-origin check uses the URL that the proxy
reports in forwarded headers, so the dev UI keeps working behind a proxy that
terminates TLS. A proxy on a non-default port must include the port in its
forwarded headers. List the external origin in `adk.web.cors.origins` if the
proxy does not report the external URL, or if it reports `ws` or `wss` as the
scheme of WebSocket requests (as Traefik does). Avoid `*`: it lets any website
open in your browser call the server.
39 changes: 35 additions & 4 deletions dev/src/main/java/com/google/adk/web/config/AdkWebCorsConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@

package com.google.adk.web.config;

import jakarta.servlet.DispatcherType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.filter.ForwardedHeaderFilter;

/**
* Configuration class for setting up Cross-Origin Resource Sharing (CORS) in the ADK Web
Expand All @@ -39,6 +46,8 @@
* <li>{@link CorsConfigurationSource}: Configures CORS settings such as allowed origins, methods,
* headers, credentials, and max age.
* <li>{@link CorsFilter}: Applies the CORS configuration to incoming requests.
* <li>{@link ForwardedHeaderFilter}: Applies reverse proxy headers to the URL of every request,
* so the same-origin checks see the URL the browser used.
* </ul>
*/
@Configuration
Expand All @@ -50,10 +59,10 @@ public class AdkWebCorsConfig {
public CorsConfigurationSource corsConfigurationSource(AdkWebCorsProperties corsProperties) {
if (corsProperties.origins().contains("*")) {
logger.warn(
"CORS is configured to allow all origins (\"*\"), which is insecure and intended for"
+ " local development only. This also applies to the /run_live WebSocket endpoint."
+ " Set 'adk.web.cors.origins' to an explicit allowlist to restrict which origins"
+ " may call the server.");
"CORS is configured to allow all origins (\"*\"), so any website open in a browser can"
+ " call this server, including the /run_live WebSocket endpoint. Set"
+ " 'adk.web.cors.origins' to an explicit allowlist to restrict which origins may"
+ " call the server.");
}

CorsConfiguration configuration = new CorsConfiguration();
Expand All @@ -74,4 +83,26 @@ public CorsConfigurationSource corsConfigurationSource(AdkWebCorsProperties cors
public CorsFilter corsFilter(CorsConfigurationSource corsConfigurationSource) {
return new CorsFilter(corsConfigurationSource);
}

/**
* Makes {@code server.forward-headers-strategy=framework} the default, so same-origin checks see
* the URL the browser used behind a TLS-terminating proxy. Registered like Spring Boot's own
* filter, which then backs off; set the property to {@code native} or {@code none} to opt out.
*/
@Bean
@ConditionalOnProperty(
name = "server.forward-headers-strategy",
havingValue = "framework",
matchIfMissing = true)
FilterRegistrationBean<ForwardedHeaderFilter> adkForwardedHeaderFilter(
ObjectProvider<ForwardedHeaderFilterCustomizer> customizers) {
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
customizers.orderedStream().forEach(customizer -> customizer.customize(filter));
FilterRegistrationBean<ForwardedHeaderFilter> registration =
new FilterRegistrationBean<>(filter);
registration.setDispatcherTypes(
DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR);
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@

/**
* Properties for configuring CORS in ADK Web. This class is used to load CORS settings from
* application properties.
* application properties. {@code origins} defaults to an empty list, which allows only same-origin
* requests.
*/
@ConfigurationProperties(prefix = "adk.web.cors")
public record AdkWebCorsProperties(
Expand All @@ -34,7 +35,7 @@ public record AdkWebCorsProperties(

public AdkWebCorsProperties {
mapping = mapping != null ? mapping : "/**";
origins = origins != null && !origins.isEmpty() ? origins : List.of("*");
origins = origins != null ? origins : List.of();
methods =
methods != null && !methods.isEmpty()
? methods
Expand Down
Loading
Loading