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 "net/quic/quic_in_memory_cache.h"
6
7#include "base/file_util.h"
8#include "base/files/file_enumerator.h"
9#include "base/stl_util.h"
10#include "base/strings/string_number_conversions.h"
11#include "net/tools/balsa/balsa_headers.h"
12
13using base::FilePath;
14using base::StringPiece;
15using std::string;
16
17// Specifies the directory used during QuicInMemoryCache
18// construction to seed the cache. Cache directory can be
19// generated using `wget -p --save-headers <url>
20
21namespace net {
22
23namespace {
24
25const FilePath::CharType* g_quic_in_memory_cache_dir = FILE_PATH_LITERAL("");
26
27// BalsaVisitor implementation (glue) which caches response bodies.
28class CachingBalsaVisitor : public NoOpBalsaVisitor {
29 public:
30  CachingBalsaVisitor() : done_framing_(false) {}
31  virtual void ProcessBodyData(const char* input, size_t size) OVERRIDE {
32    AppendToBody(input, size);
33  }
34  virtual void MessageDone() OVERRIDE {
35    done_framing_ = true;
36  }
37  virtual void HandleHeaderError(BalsaFrame* framer) OVERRIDE {
38    UnhandledError();
39  }
40  virtual void HandleHeaderWarning(BalsaFrame* framer) OVERRIDE {
41    UnhandledError();
42  }
43  virtual void HandleChunkingError(BalsaFrame* framer) OVERRIDE {
44    UnhandledError();
45  }
46  virtual void HandleBodyError(BalsaFrame* framer) OVERRIDE {
47    UnhandledError();
48  }
49  void UnhandledError() {
50    LOG(DFATAL) << "Unhandled error framing HTTP.";
51  }
52  void AppendToBody(const char* input, size_t size) {
53    body_.append(input, size);
54  }
55  bool done_framing() const { return done_framing_; }
56  const string& body() const { return body_; }
57
58 private:
59  bool done_framing_;
60  string body_;
61};
62
63}  // namespace
64
65// static
66QuicInMemoryCache* QuicInMemoryCache::GetInstance() {
67  return Singleton<QuicInMemoryCache>::get();
68}
69
70const QuicInMemoryCache::Response* QuicInMemoryCache::GetResponse(
71    const BalsaHeaders& request_headers) const {
72  ResponseMap::const_iterator it = responses_.find(GetKey(request_headers));
73  if (it == responses_.end()) {
74    return NULL;
75  }
76  return it->second;
77}
78
79void QuicInMemoryCache::AddSimpleResponse(StringPiece method,
80                                          StringPiece path,
81                                          StringPiece version,
82                                          StringPiece response_code,
83                                          StringPiece response_detail,
84                                          StringPiece body) {
85  BalsaHeaders request_headers, response_headers;
86  request_headers.SetRequestFirstlineFromStringPieces(method,
87                                                      path,
88                                                      version);
89  response_headers.SetRequestFirstlineFromStringPieces(version,
90                                                       response_code,
91                                                       response_detail);
92  response_headers.AppendHeader("content-length",
93                                base::IntToString(body.length()));
94
95  AddResponse(request_headers, response_headers, body);
96}
97
98void QuicInMemoryCache::AddResponse(const BalsaHeaders& request_headers,
99                                    const BalsaHeaders& response_headers,
100                                    StringPiece response_body) {
101  VLOG(1) << "Adding response for: " << GetKey(request_headers);
102  if (ContainsKey(responses_, GetKey(request_headers))) {
103    LOG(DFATAL) << "Response for given request already exists!";
104    return;
105  }
106  Response* new_response = new Response();
107  new_response->set_headers(response_headers);
108  new_response->set_body(response_body);
109  responses_[GetKey(request_headers)] = new_response;
110}
111
112void QuicInMemoryCache::AddSpecialResponse(StringPiece method,
113                                           StringPiece path,
114                                           StringPiece version,
115                                           SpecialResponseType response_type) {
116  BalsaHeaders request_headers, response_headers;
117  request_headers.SetRequestFirstlineFromStringPieces(method,
118                                                      path,
119                                                      version);
120  AddResponse(request_headers, response_headers, "");
121  responses_[GetKey(request_headers)]->response_type_ = response_type;
122}
123
124QuicInMemoryCache::QuicInMemoryCache() {
125  Initialize();
126}
127
128void QuicInMemoryCache::ResetForTests() {
129  STLDeleteValues(&responses_);
130  Initialize();
131}
132
133void QuicInMemoryCache::Initialize() {
134  // If there's no defined cache dir, we have no initialization to do.
135  if (g_quic_in_memory_cache_dir[0] == '\0') {
136    VLOG(1) << "No cache directory found. Skipping initialization.";
137    return;
138  }
139  VLOG(1) << "Attempting to initialize QuicInMemoryCache from directory: "
140          << g_quic_in_memory_cache_dir;
141
142  FilePath directory(g_quic_in_memory_cache_dir);
143  base::FileEnumerator file_list(directory,
144                                 true,
145                                 base::FileEnumerator::FILES);
146
147  FilePath file = file_list.Next();
148  while (!file.empty()) {
149    // Need to skip files in .svn directories
150    if (file.value().find(FILE_PATH_LITERAL("/.svn/")) != std::string::npos) {
151      file = file_list.Next();
152      continue;
153    }
154
155    BalsaHeaders request_headers, response_headers;
156
157    string file_contents;
158    base::ReadFileToString(file, &file_contents);
159
160    // Frame HTTP.
161    CachingBalsaVisitor caching_visitor;
162    BalsaFrame framer;
163    framer.set_balsa_headers(&response_headers);
164    framer.set_balsa_visitor(&caching_visitor);
165    size_t processed = 0;
166    while (processed < file_contents.length() &&
167           !caching_visitor.done_framing()) {
168      processed += framer.ProcessInput(file_contents.c_str() + processed,
169                                       file_contents.length() - processed);
170    }
171
172    string response_headers_str;
173    response_headers.DumpToString(&response_headers_str);
174    if (!caching_visitor.done_framing()) {
175      LOG(DFATAL) << "Did not frame entire message from file: " << file.value()
176                  << " (" << processed << " of " << file_contents.length()
177                  << " bytes).";
178    }
179    if (processed < file_contents.length()) {
180      // Didn't frame whole file. Assume remainder is body.
181      // This sometimes happens as a result of incompatibilities between
182      // BalsaFramer and wget's serialization of HTTP sans content-length.
183      caching_visitor.AppendToBody(file_contents.c_str() + processed,
184                                   file_contents.length() - processed);
185      processed += file_contents.length();
186    }
187
188    StringPiece base = file.AsUTF8Unsafe();
189    if (response_headers.HasHeader("X-Original-Url")) {
190      base = response_headers.GetHeader("X-Original-Url");
191      response_headers.RemoveAllOfHeader("X-Original-Url");
192      // Remove the protocol so that the string is of the form host + path,
193      // which is parsed properly below.
194      if (StringPieceUtils::StartsWithIgnoreCase(base, "https://")) {
195        base.remove_prefix(8);
196      } else if (StringPieceUtils::StartsWithIgnoreCase(base, "http://")) {
197        base.remove_prefix(7);
198      }
199    }
200    int path_start = base.find_first_of('/');
201    DCHECK_LT(0, path_start);
202    StringPiece host(base.substr(0, path_start));
203    StringPiece path(base.substr(path_start));
204    if (path[path.length() - 1] == ',') {
205      path.remove_suffix(1);
206    }
207    // Set up request headers. Assume method is GET and protocol is HTTP/1.1.
208    request_headers.SetRequestFirstlineFromStringPieces("GET",
209                                                        path,
210                                                        "HTTP/1.1");
211    request_headers.ReplaceOrAppendHeader("host", host);
212
213    VLOG(1) << "Inserting 'http://" << GetKey(request_headers)
214            << "' into QuicInMemoryCache.";
215
216    AddResponse(request_headers, response_headers, caching_visitor.body());
217
218    file = file_list.Next();
219  }
220}
221
222QuicInMemoryCache::~QuicInMemoryCache() {
223  STLDeleteValues(&responses_);
224}
225
226string QuicInMemoryCache::GetKey(const BalsaHeaders& request_headers) const {
227  StringPiece uri = request_headers.request_uri();
228  if (uri.size() == 0) {
229    return "";
230  }
231  StringPiece host;
232  if (uri[0] == '/') {
233    host = request_headers.GetHeader("host");
234  } else if (StringPieceUtils::StartsWithIgnoreCase(uri, "https://")) {
235    uri.remove_prefix(8);
236  } else if (StringPieceUtils::StartsWithIgnoreCase(uri, "http://")) {
237    uri.remove_prefix(7);
238  }
239  return host.as_string() + uri.as_string();
240}
241
242}  // namespace net
243