Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(728)

Side by Side Diff: google_apis/gaia/fake_gaia.cc

Issue 99863007: Added CrOS-specific OAuth2 browser test. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« google_apis/gaia/fake_gaia.h ('K') | « google_apis/gaia/fake_gaia.h ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "google_apis/gaia/fake_gaia.h" 5 #include "google_apis/gaia/fake_gaia.h"
6 6
7 #include "base/base_paths.h" 7 #include "base/base_paths.h"
8 #include "base/bind.h"
9 #include "base/bind_helpers.h"
8 #include "base/file_util.h" 10 #include "base/file_util.h"
9 #include "base/files/file_path.h" 11 #include "base/files/file_path.h"
10 #include "base/json/json_writer.h" 12 #include "base/json/json_writer.h"
11 #include "base/logging.h" 13 #include "base/logging.h"
12 #include "base/path_service.h" 14 #include "base/path_service.h"
13 #include "base/strings/string_number_conversions.h" 15 #include "base/strings/string_number_conversions.h"
14 #include "base/strings/string_split.h" 16 #include "base/strings/string_split.h"
15 #include "base/strings/string_util.h" 17 #include "base/strings/string_util.h"
18 #include "base/strings/stringprintf.h"
16 #include "base/values.h" 19 #include "base/values.h"
17 #include "google_apis/gaia/gaia_urls.h" 20 #include "google_apis/gaia/gaia_urls.h"
18 #include "net/base/url_util.h" 21 #include "net/base/url_util.h"
19 #include "net/http/http_status_code.h" 22 #include "net/http/http_status_code.h"
20 #include "net/test/embedded_test_server/http_request.h" 23 #include "net/test/embedded_test_server/http_request.h"
21 #include "net/test/embedded_test_server/http_response.h" 24 #include "net/test/embedded_test_server/http_response.h"
22 #include "url/url_parse.h" 25 #include "url/url_parse.h"
23 26
27 #define REGISTER_RESPONSE_HANDLER(url, method) \
28 request_handlers_.insert(std::make_pair( \
29 url.path(), base::Bind(&FakeGaia::method, base::Unretained(this))))
30
24 using namespace net::test_server; 31 using namespace net::test_server;
25 32
26 namespace { 33 namespace {
34
27 const base::FilePath::CharType kServiceLogin[] = 35 const base::FilePath::CharType kServiceLogin[] =
28 FILE_PATH_LITERAL("google_apis/test/service_login.html"); 36 FILE_PATH_LITERAL("google_apis/test/service_login.html");
37
38 // OAuth2 Authentication header value prefix.
39 const char kAuthHeaderBearer[] = "Bearer ";
40 const char kAuthHeaderOAuth[] = "OAuth ";
41
29 } 42 }
30 43
31 FakeGaia::AccessTokenInfo::AccessTokenInfo() 44 FakeGaia::AccessTokenInfo::AccessTokenInfo()
32 : expires_in(3600) {} 45 : expires_in(3600) {}
33 46
34 FakeGaia::AccessTokenInfo::~AccessTokenInfo() {} 47 FakeGaia::AccessTokenInfo::~AccessTokenInfo() {}
35 48
36 FakeGaia::FakeGaia() { 49 FakeGaia::FakeGaia() {
37 base::FilePath source_root_dir; 50 base::FilePath source_root_dir;
38 PathService::Get(base::DIR_SOURCE_ROOT, &source_root_dir); 51 PathService::Get(base::DIR_SOURCE_ROOT, &source_root_dir);
39 CHECK(base::ReadFileToString( 52 CHECK(base::ReadFileToString(
40 source_root_dir.Append(base::FilePath(kServiceLogin)), 53 source_root_dir.Append(base::FilePath(kServiceLogin)),
41 &service_login_response_)); 54 &service_login_response_));
42 } 55 }
43 56
44 FakeGaia::~FakeGaia() {} 57 FakeGaia::~FakeGaia() {}
45 58
59 void FakeGaia::SetAuthTokens(const std::string& auth_code,
60 const std::string& refresh_token,
61 const std::string& access_token,
62 const std::string& gaia_uber_token,
63 const std::string& session_sid_cookie,
64 const std::string& session_lsid_cookie) {
65 fake_auth_code_ = auth_code;
66 fake_refresh_token_ = refresh_token;
67 fake_access_token_ = access_token;
68 fake_gaia_uber_token_ = gaia_uber_token;
69 fake_session_sid_cookie_ = session_sid_cookie;
70 fake_session_lsid_cookie_ = session_lsid_cookie;
71 }
72
73 void FakeGaia::Initialize() {
74 GaiaUrls* gaia_urls = GaiaUrls::GetInstance();
75 // Handles /ServiceLogin GAIA call.
76 REGISTER_RESPONSE_HANDLER(
77 gaia_urls->service_login_url(), HandleServiceLogin);
78
79 // Handles /ServiceLoginAuth GAIA call.
80 REGISTER_RESPONSE_HANDLER(
81 gaia_urls->service_login_auth_url(), HandleServiceLoginAuth);
82
83 // Handles /o/oauth2/programmatic_auth GAIA call.
84 REGISTER_RESPONSE_HANDLER(
85 gaia_urls->client_login_to_oauth2_url(), HandleProgramaticAuth);
86
87 // Handles /o/oauth2/token GAIA call.
88 REGISTER_RESPONSE_HANDLER(
89 gaia_urls->oauth2_token_url(), HandleAuthToken);
90
91 // Handles /OAuthLogin GAIA call.
92 REGISTER_RESPONSE_HANDLER(
93 gaia_urls->oauth1_login_url(), HandleOAuthLogin);
94
95 // Handles /MergeSession GAIA call.
96 REGISTER_RESPONSE_HANDLER(
97 gaia_urls->merge_session_url(), HandleMergeSession);
98
99 // Handles /o/oauth2/token GAIA call.
Roger Tawa OOO till Jul 10th 2013/12/16 20:44:37 Same as line 87 above?
zel 2013/12/16 21:27:38 Done.
100 REGISTER_RESPONSE_HANDLER(
101 gaia_urls->oauth2_issue_token_url(), HandleIssueToken);
102
103 // Handles /oauth2/v2/tokeninfo GAIA call.
104 REGISTER_RESPONSE_HANDLER(
105 gaia_urls->oauth2_token_info_url(), HandleTokenInfo);
106 }
107
108 void FakeGaia::HandleProgramaticAuth(
109 const HttpRequest& request,
110 BasicHttpResponse* http_response) {
111 GaiaUrls* gaia_urls = GaiaUrls::GetInstance();
112 std::string scope;
113 if (!GetQueryParameter(request.content, "scope", &scope) ||
114 gaia_urls->oauth1_login_scope() != scope) {
115 http_response->set_code(net::HTTP_BAD_REQUEST);
116 return;
117 }
118
119 std::string client_id;
120 if (!GetQueryParameter(request.content, "client_id", &client_id) ||
121 gaia_urls->oauth2_chrome_client_id() != client_id) {
122 http_response->set_code(net::HTTP_BAD_REQUEST);
123 return;
124 }
125
126 http_response->AddCustomHeader(
127 "Set-Cookie",
128 base::StringPrintf(
129 "oauth_code=%s; Path=/o/GetOAuth2Token; Secure; HttpOnly;",
130 fake_auth_code_.c_str()));
131 http_response->set_code(net::HTTP_OK);
132 http_response->set_content_type("text/html");
133 }
134
135 void FakeGaia::HandleServiceLogin(const HttpRequest& request,
136 BasicHttpResponse* http_response) {
137 http_response->set_code(net::HTTP_OK);
138 http_response->set_content(service_login_response_);
139 http_response->set_content_type("text/html");
140 }
141
142 void FakeGaia::HandleOAuthLogin(const HttpRequest& request,
143 BasicHttpResponse* http_response) {
144 http_response->set_code(net::HTTP_BAD_REQUEST);
145 std::string access_token;
146 if (!GetAccessToken(request, kAuthHeaderOAuth, &access_token)) {
147 LOG(ERROR) << "/OAuthLogin missing access token in the header";
148 return;
149 }
150
151 GURL request_url = GURL("http://localhost").Resolve(request.relative_url);
152 std::string request_query = request_url.query();
153
154 std::string source;
155 if (!GetQueryParameter(request_query, "source", &source)) {
156 LOG(ERROR) << "Missing 'source' param in /OAuthLogin call";
157 return;
158 }
159
160 std::string issue_uberauth;
161 if (GetQueryParameter(request_query, "issueuberauth", &issue_uberauth) &&
162 issue_uberauth == "1") {
163 http_response->set_content(fake_gaia_uber_token_);
164 http_response->set_code(net::HTTP_OK);
165 // Issue GAIA uber token.
166 } else {
167 LOG(FATAL) << "/OAuthLogin for SID/LSID is not supported";
168 }
169 }
170
171 void FakeGaia::HandleMergeSession(const HttpRequest& request,
172 BasicHttpResponse* http_response) {
173 http_response->set_code(net::HTTP_BAD_REQUEST);
174
175 std::string uber_token;
176 if (!GetQueryParameter(request.content, "uberauth", &uber_token) ||
177 uber_token != fake_gaia_uber_token_) {
178 LOG(ERROR) << "Missing or invalid 'uberauth' param in /MergeSession call";
179 return;
180 }
181
182 std::string continue_url;
183 if (!GetQueryParameter(request.content, "continue", &continue_url)) {
184 LOG(ERROR) << "Missing or invalid 'continue' param in /MergeSession call";
185 return;
186 }
187
188 std::string source;
189 if (!GetQueryParameter(request.content, "source", &source)) {
190 LOG(ERROR) << "Missing or invalid 'source' param in /MergeSession call";
191 return;
192 }
193
194 http_response->AddCustomHeader(
195 "Set-Cookie",
196 base::StringPrintf(
197 "SID=%s; LSID=%s; Path=/; Secure; HttpOnly;",
198 fake_session_sid_cookie_.c_str(),
199 fake_session_lsid_cookie_.c_str()));
200 // TODO(zelidrag): Not used now.
201 http_response->set_content("OK");
202 http_response->set_code(net::HTTP_OK);
203 }
204
205
206 void FakeGaia::HandleServiceLoginAuth(const HttpRequest& request,
207 BasicHttpResponse* http_response) {
208 std::string continue_url =
209 GaiaUrls::GetInstance()->service_login_url().spec();
210 GetQueryParameter(request.content, "continue", &continue_url);
211 http_response->set_code(net::HTTP_OK);
212 const std::string redirect_js =
213 "document.location.href = '" + continue_url + "'";
214 http_response->set_content(
215 "<HTML><HEAD><SCRIPT>\n" + redirect_js + "\n</SCRIPT></HEAD></HTML>");
216 http_response->set_content_type("text/html");
217 }
218
219 void FakeGaia::HandleAuthToken(const HttpRequest& request,
220 BasicHttpResponse* http_response) {
221 std::string grant_type;
222 std::string refresh_token;
223 std::string client_id;
224 std::string scope;
225 std::string auth_code;
226 const AccessTokenInfo* token_info = NULL;
227 GetQueryParameter(request.content, "scope", &scope);
228
229 if (!GetQueryParameter(request.content, "grant_type", &grant_type)) {
230 http_response->set_code(net::HTTP_BAD_REQUEST);
231 LOG(ERROR) << "No 'grant_type' param in /o/oauth2/token";
232 return;
233 }
234
235 if (grant_type == "authorization_code") {
236 if (!GetQueryParameter(request.content, "code", &auth_code) ||
237 auth_code != fake_auth_code_) {
238 http_response->set_code(net::HTTP_BAD_REQUEST);
239 LOG(ERROR) << "No 'code' param in /o/oauth2/token";
240 return;
241 }
242
243 if (GaiaUrls::GetInstance()->oauth1_login_scope() != scope) {
244 http_response->set_code(net::HTTP_BAD_REQUEST);
245 LOG(ERROR) << "Invalid scope for /o/oauth2/token - " << scope;
246 return;
247 }
248
249 base::DictionaryValue response_dict;
250 response_dict.SetString("refresh_token", fake_refresh_token_);
251 response_dict.SetString("access_token", fake_access_token_);
252 response_dict.SetInteger("expires_in", 3600);
253 FormatJSONResponse(response_dict, http_response);
254 } else if (GetQueryParameter(request.content,
255 "refresh_token",
256 &refresh_token) &&
257 GetQueryParameter(request.content,
258 "client_id",
259 &client_id) &&
260 (token_info = FindAccessTokenInfo(refresh_token,
261 client_id,
262 scope))) {
263 base::DictionaryValue response_dict;
264 response_dict.SetString("access_token", token_info->token);
265 response_dict.SetInteger("expires_in", 3600);
266 FormatJSONResponse(response_dict, http_response);
267 } else {
268 LOG(ERROR) << "Bad request for /o/oauth2/token - "
269 << "refresh_token = " << refresh_token
270 << ", scope = " << scope
271 << ", client_id = " << client_id;
272 http_response->set_code(net::HTTP_BAD_REQUEST);
273 }
274 }
275
276 void FakeGaia::HandleTokenInfo(const HttpRequest& request,
277 BasicHttpResponse* http_response) {
278 const AccessTokenInfo* token_info = NULL;
279 std::string access_token;
280 if (GetQueryParameter(request.content, "access_token", &access_token)) {
281 for (AccessTokenInfoMap::const_iterator entry(
282 access_token_info_map_.begin());
283 entry != access_token_info_map_.end();
284 ++entry) {
285 if (entry->second.token == access_token) {
286 token_info = &(entry->second);
287 break;
288 }
289 }
290 }
291
292 if (token_info) {
293 base::DictionaryValue response_dict;
294 response_dict.SetString("issued_to", token_info->issued_to);
295 response_dict.SetString("audience", token_info->audience);
296 response_dict.SetString("user_id", token_info->user_id);
297 std::vector<std::string> scope_vector(token_info->scopes.begin(),
298 token_info->scopes.end());
299 response_dict.SetString("scope", JoinString(scope_vector, " "));
300 response_dict.SetInteger("expires_in", token_info->expires_in);
301 response_dict.SetString("email", token_info->email);
302 FormatJSONResponse(response_dict, http_response);
303 } else {
304 http_response->set_code(net::HTTP_BAD_REQUEST);
305 }
306 }
307
308 void FakeGaia::HandleIssueToken(const HttpRequest& request,
309 BasicHttpResponse* http_response) {
310 std::string access_token;
311 std::string scope;
312 std::string client_id;
313 const AccessTokenInfo* token_info = NULL;
314 if (GetAccessToken(request, kAuthHeaderBearer, &access_token) &&
315 GetQueryParameter(request.content, "scope", &scope) &&
316 GetQueryParameter(request.content, "client_id", &client_id) &&
317 (token_info = FindAccessTokenInfo(access_token, client_id, scope))) {
318 base::DictionaryValue response_dict;
319 response_dict.SetString("issueAdvice", "auto");
320 response_dict.SetString("expiresIn",
321 base::IntToString(token_info->expires_in));
322 response_dict.SetString("token", token_info->token);
323 FormatJSONResponse(response_dict, http_response);
324 } else {
325 http_response->set_code(net::HTTP_BAD_REQUEST);
326 }
327 }
328
329
46 scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) { 330 scoped_ptr<HttpResponse> FakeGaia::HandleRequest(const HttpRequest& request) {
47 GaiaUrls* gaia_urls = GaiaUrls::GetInstance();
48
49 // The scheme and host of the URL is actually not important but required to 331 // The scheme and host of the URL is actually not important but required to
50 // get a valid GURL in order to parse |request.relative_url|. 332 // get a valid GURL in order to parse |request.relative_url|.
51 GURL request_url = GURL("http://localhost").Resolve(request.relative_url); 333 GURL request_url = GURL("http://localhost").Resolve(request.relative_url);
52 std::string request_path = request_url.path(); 334 std::string request_path = request_url.path();
53
54 scoped_ptr<BasicHttpResponse> http_response(new BasicHttpResponse()); 335 scoped_ptr<BasicHttpResponse> http_response(new BasicHttpResponse());
55 if (request_path == gaia_urls->service_login_url().path()) { 336 RequestHandlerMap::iterator iter = request_handlers_.find(request_path);
56 http_response->set_code(net::HTTP_OK); 337 if (iter != request_handlers_.end()) {
57 http_response->set_content(service_login_response_); 338 LOG(WARNING) << "Serving request " << request_path;
58 http_response->set_content_type("text/html"); 339 iter->second.Run(request, http_response.get());
59 } else if (request_path == gaia_urls->service_login_auth_url().path()) { 340 } else {
60 std::string continue_url = gaia_urls->service_login_url().spec(); 341 LOG(ERROR) << "Unhandled request " << request_path;
61 GetQueryParameter(request.content, "continue", &continue_url); 342 return scoped_ptr<HttpResponse>(); // Request not understood.
62 http_response->set_code(net::HTTP_OK);
63 const std::string redirect_js =
64 "document.location.href = '" + continue_url + "'";
65 http_response->set_content(
66 "<HTML><HEAD><SCRIPT>\n" + redirect_js + "\n</SCRIPT></HEAD></HTML>");
67 http_response->set_content_type("text/html");
68 } else if (request_path == gaia_urls->oauth2_token_url().path()) {
69 std::string refresh_token;
70 std::string client_id;
71 std::string scope;
72 const AccessTokenInfo* token_info = NULL;
73 GetQueryParameter(request.content, "scope", &scope);
74 if (GetQueryParameter(request.content, "refresh_token", &refresh_token) &&
75 GetQueryParameter(request.content, "client_id", &client_id) &&
76 (token_info = GetAccessTokenInfo(refresh_token, client_id, scope))) {
77 base::DictionaryValue response_dict;
78 response_dict.SetString("access_token", token_info->token);
79 response_dict.SetInteger("expires_in", 3600);
80 FormatJSONResponse(response_dict, http_response.get());
81 } else {
82 http_response->set_code(net::HTTP_BAD_REQUEST);
83 }
84 } else if (request_path == gaia_urls->oauth2_token_info_url().path()) {
85 const AccessTokenInfo* token_info = NULL;
86 std::string access_token;
87 if (GetQueryParameter(request.content, "access_token", &access_token)) {
88 for (AccessTokenInfoMap::const_iterator entry(
89 access_token_info_map_.begin());
90 entry != access_token_info_map_.end();
91 ++entry) {
92 if (entry->second.token == access_token) {
93 token_info = &(entry->second);
94 break;
95 }
96 }
97 }
98
99 if (token_info) {
100 base::DictionaryValue response_dict;
101 response_dict.SetString("issued_to", token_info->issued_to);
102 response_dict.SetString("audience", token_info->audience);
103 response_dict.SetString("user_id", token_info->user_id);
104 std::vector<std::string> scope_vector(token_info->scopes.begin(),
105 token_info->scopes.end());
106 response_dict.SetString("scope", JoinString(scope_vector, " "));
107 response_dict.SetInteger("expires_in", token_info->expires_in);
108 response_dict.SetString("email", token_info->email);
109 FormatJSONResponse(response_dict, http_response.get());
110 } else {
111 http_response->set_code(net::HTTP_BAD_REQUEST);
112 }
113 } else if (request_path == gaia_urls->oauth2_issue_token_url().path()) {
114 std::string access_token;
115 std::map<std::string, std::string>::const_iterator auth_header_entry =
116 request.headers.find("Authorization");
117 if (auth_header_entry != request.headers.end()) {
118 if (StartsWithASCII(auth_header_entry->second, "Bearer ", true))
119 access_token = auth_header_entry->second.substr(7);
120 }
121
122 std::string scope;
123 std::string client_id;
124 const AccessTokenInfo* token_info = NULL;
125 if (GetQueryParameter(request.content, "scope", &scope) &&
126 GetQueryParameter(request.content, "client_id", &client_id) &&
127 (token_info = GetAccessTokenInfo(access_token, client_id, scope))) {
128 base::DictionaryValue response_dict;
129 response_dict.SetString("issueAdvice", "auto");
130 response_dict.SetString("expiresIn",
131 base::IntToString(token_info->expires_in));
132 response_dict.SetString("token", token_info->token);
133 FormatJSONResponse(response_dict, http_response.get());
134 } else {
135 http_response->set_code(net::HTTP_BAD_REQUEST);
136 }
137 } else {
138 // Request not understood.
139 return scoped_ptr<HttpResponse>();
140 } 343 }
141 344
142 return http_response.PassAs<HttpResponse>(); 345 return http_response.PassAs<HttpResponse>();
143 } 346 }
144 347
145 void FakeGaia::IssueOAuthToken(const std::string& auth_token, 348 void FakeGaia::IssueOAuthToken(const std::string& auth_token,
146 const AccessTokenInfo& token_info) { 349 const AccessTokenInfo& token_info) {
147 access_token_info_map_.insert(std::make_pair(auth_token, token_info)); 350 access_token_info_map_.insert(std::make_pair(auth_token, token_info));
148 } 351 }
149 352
150 void FakeGaia::FormatJSONResponse(const base::DictionaryValue& response_dict, 353 void FakeGaia::FormatJSONResponse(const base::DictionaryValue& response_dict,
151 BasicHttpResponse* http_response) { 354 BasicHttpResponse* http_response) {
152 std::string response_json; 355 std::string response_json;
153 base::JSONWriter::Write(&response_dict, &response_json); 356 base::JSONWriter::Write(&response_dict, &response_json);
154 http_response->set_content(response_json); 357 http_response->set_content(response_json);
155 http_response->set_code(net::HTTP_OK); 358 http_response->set_code(net::HTTP_OK);
156 } 359 }
157 360
158 const FakeGaia::AccessTokenInfo* FakeGaia::GetAccessTokenInfo( 361 const FakeGaia::AccessTokenInfo* FakeGaia::FindAccessTokenInfo(
159 const std::string& auth_token, 362 const std::string& auth_token,
160 const std::string& client_id, 363 const std::string& client_id,
161 const std::string& scope_string) const { 364 const std::string& scope_string) const {
162 if (auth_token.empty() || client_id.empty()) 365 if (auth_token.empty() || client_id.empty())
163 return NULL; 366 return NULL;
164 367
165 std::vector<std::string> scope_list; 368 std::vector<std::string> scope_list;
166 base::SplitString(scope_string, ' ', &scope_list); 369 base::SplitString(scope_string, ' ', &scope_list);
167 ScopeSet scopes(scope_list.begin(), scope_list.end()); 370 ScopeSet scopes(scope_list.begin(), scope_list.end());
168 371
(...skipping 12 matching lines...) Expand all
181 384
182 // static 385 // static
183 bool FakeGaia::GetQueryParameter(const std::string& query, 386 bool FakeGaia::GetQueryParameter(const std::string& query,
184 const std::string& key, 387 const std::string& key,
185 std::string* value) { 388 std::string* value) {
186 // Name and scheme actually don't matter, but are required to get a valid URL 389 // Name and scheme actually don't matter, but are required to get a valid URL
187 // for parsing. 390 // for parsing.
188 GURL query_url("http://localhost?" + query); 391 GURL query_url("http://localhost?" + query);
189 return net::GetValueForKeyInQuery(query_url, key, value); 392 return net::GetValueForKeyInQuery(query_url, key, value);
190 } 393 }
394
395 // static
396 bool FakeGaia::GetAccessToken(const HttpRequest& request,
397 const char* auth_token_prefix,
398 std::string* access_token) {
399 std::map<std::string, std::string>::const_iterator auth_header_entry =
400 request.headers.find("Authorization");
401 if (auth_header_entry != request.headers.end()) {
402 if (StartsWithASCII(auth_header_entry->second, auth_token_prefix, true)) {
403 *access_token = auth_header_entry->second.substr(
404 strlen(auth_token_prefix));
405 return true;
406 }
407 }
408
409 return false;
410 }
OLDNEW
« google_apis/gaia/fake_gaia.h ('K') | « google_apis/gaia/fake_gaia.h ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698