1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detection.h"
6
7#include <string.h>
8#include <algorithm>
9#include <map>
10#include <vector>
11
12#include "base/base64.h"
13#include "base/md5.h"
14#include "base/memory/scoped_ptr.h"
15#include "base/strings/string_number_conversions.h"
16#include "base/strings/string_split.h"
17#include "components/data_reduction_proxy/common/data_reduction_proxy_headers.h"
18#include "components/data_reduction_proxy/common/data_reduction_proxy_headers_test_utils.h"
19#include "net/http/http_response_headers.h"
20#include "testing/gtest/include/gtest/gtest.h"
21
22#if defined(OS_ANDROID)
23#include "base/android/jni_android.h"
24#include "net/android/network_library.h"
25#endif
26
27namespace {
28
29// Calcuates MD5 hash value for a string and then base64 encode it. Testcases
30// contain expected fingerprint in plain text, which needs to be encoded before
31// comparison.
32std::string GetEncoded(const std::string& input) {
33  base::MD5Digest digest;
34  base::MD5Sum(input.c_str(), input.size(), &digest);
35  std::string base64encoded;
36  base::Base64Encode(std::string((char*)digest.a,
37                     ARRAYSIZE_UNSAFE(digest.a)), &base64encoded);
38  return base64encoded;
39}
40
41// Replaces all contents within "[]" by corresponding base64 encoded MD5 value.
42// It can handle nested case like: [[abc]def]. This helper function transforms
43// fingerprint in plain text to actual encoded fingerprint.
44void ReplaceWithEncodedString(std::string* input) {
45  size_t start, end, temp;
46  while (true) {
47    start = input->find("[");
48    if (start == std::string::npos) break;
49    while (true) {
50      temp = input->find("[", start + 1);
51      end = input->find("]", start + 1);
52      if (end != std::string::npos && end < temp)
53        break;
54
55      start = temp;
56    }
57    std::string need_to_encode = input->substr(start + 1, end - start - 1);
58    *input = input->substr(0, start) + GetEncoded(need_to_encode) +
59             input->substr(end + 1);
60  }
61}
62
63// Returns a vector contains all the values from a comma-separated string.
64// Some testcases contain string representation of a vector, this helper
65// function generates a vector from a input string.
66std::vector<std::string> StringsToVector(const std::string& values) {
67  std::vector<std::string> ret;
68  if (values.empty())
69    return ret;
70  size_t now = 0;
71  size_t next;
72  while ((next = values.find(",", now)) != std::string::npos) {
73    ret.push_back(values.substr(now, next - now));
74    now = next + 1;
75  }
76  return ret;
77}
78
79void InitEnv() {
80#if defined(OS_ANDROID)
81  JNIEnv* env = base::android::AttachCurrentThread();
82  static bool inited = false;
83  if (!inited) {
84    net::android::RegisterNetworkLibrary(env);
85    inited = true;
86  }
87#endif
88}
89
90}  // namespace
91
92namespace data_reduction_proxy {
93
94class DataReductionProxyTamperDetectionTest : public testing::Test {
95
96};
97
98// Tests function ValidateChromeProxyHeader.
99TEST_F(DataReductionProxyTamperDetectionTest, ChromeProxy) {
100  // |received_fingerprint| is not the actual fingerprint from data reduction
101  // proxy, instead, the base64 encoded field is in plain text (within "[]")
102  // and needs to be encoded first.
103  struct {
104    std::string label;
105    std::string raw_header;
106    std::string received_fingerprint;
107    bool expected_tampered_with;
108  } test[] = {
109    {
110      "Checks sorting.",
111      "HTTP/1.1 200 OK\n"
112      "Chrome-Proxy: c,b,a,3,2,1,fcp=f\n",
113      "[1,2,3,a,b,c,]",
114      false,
115    },
116    {
117      "Checks Chrome-Proxy's fingerprint removing.",
118      "HTTP/1.1 200 OK\n"
119      "Chrome-Proxy: a,b,c,d,e,3,2,1,fcp=f\n",
120      "[1,2,3,a,b,c,d,e,]",
121      false,
122    },
123    {
124      "Checks no Chrome-Proxy header case (should not happen).",
125      "HTTP/1.1 200 OK\n",
126      "[]",
127      false,
128    },
129    {
130      "Checks empty Chrome-Proxy header case (should not happen).",
131      "HTTP/1.1 200 OK\n"
132      "Chrome-Proxy:    \n",
133      "[,]",
134      false,
135    },
136    {
137      "Checks Chrome-Proxy header with its fingerprint only case.",
138      "HTTP/1.1 200 OK\n"
139      "Chrome-Proxy: fcp=f\n",
140      "[]",
141      false,
142    },
143    {
144      "Checks empty Chrome-Proxy header case, with extra ',' and ' '",
145      "HTTP/1.1 200 OK\n"
146      "Chrome-Proxy: fcp=f  ,  \n",
147      "[]",
148      false,
149    },
150    {
151      "Changed no value to empty value.",
152      "HTTP/1.1 200 OK\n"
153      "Chrome-Proxy: fcp=f\n",
154      "[,]",
155      true,
156    },
157    {
158      "Changed header values.",
159      "HTTP/1.1 200 OK\n"
160      "Chrome-Proxy: a,b=2,c,d=1,fcp=f\n",
161      "[a,b=3,c,d=1,]",
162      true,
163    },
164    {
165      "Changed order of header values.",
166      "HTTP/1.1 200 OK\n"
167      "Chrome-Proxy: c,b,a,fcp=1\n",
168      "[c,b,a,]",
169      true,
170    },
171    {
172      "Checks Chrome-Proxy header with extra ' '.",
173      "HTTP/1.1 200 OK\n"
174      "Chrome-Proxy: a  , b   , c,   d,  fcp=f\n",
175      "[a,b,c,d,]",
176      false
177    },
178    {
179      "Check Chrome-Proxy header with multiple lines and ' '.",
180      "HTTP/1.1 200 OK\n"
181      "Chrome-Proxy:     a    ,   c   , d, fcp=f    \n"
182      "Chrome-Proxy:    b \n",
183      "[a,b,c,d,]",
184      false
185    },
186    {
187      "Checks Chrome-Proxy header with multiple lines, at different positions",
188      "HTTP/1.1 200 OK\n"
189      "Chrome-Proxy:     a   \n"
190      "Chrome-Proxy:    c \n"
191      "Content-Type: 1\n"
192      "Cache-Control: 2\n"
193      "ETag: 3\n"
194      "Chrome-Proxy:    b  \n"
195      "Connection: 4\n"
196      "Expires: 5\n"
197      "Chrome-Proxy:    fcp=f \n"
198      "Via: \n"
199      "Content-Length: 12345\n",
200      "[a,b,c,]",
201      false
202    },
203    {
204      "Checks Chrome-Proxy header with multiple same values.",
205      "HTTP/1.1 200 OK\n"
206      "Chrome-Proxy:     a   \n"
207      "Chrome-Proxy:    b\n"
208      "Chrome-Proxy:    c\n"
209      "Chrome-Proxy:    d,   fcp=f    \n"
210      "Chrome-Proxy:     a   \n",
211      "[a,a,b,c,d,]",
212      false
213    },
214    {
215      "Changed Chrome-Proxy header with multiple lines..",
216      "HTTP/1.1 200 OK\n"
217      "Chrome-Proxy: a\n"
218      "Chrome-Proxy: a\n"
219      "Chrome-Proxy: b\n"
220      "Chrome-Proxy: c,fcp=f\n",
221      "[a,b,c,]",
222      true,
223    },
224    {
225      "Checks case whose received fingerprint is empty.",
226      "HTTP/1.1 200 OK\n"
227      "Chrome-Proxy: a,b,c,fcp=1\n",
228      "[]",
229      true,
230    },
231    {
232      "Checks case whose received fingerprint cannot be base64 decoded.",
233      "HTTP/1.1 200 OK\n"
234      "Chrome-Proxy: a,b,c,fcp=1\n",
235      "not_base64_encoded",
236      true,
237    },
238  };
239
240  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test); ++i) {
241    ReplaceWithEncodedString(&test[i].received_fingerprint);
242
243    std::string raw_headers(test[i].raw_header);
244    HeadersToRaw(&raw_headers);
245    scoped_refptr<net::HttpResponseHeaders> headers(
246        new net::HttpResponseHeaders(raw_headers));
247
248    DataReductionProxyTamperDetection tamper_detection(headers.get(), true, 0);
249
250    bool tampered = tamper_detection.ValidateChromeProxyHeader(
251        test[i].received_fingerprint);
252
253    EXPECT_EQ(test[i].expected_tampered_with, tampered) << test[i].label;
254  }
255}
256
257// Tests function ValidateViaHeader.
258TEST_F(DataReductionProxyTamperDetectionTest, Via) {
259  struct {
260    std::string label;
261    std::string raw_header;
262    std::string received_fingerprint;
263    bool expected_tampered_with;
264    bool expected_has_chrome_proxy_via_header;
265  } test[] = {
266    {
267      "Checks the case that Chrome-Compression-Proxy occurs at the last.",
268      "HTTP/1.1 200 OK\n"
269      "Via: a, b, c, 1.1 Chrome-Compression-Proxy\n",
270      "",
271      false,
272      true,
273    },
274    {
275      "Checks when there is intermediary.",
276      "HTTP/1.1 200 OK\n"
277      "Via: a, b,  c,   1.1 Chrome-Compression-Proxy,    xyz\n",
278      "",
279      true,
280      true,
281    },
282    {
283      "Checks the case of empty Via header.",
284      "HTTP/1.1 200 OK\n"
285      "Via:  \n",
286      "",
287      true,
288      false,
289    },
290    {
291      "Checks the case that only the data reduction proxy's Via header occurs.",
292      "HTTP/1.1 200 OK\n"
293      "Via:  1.1 Chrome-Compression-Proxy    \n",
294      "",
295      false,
296      true,
297    },
298    {
299      "Checks the case that there are ' ', i.e., empty value after the data"
300      " reduction proxy's Via header.",
301      "HTTP/1.1 200 OK\n"
302      "Via:  1.1 Chrome-Compression-Proxy  ,  , \n",
303      "",
304      false,
305      true,
306    },
307    {
308      "Checks the case when there is no Via header",
309      "HTTP/1.1 200 OK\n",
310      "",
311      true,
312      false,
313    },
314    // Same to above test cases, but with deprecated data reduciton proxy Via
315    // header.
316    {
317      "Checks the case that Chrome Compression Proxy occurs at the last.",
318      "HTTP/1.1 200 OK\n"
319      "Via: a, b, c, 1.1 Chrome Compression Proxy\n",
320      "",
321      false,
322      true,
323    },
324    {
325      "Checks when there is intermediary.",
326      "HTTP/1.1 200 OK\n"
327      "Via: a, b,  c,   1.1 Chrome Compression Proxy,    xyz\n",
328      "",
329      true,
330      true,
331    },
332    {
333      "Checks the case of empty Via header.",
334      "HTTP/1.1 200 OK\n"
335      "Via:  \n",
336      "",
337      true,
338      false,
339    },
340    {
341      "Checks the case that only the data reduction proxy's Via header occurs.",
342      "HTTP/1.1 200 OK\n"
343      "Via:  1.1 Chrome Compression Proxy    \n",
344      "",
345      false,
346      true,
347    },
348    {
349      "Checks the case that there are ' ', i.e., empty value after the data"
350      "reduction proxy's Via header.",
351      "HTTP/1.1 200 OK\n"
352      "Via:  1.1 Chrome Compression Proxy  ,  , \n",
353      "",
354      false,
355      true,
356    },
357    {
358      "Checks the case when there is no Via header",
359      "HTTP/1.1 200 OK\n",
360      "",
361      true,
362      false,
363    },
364  };
365
366  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test); ++i) {
367    std::string raw_headers(test[i].raw_header);
368    HeadersToRaw(&raw_headers);
369    scoped_refptr<net::HttpResponseHeaders> headers(
370        new net::HttpResponseHeaders(raw_headers));
371
372    DataReductionProxyTamperDetection tamper_detection(headers.get(), true, 0);
373
374    bool has_chrome_proxy_via_header;
375    bool tampered = tamper_detection.ValidateViaHeader(
376        test[i].received_fingerprint, &has_chrome_proxy_via_header);
377
378    EXPECT_EQ(test[i].expected_tampered_with, tampered) << test[i].label;
379    EXPECT_EQ(test[i].expected_has_chrome_proxy_via_header,
380              has_chrome_proxy_via_header) << test[i].label;
381  }
382}
383
384// Tests function ValidateOtherHeaders.
385TEST_F(DataReductionProxyTamperDetectionTest, OtherHeaders) {
386  // For following testcases, |received_fingerprint| is not the actual
387  // fingerprint from data reduction proxy, instead, the base64 encoded field
388  // is in plain text (within "[]") and needs to be encoded first. For example,
389  // "[12345;]|content-length" needs to be encoded to
390  // "Base64Encoded(MD5(12345;))|content-length" before calling the checking
391  // function.
392  struct {
393    std::string label;
394    std::string raw_header;
395    std::string received_fingerprint;
396    bool expected_tampered_with;
397  } test[] = {
398    {
399      "Checks the case that only one header is requested.",
400      "HTTP/1.1 200 OK\n"
401      "Content-Length: 12345\n",
402      "[12345,;]|content-length",
403      false
404    },
405    {
406      "Checks the case that there is only one requested header and it does not"
407      "exist.",
408      "HTTP/1.1 200 OK\n",
409      "[;]|non_exist_header",
410      false
411    },
412    {
413      "Checks the case of multiple headers are requested.",
414      "HTTP/1.1 200 OK\n"
415      "Content-Type: 1\n"
416      "Cache-Control: 2\n"
417      "ETag: 3\n"
418      "Connection: 4\n"
419      "Expires: 5\n",
420      "[1,;2,;3,;4,;5,;]|content-type|cache-control|etag|connection|expires",
421      false
422    },
423    {
424      "Checks the case that one header has multiple values.",
425      "HTTP/1.1 200 OK\n"
426      "Content-Type: aaa1,    bbb1, ccc1\n"
427      "Cache-Control: aaa2\n",
428      "[aaa1,bbb1,ccc1,;aaa2,;]|content-type|cache-control",
429      false
430    },
431    {
432      "Checks the case that one header has multiple lines.",
433      "HTTP/1.1 200 OK\n"
434      "Content-Type: aaa1,   ccc1\n"
435      "Content-Type: xxx1,    bbb1, ccc1\n"
436      "Cache-Control: aaa2\n",
437      "[aaa1,bbb1,ccc1,ccc1,xxx1,;aaa2,;]|content-type|cache-control",
438      false
439    },
440    {
441      "Checks the case that more than one headers have multiple values.",
442      "HTTP/1.1 200 OK\n"
443      "Content-Type: aaa1,   ccc1\n"
444      "Cache-Control: ccc2    , bbb2\n"
445      "Content-Type:  bbb1, ccc1\n"
446      "Cache-Control: aaa2   \n",
447      "[aaa1,bbb1,ccc1,ccc1,;aaa2,bbb2,ccc2,;]|content-type|cache-control",
448      false
449    },
450    {
451      "Checks the case that one of the requested headers is missing (Expires).",
452      "HTTP/1.1 200 OK\n"
453      "Content-Type: aaa1,   ccc1\n",
454      "[aaa1,ccc1,;;]|content-type|expires",
455      false
456    },
457    {
458      "Checks the case that some of the requested headers have empty value.",
459      "HTTP/1.1 200 OK\n"
460      "Content-Type:   \n"
461      "Cache-Control: \n",
462      "[,;,;]|content-type|cache-control",
463      false
464    },
465    {
466      "Checks the case that all the requested headers are missing.",
467      "HTTP/1.1 200 OK\n",
468      "[;;]|content-type|expires",
469      false
470    },
471    {
472      "Checks the case that some headers are missing, some of them are empty.",
473      "HTTP/1.1 200 OK\n"
474      "Cache-Control: \n",
475      "[;,;]|content-type|cache-control",
476      false
477    },
478    {
479      "Checks the case there is no requested header (header list is empty).",
480      "HTTP/1.1 200 OK\n"
481      "Chrome-Proxy: aut=aauutthh,bbbypas=0,aaxxx=xxx,bbbloc=1\n"
482      "Content-Type: 1\n"
483      "Cache-Control: 2\n",
484      "[]",
485      false
486    },
487    {
488      "Checks tampered requested header values.",
489      "HTTP/1.1 200 OK\n"
490      "Content-Type: aaa1,   ccc1\n"
491      "Cache-Control: ccc2    , bbb2\n",
492      "[aaa1,bbb1,;bbb2,ccc2,;]|content-type|cache-control",
493      true
494    },
495  };
496
497  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test); ++i) {
498    ReplaceWithEncodedString(&test[i].received_fingerprint);
499
500    std::string raw_headers(test[i].raw_header);
501    HeadersToRaw(&raw_headers);
502    scoped_refptr<net::HttpResponseHeaders> headers(
503        new net::HttpResponseHeaders(raw_headers));
504
505    DataReductionProxyTamperDetection tamper_detection(headers.get(), true, 0);
506
507    bool tampered = tamper_detection.ValidateOtherHeaders(
508        test[i].received_fingerprint);
509
510    EXPECT_EQ(test[i].expected_tampered_with, tampered) << test[i].label;
511  }
512}
513
514// Tests function ValidateContentLengthHeader.
515TEST_F(DataReductionProxyTamperDetectionTest, ContentLength) {
516  struct {
517    std::string label;
518    std::string raw_header;
519    std::string received_fingerprint;
520    bool expected_tampered_with;
521  } test[] = {
522    {
523      "Checks the case fingerprint matches received response.",
524      "HTTP/1.1 200 OK\n"
525      "Content-Length: 12345\n",
526      "12345",
527      false,
528    },
529    {
530      "Checks case that response got modified.",
531      "HTTP/1.1 200 OK\n"
532      "Content-Length: 12345\n",
533      "125",
534      true,
535    },
536    {
537      "Checks the case that the data reduction proxy has not sent"
538      "Content-Length header.",
539      "HTTP/1.1 200 OK\n"
540      "Content-Length: 12345\n",
541      "",
542      false,
543    },
544    {
545      "Checks the case that the data reduction proxy sends invalid"
546      "Content-Length header.",
547      "HTTP/1.1 200 OK\n"
548      "Content-Length: 12345\n",
549      "aaa",
550      false,
551    },
552    {
553      "Checks the case that the data reduction proxy sends invalid"
554      "Content-Length header.",
555      "HTTP/1.1 200 OK\n"
556      "Content-Length: aaa\n",
557      "aaa",
558      false,
559    },
560    {
561      "Checks the case that Content-Length header is missing at the Chromium"
562      "client side.",
563      "HTTP/1.1 200 OK\n",
564      "123",
565      false,
566    },
567    {
568      "Checks the case that Content-Length header are missing at both end.",
569      "HTTP/1.1 200 OK\n",
570      "",
571      false,
572    },
573  };
574
575  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test); ++i) {
576    std::string raw_headers(test[i].raw_header);
577    HeadersToRaw(&raw_headers);
578    scoped_refptr<net::HttpResponseHeaders> headers(
579        new net::HttpResponseHeaders(raw_headers));
580
581    DataReductionProxyTamperDetection tamper_detection(headers.get(), true, 0);
582
583    bool tampered = tamper_detection.ValidateContentLengthHeader(
584        test[i].received_fingerprint);
585
586    EXPECT_EQ(test[i].expected_tampered_with, tampered) << test[i].label;
587  }
588}
589
590// Tests ValuesToSortedString function.
591TEST_F(DataReductionProxyTamperDetectionTest, ValuesToSortedString) {
592  struct {
593    std::string label;
594    std::string input_values;
595    std::string expected_output_string;
596  } test[] = {
597    {
598      "Checks the correctness of sorting.",
599      "3,2,1,",
600      "1,2,3,",
601    },
602    {
603      "Checks the case that there is an empty input vector.",
604      "",
605      "",
606    },
607    {
608      "Checks the case that there is an empty string in the input vector.",
609      ",",
610      ",",
611    },
612  };
613
614  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test); ++i) {
615    std::vector<std::string> input_values =
616        StringsToVector(test[i].input_values);
617    std::string output_string =
618        DataReductionProxyTamperDetection::ValuesToSortedString(&input_values);
619    EXPECT_EQ(output_string, test[i].expected_output_string) << test[i].label;
620  }
621}
622
623// Tests GetHeaderValues function.
624TEST_F(DataReductionProxyTamperDetectionTest, GetHeaderValues) {
625  struct {
626    std::string label;
627    std::string raw_header;
628    std::string header_name;
629    std::string expected_output_values;
630  } test[] = {
631    {
632      "Checks the correctness of getting single line header.",
633      "HTTP/1.1 200 OK\n"
634      "test: 1, 2, 3\n",
635      "test",
636      "1,2,3,",
637    },
638    {
639      "Checks the correctness of getting multiple lines header.",
640      "HTTP/1.1 200 OK\n"
641      "test: 1, 2, 3\n"
642      "test: 4, 5, 6\n"
643      "test: 7, 8, 9\n",
644      "test",
645      "1,2,3,4,5,6,7,8,9,",
646    },
647    {
648      "Checks the correctness of getting missing header.",
649      "HTTP/1.1 200 OK\n",
650      "test",
651      "",
652    },
653    {
654      "Checks the correctness of getting empty header.",
655      "HTTP/1.1 200 OK\n"
656      "test:   \n",
657      "test",
658      ",",
659    },
660  };
661
662  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test); ++i) {
663    std::string raw_headers(test[i].raw_header);
664    HeadersToRaw(&raw_headers);
665    scoped_refptr<net::HttpResponseHeaders> headers(
666        new net::HttpResponseHeaders(raw_headers));
667
668    std::vector<std::string> expected_output_values =
669        StringsToVector(test[i].expected_output_values);
670
671    std::vector<std::string> output_values =
672        DataReductionProxyTamperDetection::GetHeaderValues(headers.get(),
673                                                           test[i].header_name);
674    EXPECT_EQ(expected_output_values, output_values) << test[i].label;
675  }
676}
677
678// Tests main function DetectAndReport.
679TEST_F(DataReductionProxyTamperDetectionTest, DetectAndReport) {
680  struct {
681    std::string label;
682    std::string raw_header;
683    bool expected_tampered_with;
684  } test[] = {
685    {
686      "Check no fingerprint added case.",
687      "HTTP/1.1 200 OK\n"
688      "Via: a1, b2, 1.1 Chrome-Compression-Proxy\n"
689      "Content-Length: 12345\n"
690      "Chrome-Proxy: bypass=0\n",
691      false,
692    },
693    {
694      "Check the case Chrome-Proxy fingerprint doesn't match.",
695      "HTTP/1.1 200 OK\n"
696      "Via: a1, b2, 1.1 Chrome-Compression-Proxy\n"
697      "Content-Length: 12345\n"
698      "header1: header_1\n"
699      "header2: header_2\n"
700      "header3: header_3\n"
701      "Chrome-Proxy: fcl=12345, "
702      "foh=[header_1,;header_2,;header_3,;]|header1|header2|header3,fvia=0,"
703      "fcp=abc\n",
704      true,
705    },
706    {
707      "Check the case response matches the fingerprint completely.",
708      "HTTP/1.1 200 OK\n"
709      "Via: a1, b2, 1.1 Chrome-Compression-Proxy\n"
710      "Content-Length: 12345\n"
711      "header1: header_1\n"
712      "header2: header_2\n"
713      "header3: header_3\n"
714      "Chrome-Proxy: fcl=12345, "
715      "foh=[header_1,;header_2,;header_3,;]|header1|header2|header3,"
716      "fvia=0, fcp=[fcl=12345,foh=[header_1,;header_2,;header_3,;]"
717      "|header1|header2|header3,fvia=0,]\n",
718      false,
719    },
720    {
721      "Check the case that Content-Length doesn't match.",
722      "HTTP/1.1 200 OK\n"
723      "Via: a1, b2, 1.1 Chrome-Compression-Proxy\n"
724      "Content-Length: 0\n"
725      "header1: header_1\n"
726      "header2: header_2\n"
727      "header3: header_3\n"
728      "Chrome-Proxy: fcl=12345, "
729      "foh=[header_1,;header_2,;header_3,;]|header1|header2|header3, fvia=0, "
730      "fcp=[fcl=12345,foh=[header_1,;header_2,;header_3,;]|"
731      "header1|header2|header3,fvia=0,]\n",
732      true,
733    },
734  };
735
736  InitEnv();
737
738  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test); ++i) {
739    std::string raw_headers(test[i].raw_header);
740    ReplaceWithEncodedString(&raw_headers);
741    HeadersToRaw(&raw_headers);
742    scoped_refptr<net::HttpResponseHeaders> headers(
743        new net::HttpResponseHeaders(raw_headers));
744
745    EXPECT_EQ(
746        test[i].expected_tampered_with,
747        DataReductionProxyTamperDetection::DetectAndReport(headers.get(), true))
748        << test[i].label;
749  }
750}
751
752} // namespace
753