From cea2cf9c498ea284029bfa70019ff3d3de653f92 Mon Sep 17 00:00:00 2001
From: Georgi Mirchev <gmirchev90@gmail.com>
Date: Wed, 18 Apr 2018 14:58:36 +0300
Subject: [PATCH] Recognize & handle brotli compression of the response body

---
 browsermob-core/pom.xml                       | 12 +++++
 .../filters/ServerResponseCaptureFilter.java  |  8 ++++
 .../bmp/util/BrowserMobHttpUtil.java          | 44 +++++++++++++++++++
 3 files changed, 64 insertions(+)

diff --git a/browsermob-core/pom.xml b/browsermob-core/pom.xml
index 2071434ee..1bac1fb7d 100644
--- a/browsermob-core/pom.xml
+++ b/browsermob-core/pom.xml
@@ -245,5 +245,17 @@
             <artifactId>hamcrest-library</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.meteogroup.jbrotli</groupId>
+            <artifactId>jbrotli-servlet</artifactId>
+            <version>0.5.0</version>
+        </dependency>
     </dependencies>
+    <repositories>
+        <repository>
+            <id>bintray-nitram509-jbrotli</id>
+            <name>bintray</name>
+            <url>http://dl.bintray.com/nitram509/jbrotli</url>
+        </repository>
+    </repositories>
 </project>
\ No newline at end of file
diff --git a/browsermob-core/src/main/java/net/lightbody/bmp/filters/ServerResponseCaptureFilter.java b/browsermob-core/src/main/java/net/lightbody/bmp/filters/ServerResponseCaptureFilter.java
index d69ad8f76..1bce61ad7 100644
--- a/browsermob-core/src/main/java/net/lightbody/bmp/filters/ServerResponseCaptureFilter.java
+++ b/browsermob-core/src/main/java/net/lightbody/bmp/filters/ServerResponseCaptureFilter.java
@@ -25,6 +25,7 @@
  */
 public class ServerResponseCaptureFilter extends HttpFiltersAdapter {
     private static final Logger log = LoggerFactory.getLogger(ServerResponseCaptureFilter.class);
+    private static final String BROTLI_COMPRESSION = "br";
 
     /**
      * Populated by serverToProxyResponse() when processing the HttpResponse object
@@ -133,6 +134,13 @@ protected void decompressContents() {
             } catch (RuntimeException e) {
                 log.warn("Failed to decompress response with encoding type " + contentEncoding + " when decoding request from " + originalRequest.getUri(), e);
             }
+        } else if(contentEncoding.equals(BROTLI_COMPRESSION)) {
+            try {
+                fullResponseContents = BrowserMobHttpUtil.decompressBrotliContents(getRawResponseContents());
+                decompressionSuccessful = true;
+            } catch (RuntimeException e) {
+                log.warn("Failed to decompress response with encoding type " + contentEncoding + " when decoding request from " + originalRequest.getUri(), e);
+            }
         } else {
             log.warn("Cannot decode unsupported content encoding type {}", contentEncoding);
         }
diff --git a/browsermob-core/src/main/java/net/lightbody/bmp/util/BrowserMobHttpUtil.java b/browsermob-core/src/main/java/net/lightbody/bmp/util/BrowserMobHttpUtil.java
index 98172810f..9af7734a2 100644
--- a/browsermob-core/src/main/java/net/lightbody/bmp/util/BrowserMobHttpUtil.java
+++ b/browsermob-core/src/main/java/net/lightbody/bmp/util/BrowserMobHttpUtil.java
@@ -9,12 +9,15 @@
 import io.netty.handler.codec.http.HttpResponse;
 import net.lightbody.bmp.exception.DecompressionException;
 import net.lightbody.bmp.exception.UnsupportedCharsetException;
+import org.meteogroup.jbrotli.io.BrotliInputStream;
+import org.meteogroup.jbrotli.libloader.BrotliLibraryLoader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.Charset;
@@ -111,6 +114,47 @@ public static byte[] decompressContents(byte[] fullMessage) throws Decompression
         return fullMessage;
     }
 
+    /**
+     * Decompresses the brotli byte stream.
+     *
+     * @param fullMessage brotli byte stream to decompress
+     * @return decompressed bytes
+     * @throws DecompressionException thrown if the fullMessage cannot be read or decompressed for any reason
+     */
+    public static byte[] decompressBrotliContents(byte[] fullMessage) throws DecompressionException {
+        BrotliLibraryLoader.loadBrotli();
+
+        InputStream brotliInputReader = null;
+        ByteArrayOutputStream uncompressed = null;
+
+        try {
+            brotliInputReader = new BrotliInputStream(new ByteArrayInputStream(fullMessage));
+
+            uncompressed = new ByteArrayOutputStream();
+
+            byte[] decompressBuffer = new byte[DECOMPRESS_BUFFER_SIZE];
+            int bytesRead;
+            while ((bytesRead = brotliInputReader.read(decompressBuffer, 0, decompressBuffer.length)) != -1) {
+                uncompressed.write(decompressBuffer, 0, bytesRead);
+            }
+
+            uncompressed.flush();
+            fullMessage = uncompressed.toByteArray();
+        } catch (IOException e) {
+            throw new DecompressionException("Unable to decompress response", e);
+        } finally {
+            try {
+                if (brotliInputReader != null) {
+                    brotliInputReader.close();
+                }
+            } catch (IOException e) {
+                log.warn("Unable to close brotli stream", e);
+            }
+        }
+
+        return fullMessage;
+    }
+
     /**
      * Returns true if the content type string indicates textual content. Currently these are any Content-Types that start with one of the
      * following: