4343import org .eclipse .aether .spi .connector .checksum .ChecksumAlgorithmHelper ;
4444import org .eclipse .aether .spi .connector .transport .http .RFC9457 .RFC9457Payload ;
4545import org .eclipse .jetty .alpn .server .ALPNServerConnectionFactory ;
46+ import org .eclipse .jetty .compression .server .CompressionConfig ;
47+ import org .eclipse .jetty .compression .server .CompressionHandler ;
4648import org .eclipse .jetty .http .DateGenerator ;
4749import org .eclipse .jetty .http .HttpField ;
50+ import org .eclipse .jetty .http .HttpFields ;
4851import org .eclipse .jetty .http .HttpHeader ;
4952import org .eclipse .jetty .http .HttpMethod ;
53+ import org .eclipse .jetty .http .HttpURI ;
54+ import org .eclipse .jetty .http .pathmap .MatchedResource ;
55+ import org .eclipse .jetty .http .pathmap .PathMappings ;
56+ import org .eclipse .jetty .http .pathmap .PathSpec ;
5057import org .eclipse .jetty .http2 .server .HTTP2ServerConnectionFactory ;
5158import org .eclipse .jetty .io .Content ;
5259import org .eclipse .jetty .server .Handler ;
@@ -72,12 +79,16 @@ public static class LogEntry {
7279
7380 private final String path ;
7481
75- private final Map <String , String > headers ;
82+ private final Map <String , String > requestHeaders ;
7683
77- public LogEntry (String method , String path , Map <String , String > headers ) {
84+ private final Map <String , String > responseHeaders ;
85+
86+ public LogEntry (
87+ String method , String path , Map <String , String > requestHeaders , Map <String , String > responseHeaders ) {
7888 this .method = method ;
7989 this .path = path ;
80- this .headers = headers ;
90+ this .requestHeaders = requestHeaders ;
91+ this .responseHeaders = responseHeaders ;
8192 }
8293
8394 public String getMethod () {
@@ -88,8 +99,12 @@ public String getPath() {
8899 return path ;
89100 }
90101
91- public Map <String , String > getHeaders () {
92- return headers ;
102+ public Map <String , String > getRequestHeaders () {
103+ return requestHeaders ;
104+ }
105+
106+ public Map <String , String > getResponseHeaders () {
107+ return responseHeaders ;
93108 }
94109
95110 @ Override
@@ -290,15 +305,15 @@ public HttpServer start() throws Exception {
290305 server = new Server ();
291306 httpConnector = new ServerConnector (server );
292307 server .addConnector (httpConnector );
293- server .setHandler (new Handler .Sequence (
308+
309+ server .setHandler (new LogHandler (new CompressionEnforcingHandler (new Handler .Sequence (
294310 new ConnectionClosingHandler (),
295311 new ServerErrorHandler (),
296- new LogHandler (),
297312 new ProxyAuthHandler (),
298313 new AuthHandler (),
299314 new RedirectHandler (),
300315 new RepoHandler (),
301- new RFC9457Handler ()));
316+ new RFC9457Handler ())))) ;
302317 server .start ();
303318
304319 return this ;
@@ -313,6 +328,112 @@ public void stop() throws Exception {
313328 }
314329 }
315330
331+ private class CompressionEnforcingHandler extends CompressionHandler {
332+ // duplicate of CompressionHandler.pathConfigs which is private
333+ private final PathMappings <CompressionConfig > pathConfigs = new PathMappings <>();
334+
335+ CompressionEnforcingHandler (Handler handler ) {
336+ super (handler );
337+ this .putConfiguration (
338+ "/br/*" ,
339+ CompressionConfig .builder ().compressIncludeEncoding ("br" ).build ());
340+ this .putConfiguration (
341+ "/zstd/*" ,
342+ CompressionConfig .builder ().compressIncludeEncoding ("zstd" ).build ());
343+ this .putConfiguration (
344+ "/gzip/*" ,
345+ CompressionConfig .builder ().compressIncludeEncoding ("gzip" ).build ());
346+ this .putConfiguration (
347+ "/deflate/*" ,
348+ CompressionConfig .builder ()
349+ .compressIncludeEncoding ("deflate" )
350+ .build ());
351+ }
352+
353+ @ Override
354+ public CompressionConfig putConfiguration (PathSpec pathSpec , CompressionConfig config ) {
355+ // deliberately not set it in the super class yet
356+ return pathConfigs .put (pathSpec , config );
357+ }
358+
359+ @ Override
360+ public boolean handle (Request request , Response response , Callback callback ) throws Exception {
361+ Handler next = getHandler ();
362+ if (next == null ) {
363+ return false ;
364+ }
365+ String pathInContext = Request .getPathInContext (request );
366+ MatchedResource <CompressionConfig > matchedConfig = this .pathConfigs .getMatched (pathInContext );
367+ if (matchedConfig == null ) {
368+ if (LOGGER .isDebugEnabled ()) {
369+ LOGGER .debug ("skipping compression: path {} has no matching compression config" , pathInContext );
370+ }
371+ // No configuration, skip
372+ return next .handle (request , response , callback );
373+ }
374+
375+ // set the matched config in the super class for further processing, but for all paths
376+ // no need to reset it later as this handler is not used among multiple requests
377+ super .putConfiguration (PathSpec .from ("/*" ), matchedConfig .getResource ());
378+ // first path segment determines the encoding, remove it from the request path for further processing
379+ return super .handle (new StripLeadingPathSegmentsRequestWrapper (request , 1 ), response , callback );
380+ }
381+ }
382+
383+ private static class StripLeadingPathSegmentsRequestWrapper extends Request .Wrapper {
384+ private final HttpURI modifiedURI ;
385+
386+ StripLeadingPathSegmentsRequestWrapper (Request wrapped , int segmentsToStrip ) {
387+ super (wrapped );
388+ this .modifiedURI = stripPathSegments (wrapped .getHttpURI (), segmentsToStrip );
389+ }
390+
391+ private static org .eclipse .jetty .http .HttpURI stripPathSegments (
392+ org .eclipse .jetty .http .HttpURI originalURI , int segmentsToStrip ) {
393+ if (segmentsToStrip <= 0 ) {
394+ return originalURI ;
395+ }
396+
397+ String originalPath = originalURI .getPath ();
398+ if (originalPath == null || originalPath .isEmpty ()) {
399+ return originalURI ;
400+ }
401+
402+ // Split path into segments
403+ String [] segments = originalPath .split ("/" );
404+ StringBuilder newPath = new StringBuilder ();
405+
406+ // Skip empty first segment (from leading /) and the specified number of segments
407+ int skipCount = 0 ;
408+ for (int i = 0 ; i < segments .length ; i ++) {
409+ if (segments [i ].isEmpty () && i == 0 ) {
410+ // Skip leading empty segment from leading /
411+ continue ;
412+ }
413+ if (skipCount < segmentsToStrip ) {
414+ skipCount ++;
415+ continue ;
416+ }
417+ newPath .append ("/" ).append (segments [i ]);
418+ }
419+
420+ // If we stripped everything, return root path
421+ if (newPath .isEmpty ()) {
422+ newPath .append ("/" );
423+ }
424+
425+ // Build new URI with modified path
426+ return org .eclipse .jetty .http .HttpURI .build (originalURI )
427+ .path (newPath .toString ())
428+ .asImmutable ();
429+ }
430+
431+ @ Override
432+ public HttpURI getHttpURI () {
433+ return modifiedURI ;
434+ }
435+ }
436+
316437 private class ConnectionClosingHandler extends Handler .Abstract {
317438
318439 @ Override
@@ -337,22 +458,41 @@ public boolean handle(Request request, Response response, Callback callback) thr
337458 }
338459 }
339460
340- private class LogHandler extends Handler .Abstract {
461+ private class LogHandler extends Handler .Wrapper {
462+
463+ LogHandler (Handler handler ) {
464+ super (handler );
465+ }
466+
341467 @ Override
342- public boolean handle (Request req , Response response , Callback callback ) {
468+ public boolean handle (Request req , Response response , Callback callback ) throws Exception {
469+
343470 LOGGER .info (
344471 "{} {}{}" ,
345472 req .getMethod (),
346473 req .getHttpURI ().getDecodedPath (),
347474 req .getHttpURI ().getQuery () != null ? "?" + req .getHttpURI ().getQuery () : "" );
348475
349- Map <String , String > headers = new TreeMap <>(String .CASE_INSENSITIVE_ORDER );
350- for (HttpField header : req .getHeaders ()) {
351- headers .put (header .getName (), header .getValueList ().stream ().collect (Collectors .joining (", " )));
476+ Map <String , String > requestHeaders =
477+ toUnmodifiableMap (req .getHeaders ()); // capture request headers before other handlers modify them
478+ try {
479+ return super .handle (req , response , callback );
480+ } finally {
481+ // capture response headers after other handlers modified them
482+ logEntries .add (new LogEntry (
483+ req .getMethod (),
484+ req .getHttpURI ().getPathQuery (),
485+ requestHeaders ,
486+ toUnmodifiableMap (response .getHeaders ())));
352487 }
353- logEntries .add (new LogEntry (
354- req .getMethod (), req .getHttpURI ().getPathQuery (), Collections .unmodifiableMap (headers )));
355- return false ;
488+ }
489+
490+ Map <String , String > toUnmodifiableMap (HttpFields headers ) {
491+ Map <String , String > map = new TreeMap <>(String .CASE_INSENSITIVE_ORDER );
492+ for (HttpField header : headers ) {
493+ map .put (header .getName (), header .getValueList ().stream ().collect (Collectors .joining (", " )));
494+ }
495+ return Collections .unmodifiableMap (map );
356496 }
357497 }
358498
0 commit comments