Skip to content

Commit 6f81b4e

Browse files
authored
Merge pull request #2935 from murgatroid99/grpc-js-xds_server_http_filters
grpc-js-xds: Add support for server http filters
2 parents c4580fa + ff679ae commit 6f81b4e

File tree

6 files changed

+104
-28
lines changed

6 files changed

+104
-28
lines changed

packages/grpc-js-xds/src/http-filter.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
// This is a non-public, unstable API, but it's very convenient
1818
import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util';
19-
import { experimental, logVerbosity } from '@grpc/grpc-js';
19+
import { experimental, logVerbosity, ServerInterceptor } from '@grpc/grpc-js';
2020
import { Any__Output } from './generated/google/protobuf/Any';
2121
import Filter = experimental.Filter;
2222
import FilterFactory = experimental.FilterFactory;
@@ -64,7 +64,8 @@ export interface HttpFilterFactoryConstructor<FilterType extends Filter> {
6464
export interface HttpFilterRegistryEntry {
6565
parseTopLevelFilterConfig(encodedConfig: Any__Output): HttpFilterConfig | null;
6666
parseOverrideFilterConfig(encodedConfig: Any__Output): HttpFilterConfig | null;
67-
httpFilterConstructor: HttpFilterFactoryConstructor<Filter>;
67+
httpFilterConstructor?: HttpFilterFactoryConstructor<Filter> | undefined;
68+
createServerFilter?: ((config: HttpFilterConfig, overrideConfigMap: Map<string, HttpFilterConfig>) => ServerInterceptor) | undefined;
6869
}
6970

7071
const FILTER_REGISTRY = new Map<string, HttpFilterRegistryEntry>();
@@ -106,7 +107,7 @@ export function getTopLevelFilterUrl(encodedConfig: Any__Output): string {
106107
}
107108
}
108109

109-
export function validateTopLevelFilter(httpFilter: HttpFilter__Output): boolean {
110+
export function validateTopLevelFilter(httpFilter: HttpFilter__Output, client: boolean): boolean {
110111
if (!httpFilter.typed_config) {
111112
trace(httpFilter.name + ' validation failed: typed_config unset');
112113
return false;
@@ -121,6 +122,17 @@ export function validateTopLevelFilter(httpFilter: HttpFilter__Output): boolean
121122
}
122123
const registryEntry = FILTER_REGISTRY.get(typeUrl);
123124
if (registryEntry) {
125+
if (!httpFilter.is_optional) {
126+
if (client) {
127+
if (!registryEntry.httpFilterConstructor) {
128+
return false;
129+
}
130+
} else {
131+
if (!registryEntry.createServerFilter) {
132+
return false;
133+
}
134+
}
135+
}
124136
const parsedConfig = registryEntry.parseTopLevelFilterConfig(encodedConfig);
125137
if (parsedConfig === null) {
126138
trace(httpFilter.name + ' validation failed: config parsing failed');
@@ -185,7 +197,7 @@ export function validateOverrideFilter(encodedConfig: Any__Output): boolean {
185197
}
186198
}
187199

188-
export function parseTopLevelFilterConfig(encodedConfig: Any__Output) {
200+
export function parseTopLevelFilterConfig(encodedConfig: Any__Output, client: boolean) {
189201
let typeUrl: string;
190202
try {
191203
typeUrl = getTopLevelFilterUrl(encodedConfig);
@@ -194,6 +206,15 @@ export function parseTopLevelFilterConfig(encodedConfig: Any__Output) {
194206
}
195207
const registryEntry = FILTER_REGISTRY.get(typeUrl);
196208
if (registryEntry) {
209+
if (client) {
210+
if (!registryEntry.httpFilterConstructor) {
211+
return null;
212+
}
213+
} else {
214+
if (!registryEntry.createServerFilter) {
215+
return null;
216+
}
217+
}
197218
return registryEntry.parseTopLevelFilterConfig(encodedConfig);
198219
} else {
199220
// Filter type URL not found in registry
@@ -236,11 +257,20 @@ export function parseOverrideFilterConfig(encodedConfig: Any__Output) {
236257
}
237258
}
238259

239-
export function createHttpFilter(config: HttpFilterConfig, overrideConfig?: HttpFilterConfig): FilterFactory<Filter> | null {
260+
export function createClientHttpFilter(config: HttpFilterConfig, overrideConfig?: HttpFilterConfig): FilterFactory<Filter> | null {
240261
const registryEntry = FILTER_REGISTRY.get(config.typeUrl);
241-
if (registryEntry) {
262+
if (registryEntry && registryEntry.httpFilterConstructor) {
242263
return new registryEntry.httpFilterConstructor(config, overrideConfig);
243264
} else {
244265
return null;
245266
}
246267
}
268+
269+
export function createServerHttpFilter(config: HttpFilterConfig, overrideConfigMap: Map<string, HttpFilterConfig>): ServerInterceptor | null {
270+
const registryEntry = FILTER_REGISTRY.get(config.typeUrl);
271+
if (registryEntry && registryEntry.createServerFilter) {
272+
return registryEntry.createServerFilter(config, overrideConfigMap);
273+
} else {
274+
return null;
275+
}
276+
}

packages/grpc-js-xds/src/http-filter/fault-injection-filter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ function asyncTimeout(timeMs: number): Promise<void> {
213213

214214
/**
215215
* Returns true with probability numerator/denominator.
216-
* @param numerator
217-
* @param denominator
216+
* @param numerator
217+
* @param denominator
218218
*/
219219
function rollRandomPercentage(numerator: number, denominator: number): boolean {
220220
return Math.random() * denominator < numerator;
@@ -344,4 +344,4 @@ export function setup() {
344344
parseOverrideFilterConfig: parseHTTPFaultConfig,
345345
httpFilterConstructor: FaultInjectionFilterFactory
346346
});
347-
}
347+
}

packages/grpc-js-xds/src/http-filter/router-filter.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { experimental } from '@grpc/grpc-js';
17+
import { experimental, ServerInterceptingCall, ServerInterceptor } from '@grpc/grpc-js';
1818
import { Any__Output } from '../generated/google/protobuf/Any';
1919
import { HttpFilterConfig, registerHttpFilter } from '../http-filter';
2020
import Filter = experimental.Filter;
@@ -31,6 +31,21 @@ class RouterFilterFactory implements FilterFactory<RouterFilter> {
3131
}
3232
}
3333

34+
function createServerHttpFilter(config: HttpFilterConfig, overrideConfigMap: Map<string, HttpFilterConfig>): ServerInterceptor {
35+
return (methodDescriptor, call) => {
36+
return new ServerInterceptingCall(call, {
37+
start: next => {
38+
next({
39+
onReceiveMetadata: (metadata, next) => {
40+
metadata.remove('grpc-route');
41+
next(metadata);
42+
}
43+
})
44+
}
45+
});
46+
};
47+
}
48+
3449
const ROUTER_FILTER_URL = 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router';
3550

3651
function parseConfig(encodedConfig: Any__Output): HttpFilterConfig | null {
@@ -44,6 +59,7 @@ export function setup() {
4459
registerHttpFilter(ROUTER_FILTER_URL, {
4560
parseTopLevelFilterConfig: parseConfig,
4661
parseOverrideFilterConfig: parseConfig,
47-
httpFilterConstructor: RouterFilterFactory
62+
httpFilterConstructor: RouterFilterFactory,
63+
createServerFilter: createServerHttpFilter
4864
});
49-
}
65+
}

packages/grpc-js-xds/src/resolver-xds.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { HashPolicy, RouteAction, SingleClusterRouteAction, WeightedCluster, Wei
3131
import { decodeSingleResource, HTTP_CONNECTION_MANGER_TYPE_URL } from './resources';
3232
import Duration = experimental.Duration;
3333
import { Duration__Output } from './generated/google/protobuf/Duration';
34-
import { createHttpFilter, HttpFilterConfig, parseOverrideFilterConfig, parseTopLevelFilterConfig } from './http-filter';
34+
import { createClientHttpFilter, HttpFilterConfig, parseOverrideFilterConfig, parseTopLevelFilterConfig } from './http-filter';
3535
import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_FEDERATION, EXPERIMENTAL_RETRY, EXPERIMENTAL_RING_HASH } from './environment';
3636
import Filter = experimental.Filter;
3737
import FilterFactory = experimental.FilterFactory;
@@ -171,7 +171,7 @@ class XdsResolver implements Resolver {
171171
if (EXPERIMENTAL_FAULT_INJECTION) {
172172
for (const filter of httpConnectionManager.http_filters) {
173173
// typed_config must be set here, or validation would have failed
174-
const filterConfig = parseTopLevelFilterConfig(filter.typed_config!);
174+
const filterConfig = parseTopLevelFilterConfig(filter.typed_config!, true);
175175
if (filterConfig) {
176176
ldsHttpFilterConfigs.push({name: filter.name, config: filterConfig});
177177
}
@@ -273,17 +273,17 @@ class XdsResolver implements Resolver {
273273
if (EXPERIMENTAL_FAULT_INJECTION) {
274274
for (const filterConfig of ldsHttpFilterConfigs) {
275275
if (routeHttpFilterOverrides.has(filterConfig.name)) {
276-
const filter = createHttpFilter(filterConfig.config, routeHttpFilterOverrides.get(filterConfig.name)!);
276+
const filter = createClientHttpFilter(filterConfig.config, routeHttpFilterOverrides.get(filterConfig.name)!);
277277
if (filter) {
278278
extraFilterFactories.push(filter);
279279
}
280280
} else if (virtualHostHttpFilterOverrides.has(filterConfig.name)) {
281-
const filter = createHttpFilter(filterConfig.config, virtualHostHttpFilterOverrides.get(filterConfig.name)!);
281+
const filter = createClientHttpFilter(filterConfig.config, virtualHostHttpFilterOverrides.get(filterConfig.name)!);
282282
if (filter) {
283283
extraFilterFactories.push(filter);
284284
}
285285
} else {
286-
const filter = createHttpFilter(filterConfig.config);
286+
const filter = createClientHttpFilter(filterConfig.config);
287287
if (filter) {
288288
extraFilterFactories.push(filter);
289289
}
@@ -308,22 +308,22 @@ class XdsResolver implements Resolver {
308308
}
309309
for (const filterConfig of ldsHttpFilterConfigs) {
310310
if (clusterHttpFilterOverrides.has(filterConfig.name)) {
311-
const filter = createHttpFilter(filterConfig.config, clusterHttpFilterOverrides.get(filterConfig.name)!);
311+
const filter = createClientHttpFilter(filterConfig.config, clusterHttpFilterOverrides.get(filterConfig.name)!);
312312
if (filter) {
313313
extraFilterFactories.push(filter);
314314
}
315315
} else if (routeHttpFilterOverrides.has(filterConfig.name)) {
316-
const filter = createHttpFilter(filterConfig.config, routeHttpFilterOverrides.get(filterConfig.name)!);
316+
const filter = createClientHttpFilter(filterConfig.config, routeHttpFilterOverrides.get(filterConfig.name)!);
317317
if (filter) {
318318
extraFilterFactories.push(filter);
319319
}
320320
} else if (virtualHostHttpFilterOverrides.has(filterConfig.name)) {
321-
const filter = createHttpFilter(filterConfig.config, virtualHostHttpFilterOverrides.get(filterConfig.name)!);
321+
const filter = createClientHttpFilter(filterConfig.config, virtualHostHttpFilterOverrides.get(filterConfig.name)!);
322322
if (filter) {
323323
extraFilterFactories.push(filter);
324324
}
325325
} else {
326-
const filter = createHttpFilter(filterConfig.config);
326+
const filter = createClientHttpFilter(filterConfig.config);
327327
if (filter) {
328328
extraFilterFactories.push(filter);
329329
}

packages/grpc-js-xds/src/server.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { findVirtualHostForDomain } from "./xds-dependency-manager";
3737
import { LogVerbosity } from "@grpc/grpc-js/build/src/constants";
3838
import { XdsServerCredentials } from "./xds-credentials";
3939
import { CertificateValidationContext__Output } from "./generated/envoy/extensions/transport_sockets/tls/v3/CertificateValidationContext";
40+
import { createServerHttpFilter, HttpFilterConfig, parseOverrideFilterConfig, parseTopLevelFilterConfig } from "./http-filter";
4041

4142
const TRACER_NAME = 'xds_server';
4243

@@ -64,6 +65,7 @@ interface NormalizedFilterChainMatch {
6465
}
6566

6667
interface RouteEntry {
68+
id: string;
6769
matcher: Matcher;
6870
isNonForwardingAction: boolean;
6971
}
@@ -94,6 +96,10 @@ class FilterChainEntry {
9496
private virtualHosts: VirtualHostEntry[] | null = null;
9597
private connectionInjector: ConnectionInjector;
9698
private hasRouteConfigErrors = false;
99+
/**
100+
* filter name -> route ID -> config
101+
*/
102+
private overrideConfigMaps = new Map<string, Map<string, HttpFilterConfig>>();
97103
constructor(private configParameters: ConfigParameters, filterChain: FilterChain__Output, credentials: ServerCredentials, onRouteConfigPopulated: () => void) {
98104
this.matchers = normalizeFilterChainMatch(filterChain.filter_chain_match);
99105
const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, filterChain.filters[0].typed_config!.value);
@@ -145,6 +151,7 @@ class FilterChainEntry {
145151
for (const route of virtualHost.routes) {
146152
if (route.matcher.apply(methodDescriptor.path, metadata)) {
147153
if (route.isNonForwardingAction) {
154+
metadata.set('grpc-route', route.id);
148155
next(metadata);
149156
} else {
150157
call.sendStatus(routeErrorStatus);
@@ -158,6 +165,18 @@ class FilterChainEntry {
158165
}
159166
});
160167
}
168+
const httpFilterInterceptors: ServerInterceptor[] = [];
169+
for (const filter of httpConnectionManager.http_filters) {
170+
const filterConfig = parseTopLevelFilterConfig(filter.typed_config!, false);
171+
if (filterConfig) {
172+
const filterOverrideConfigMap = new Map<string, HttpFilterConfig>();
173+
this.overrideConfigMaps.set(filterConfig.typeUrl, filterOverrideConfigMap);
174+
const filterInterceptor = createServerHttpFilter(filterConfig, filterOverrideConfigMap);
175+
if (filterInterceptor) {
176+
httpFilterInterceptors.push(filterInterceptor);
177+
}
178+
}
179+
}
161180
if (credentials instanceof XdsServerCredentials) {
162181
if (filterChain.transport_socket) {
163182
trace('Using secure credentials');
@@ -193,20 +212,24 @@ class FilterChainEntry {
193212
credentials = credentials.getFallbackCredentials();
194213
}
195214
}
196-
const interceptingCredentials = createServerCredentialsWithInterceptors(credentials, [interceptor]);
215+
const interceptingCredentials = createServerCredentialsWithInterceptors(credentials, [interceptor, ...httpFilterInterceptors]);
197216
this.connectionInjector = configParameters.createConnectionInjector(interceptingCredentials);
198217
}
199218

200219
private handleRouteConfigurationResource(routeConfig: RouteConfiguration__Output) {
201220
let hasRouteConfigErrors = false;
202221
this.virtualHosts = [];
203-
for (const virtualHost of routeConfig.virtual_hosts) {
222+
for (const overrideMap of this.overrideConfigMaps.values()) {
223+
overrideMap.clear();
224+
}
225+
for (const [virtualHostIndex, virtualHost] of routeConfig.virtual_hosts.entries()) {
204226
const virtualHostEntry: VirtualHostEntry = {
205227
domains: virtualHost.domains,
206228
routes: []
207229
};
208-
for (const route of virtualHost.routes) {
230+
for (const [routeIndex, route] of virtualHost.routes.entries()) {
209231
const routeEntry: RouteEntry = {
232+
id: `virtualhost=${virtualHostIndex} route=${routeIndex}`,
210233
matcher: getPredicateForMatcher(route.match!),
211234
isNonForwardingAction: route.action === 'non_forwarding_action'
212235
};
@@ -215,6 +238,12 @@ class FilterChainEntry {
215238
this.logConfigurationError('For domains matching [' + virtualHostEntry.domains + '] requests will be rejected for routes matching ' + routeEntry.matcher.toString());
216239
}
217240
virtualHostEntry.routes.push(routeEntry);
241+
for (const [filterName, overrideConfig] of Object.entries(route.typed_per_filter_config)) {
242+
const parsedConfig = parseOverrideFilterConfig(overrideConfig);
243+
if (parsedConfig) {
244+
this.overrideConfigMaps.get(filterName)?.set(routeEntry.id, parsedConfig);
245+
}
246+
}
218247
}
219248
this.virtualHosts.push(virtualHostEntry);
220249
}

packages/grpc-js-xds/src/xds-resource-type/listener-resource-type.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,10 @@ function normalizeFilterChainMatch(filterChainMatch: FilterChainMatch__Output):
8787

8888
/**
8989
* @param httpConnectionManager
90+
* @param
9091
* @returns A list of validation errors, if there are any. An empty list indicates success
9192
*/
92-
function validateHttpConnectionManager(httpConnectionManager: HttpConnectionManager__Output): string[] {
93+
function validateHttpConnectionManager(httpConnectionManager: HttpConnectionManager__Output, client: boolean): string[] {
9394
const errors: string[] = [];
9495
if (EXPERIMENTAL_FAULT_INJECTION) {
9596
const filterNames = new Set<string>();
@@ -98,7 +99,7 @@ function validateHttpConnectionManager(httpConnectionManager: HttpConnectionMana
9899
errors.push(`duplicate HTTP filter name: ${httpFilter.name}`);
99100
}
100101
filterNames.add(httpFilter.name);
101-
if (!validateTopLevelFilter(httpFilter)) {
102+
if (!validateTopLevelFilter(httpFilter, client)) {
102103
errors.push(`${httpFilter.name} filter validation failed`);
103104
}
104105
/* Validate that the last filter, and only the last filter, is the
@@ -237,7 +238,7 @@ function validateFilterChain(context: XdsDecodeContext, filterChain: FilterChain
237238
if (filterChain.filters.length === 1) {
238239
if (filterChain.filters[0].typed_config?.type_url === HTTP_CONNECTION_MANGER_TYPE_URL) {
239240
const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, filterChain.filters[0].typed_config.value);
240-
errors.push(...validateHttpConnectionManager(httpConnectionManager).map(error => `filters[0].typed_config: ${error}`));
241+
errors.push(...validateHttpConnectionManager(httpConnectionManager, false).map(error => `filters[0].typed_config: ${error}`));
241242
} else {
242243
errors.push(`Unexpected value of filters[0].typed_config.type_url: ${filterChain.filters[0].typed_config?.type_url}`);
243244
}
@@ -270,7 +271,7 @@ export class ListenerResourceType extends XdsResourceType {
270271
message.api_listener.api_listener.type_url === HTTP_CONNECTION_MANGER_TYPE_URL
271272
) {
272273
const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, message.api_listener!.api_listener.value);
273-
errors.push(...validateHttpConnectionManager(httpConnectionManager).map(error => `api_listener.api_listener: ${error}`));
274+
errors.push(...validateHttpConnectionManager(httpConnectionManager, true).map(error => `api_listener.api_listener: ${error}`));
274275
} else {
275276
errors.push(`api_listener.api_listener.type_url != ${HTTP_CONNECTION_MANGER_TYPE_URL}`);
276277
}

0 commit comments

Comments
 (0)