1// Copyright 2013 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 "cloud_print/gcp20/prototype/privet_http_server.h"
6
7#include "base/command_line.h"
8#include "base/json/json_writer.h"
9#include "base/strings/stringprintf.h"
10#include "cloud_print/gcp20/prototype/gcp20_switches.h"
11#include "net/base/ip_endpoint.h"
12#include "net/base/net_errors.h"
13#include "net/base/url_util.h"
14#include "net/socket/tcp_server_socket.h"
15#include "url/gurl.h"
16
17namespace {
18
19const int kDeviceBusyTimeout = 30;  // in seconds
20const int kPendingUserActionTimeout = 5;  // in seconds
21
22const char kPrivetInfo[] = "/privet/info";
23const char kPrivetRegister[] = "/privet/register";
24const char kPrivetCapabilities[] = "/privet/capabilities";
25const char kPrivetPrinterCreateJob[] = "/privet/printer/createjob";
26const char kPrivetPrinterSubmitDoc[] = "/privet/printer/submitdoc";
27const char kPrivetPrinterJobState[] = "/privet/printer/jobstate";
28
29// {"error":|error_type|}
30scoped_ptr<base::DictionaryValue> CreateError(const std::string& error_type) {
31  scoped_ptr<base::DictionaryValue> error(new base::DictionaryValue);
32  error->SetString("error", error_type);
33
34  return error.Pass();
35}
36
37// {"error":|error_type|, "description":|description|}
38scoped_ptr<base::DictionaryValue> CreateErrorWithDescription(
39    const std::string& error_type,
40    const std::string& description) {
41  scoped_ptr<base::DictionaryValue> error(CreateError(error_type));
42  error->SetString("description", description);
43  return error.Pass();
44}
45
46// {"error":|error_type|, "timeout":|timeout|}
47scoped_ptr<base::DictionaryValue> CreateErrorWithTimeout(
48    const std::string& error_type,
49    int timeout) {
50  scoped_ptr<base::DictionaryValue> error(CreateError(error_type));
51  error->SetInteger("timeout", timeout);
52  return error.Pass();
53}
54
55// Converts state to string.
56std::string LocalPrintJobStateToString(LocalPrintJob::State state) {
57  switch (state) {
58    case LocalPrintJob::STATE_DRAFT:
59      return "draft";
60      break;
61    case LocalPrintJob::STATE_ABORTED:
62      return "done";
63      break;
64    case LocalPrintJob::STATE_DONE:
65      return "done";
66      break;
67    default:
68      NOTREACHED();
69      return std::string();
70  }
71}
72
73
74// Returns |true| if |request| should be GET method.
75bool IsGetMethod(const std::string& request) {
76  return request == kPrivetInfo ||
77         request == kPrivetCapabilities ||
78         request == kPrivetPrinterJobState;
79}
80
81// Returns |true| if |request| should be POST method.
82bool IsPostMethod(const std::string& request) {
83  return request == kPrivetRegister ||
84         request == kPrivetPrinterCreateJob ||
85         request == kPrivetPrinterSubmitDoc;
86}
87
88}  // namespace
89
90PrivetHttpServer::DeviceInfo::DeviceInfo() : uptime(0) {
91}
92
93PrivetHttpServer::DeviceInfo::~DeviceInfo() {
94}
95
96PrivetHttpServer::PrivetHttpServer(Delegate* delegate)
97    : port_(0),
98      delegate_(delegate) {
99}
100
101PrivetHttpServer::~PrivetHttpServer() {
102  Shutdown();
103}
104
105bool PrivetHttpServer::Start(uint16 port) {
106  if (server_)
107    return true;
108
109  scoped_ptr<net::ServerSocket> server_socket(
110      new net::TCPServerSocket(NULL, net::NetLog::Source()));
111  std::string listen_address = "::";
112  if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kDisableIpv6))
113    listen_address = "0.0.0.0";
114  server_socket->ListenWithAddressAndPort(listen_address, port, 1);
115  server_.reset(new net::HttpServer(server_socket.Pass(), this));
116
117  net::IPEndPoint address;
118  if (server_->GetLocalAddress(&address) != net::OK) {
119    NOTREACHED() << "Cannot start HTTP server";
120    return false;
121  }
122
123  VLOG(1) << "Address of HTTP server: " << address.ToString();
124  return true;
125}
126
127void PrivetHttpServer::Shutdown() {
128  if (!server_)
129    return;
130
131  server_.reset(NULL);
132}
133
134void PrivetHttpServer::OnHttpRequest(int connection_id,
135                                     const net::HttpServerRequestInfo& info) {
136  VLOG(1) << "Processing HTTP request: " << info.path;
137  GURL url("http://host" + info.path);
138
139  if (!ValidateRequestMethod(connection_id, url.path(), info.method))
140    return;
141
142  if (!CommandLine::ForCurrentProcess()->HasSwitch(switches::kDisableXTocken)) {
143    net::HttpServerRequestInfo::HeadersMap::const_iterator iter =
144        info.headers.find("x-privet-token");
145    if (iter == info.headers.end()) {
146      server_->Send(connection_id, net::HTTP_BAD_REQUEST,
147                    "Missing X-Privet-Token header.\n"
148                    "TODO: Message should be in header, not in the body!",
149                    "text/plain");
150      return;
151    }
152
153    if (url.path() != kPrivetInfo &&
154        !delegate_->CheckXPrivetTokenHeader(iter->second)) {
155      server_->Send(connection_id, net::HTTP_OK,
156                    "{\"error\":\"invalid_x_privet_token\"}",
157                    "application/json");
158      return;
159    }
160  }
161
162  std::string response;
163  net::HttpStatusCode status_code = ProcessHttpRequest(url, info, &response);
164
165  server_->Send(connection_id, status_code, response, "application/json");
166}
167
168void PrivetHttpServer::OnWebSocketRequest(
169    int connection_id,
170    const net::HttpServerRequestInfo& info) {
171}
172
173void PrivetHttpServer::OnWebSocketMessage(int connection_id,
174                                          const std::string& data) {
175}
176
177void PrivetHttpServer::OnClose(int connection_id) {
178}
179
180void PrivetHttpServer::ReportInvalidMethod(int connection_id) {
181  server_->Send(connection_id, net::HTTP_BAD_REQUEST, "Invalid method",
182                "text/plain");
183}
184
185bool PrivetHttpServer::ValidateRequestMethod(int connection_id,
186                                             const std::string& request,
187                                             const std::string& method) {
188  DCHECK(!IsGetMethod(request) || !IsPostMethod(request));
189
190  if (!IsGetMethod(request) && !IsPostMethod(request)) {
191    server_->Send404(connection_id);
192    return false;
193  }
194
195  if (CommandLine::ForCurrentProcess()->HasSwitch("disable-method-check"))
196    return true;
197
198  if ((IsGetMethod(request) && method != "GET") ||
199      (IsPostMethod(request) && method != "POST")) {
200    ReportInvalidMethod(connection_id);
201    return false;
202  }
203
204  return true;
205}
206
207net::HttpStatusCode PrivetHttpServer::ProcessHttpRequest(
208    const GURL& url,
209    const net::HttpServerRequestInfo& info,
210    std::string* response) {
211  net::HttpStatusCode status_code = net::HTTP_OK;
212  scoped_ptr<base::DictionaryValue> json_response;
213
214  if (url.path() == kPrivetInfo) {
215    json_response = ProcessInfo(&status_code);
216  } else if (url.path() == kPrivetRegister) {
217    json_response = ProcessRegister(url, &status_code);
218  } else if (url.path() == kPrivetCapabilities) {
219    json_response = ProcessCapabilities(&status_code);
220  } else if (url.path() == kPrivetPrinterCreateJob) {
221    json_response = ProcessCreateJob(url, info.data, &status_code);
222  } else if (url.path() == kPrivetPrinterSubmitDoc) {
223    json_response = ProcessSubmitDoc(url, info, &status_code);
224  } else if (url.path() == kPrivetPrinterJobState) {
225    json_response = ProcessJobState(url, &status_code);
226  } else {
227    NOTREACHED();
228  }
229
230  if (!json_response) {
231    response->clear();
232    return status_code;
233  }
234
235  base::JSONWriter::WriteWithOptions(json_response.get(),
236                                     base::JSONWriter::OPTIONS_PRETTY_PRINT,
237                                     response);
238  return status_code;
239}
240
241// Privet API methods:
242
243scoped_ptr<base::DictionaryValue> PrivetHttpServer::ProcessInfo(
244    net::HttpStatusCode* status_code) const {
245
246  DeviceInfo info;
247  delegate_->CreateInfo(&info);
248
249  scoped_ptr<base::DictionaryValue> response(new base::DictionaryValue);
250  response->SetString("version", info.version);
251  response->SetString("name", info.name);
252  response->SetString("description", info.description);
253  response->SetString("url", info.url);
254  response->SetString("id", info.id);
255  response->SetString("device_state", info.device_state);
256  response->SetString("connection_state", info.connection_state);
257  response->SetString("manufacturer", info.manufacturer);
258  response->SetString("model", info.model);
259  response->SetString("serial_number", info.serial_number);
260  response->SetString("firmware", info.firmware);
261  response->SetInteger("uptime", info.uptime);
262  response->SetString("x-privet-token", info.x_privet_token);
263
264  base::ListValue api;
265  for (size_t i = 0; i < info.api.size(); ++i)
266    api.AppendString(info.api[i]);
267  response->Set("api", api.DeepCopy());
268
269  base::ListValue type;
270  for (size_t i = 0; i < info.type.size(); ++i)
271    type.AppendString(info.type[i]);
272  response->Set("type", type.DeepCopy());
273
274  *status_code = net::HTTP_OK;
275  return response.Pass();
276}
277
278scoped_ptr<base::DictionaryValue> PrivetHttpServer::ProcessCapabilities(
279    net::HttpStatusCode* status_code) const {
280  if (!delegate_->IsRegistered()) {
281    *status_code = net::HTTP_NOT_FOUND;
282    return scoped_ptr<base::DictionaryValue>();
283  }
284  return make_scoped_ptr(delegate_->GetCapabilities().DeepCopy());
285}
286
287scoped_ptr<base::DictionaryValue> PrivetHttpServer::ProcessCreateJob(
288    const GURL& url,
289    const std::string& body,
290    net::HttpStatusCode* status_code) const {
291  if (!delegate_->IsRegistered() || !delegate_->IsLocalPrintingAllowed()) {
292    *status_code = net::HTTP_NOT_FOUND;
293    return scoped_ptr<base::DictionaryValue>();
294  }
295
296  std::string job_id;
297  // TODO(maksymb): Use base::Time for expiration
298  int expires_in = 0;
299  // TODO(maksymb): Use base::TimeDelta for timeout values
300  int error_timeout = -1;
301  std::string error_description;
302
303  LocalPrintJob::CreateResult result =
304      delegate_->CreateJob(body, &job_id, &expires_in,
305                           &error_timeout, &error_description);
306
307  scoped_ptr<base::DictionaryValue> response;
308  *status_code = net::HTTP_OK;
309  switch (result) {
310    case LocalPrintJob::CREATE_SUCCESS:
311      response.reset(new base::DictionaryValue);
312      response->SetString("job_id", job_id);
313      response->SetInteger("expires_in", expires_in);
314      return response.Pass();
315
316    case LocalPrintJob::CREATE_INVALID_TICKET:
317      return CreateError("invalid_ticket");
318    case LocalPrintJob::CREATE_PRINTER_BUSY:
319      return CreateErrorWithTimeout("printer_busy", error_timeout);
320    case LocalPrintJob::CREATE_PRINTER_ERROR:
321      return CreateErrorWithDescription("printer_error", error_description);
322  }
323  return scoped_ptr<base::DictionaryValue>();
324}
325
326scoped_ptr<base::DictionaryValue> PrivetHttpServer::ProcessSubmitDoc(
327    const GURL& url,
328    const net::HttpServerRequestInfo& info,
329    net::HttpStatusCode* status_code) const {
330  if (!delegate_->IsRegistered() || !delegate_->IsLocalPrintingAllowed()) {
331    *status_code = net::HTTP_NOT_FOUND;
332    return scoped_ptr<base::DictionaryValue>();
333  }
334
335  using net::GetValueForKeyInQuery;
336
337  // Parse query
338  LocalPrintJob job;
339  std::string offline;
340  std::string job_id;
341  bool job_name_present = GetValueForKeyInQuery(url, "job_name", &job.job_name);
342  bool job_id_present = GetValueForKeyInQuery(url, "job_id", &job_id);
343  GetValueForKeyInQuery(url, "client_name", &job.client_name);
344  GetValueForKeyInQuery(url, "user_name", &job.user_name);
345  GetValueForKeyInQuery(url, "offline", &offline);
346  job.offline = (offline == "1");
347  job.content_type = info.GetHeaderValue("content-type");
348  job.content = info.data;
349
350  // Call delegate
351  // TODO(maksymb): Use base::Time for expiration
352  int expires_in = 0;
353  std::string error_description;
354  int timeout;
355  LocalPrintJob::SaveResult status = job_id_present
356      ? delegate_->SubmitDocWithId(job, job_id, &expires_in,
357                                   &error_description, &timeout)
358      : delegate_->SubmitDoc(job, &job_id, &expires_in,
359                             &error_description, &timeout);
360
361  // Create response
362  *status_code = net::HTTP_OK;
363  scoped_ptr<base::DictionaryValue> response(new base::DictionaryValue);
364  switch (status) {
365    case LocalPrintJob::SAVE_SUCCESS:
366      response->SetString("job_id", job_id);
367      response->SetInteger("expires_in", expires_in);
368      response->SetString("job_type", job.content_type);
369      response->SetString(
370          "job_size",
371          base::StringPrintf("%u", static_cast<uint32>(job.content.size())));
372      if (job_name_present)
373        response->SetString("job_name", job.job_name);
374      return response.Pass();
375
376    case LocalPrintJob::SAVE_INVALID_PRINT_JOB:
377      return CreateErrorWithTimeout("invalid_print_job", timeout);
378    case LocalPrintJob::SAVE_INVALID_DOCUMENT_TYPE:
379      return CreateError("invalid_document_type");
380    case LocalPrintJob::SAVE_INVALID_DOCUMENT:
381      return CreateError("invalid_document");
382    case LocalPrintJob::SAVE_DOCUMENT_TOO_LARGE:
383      return CreateError("document_too_large");
384    case LocalPrintJob::SAVE_PRINTER_BUSY:
385      return CreateErrorWithTimeout("printer_busy", -2);
386    case LocalPrintJob::SAVE_PRINTER_ERROR:
387      return CreateErrorWithDescription("printer_error", error_description);
388    default:
389      NOTREACHED();
390      return scoped_ptr<base::DictionaryValue>();
391  }
392}
393
394scoped_ptr<base::DictionaryValue> PrivetHttpServer::ProcessJobState(
395    const GURL& url,
396    net::HttpStatusCode* status_code) const {
397  if (!delegate_->IsRegistered() || !delegate_->IsLocalPrintingAllowed()) {
398    *status_code = net::HTTP_NOT_FOUND;
399    return scoped_ptr<base::DictionaryValue>();
400  }
401
402  std::string job_id;
403  net::GetValueForKeyInQuery(url, "job_id", &job_id);
404  LocalPrintJob::Info info;
405  if (!delegate_->GetJobState(job_id, &info))
406    return CreateError("invalid_print_job");
407
408  scoped_ptr<base::DictionaryValue> response(new base::DictionaryValue);
409  response->SetString("job_id", job_id);
410  response->SetString("state", LocalPrintJobStateToString(info.state));
411  response->SetInteger("expires_in", info.expires_in);
412  return response.Pass();
413}
414
415scoped_ptr<base::DictionaryValue> PrivetHttpServer::ProcessRegister(
416    const GURL& url,
417    net::HttpStatusCode* status_code) const {
418  if (delegate_->IsRegistered()) {
419    *status_code = net::HTTP_NOT_FOUND;
420    return scoped_ptr<base::DictionaryValue>();
421  }
422
423  std::string action;
424  std::string user;
425  bool params_present =
426      net::GetValueForKeyInQuery(url, "action", &action) &&
427      net::GetValueForKeyInQuery(url, "user", &user) &&
428      !user.empty();
429
430  RegistrationErrorStatus status = REG_ERROR_INVALID_PARAMS;
431  scoped_ptr<base::DictionaryValue> response(new base::DictionaryValue);
432
433  if (params_present) {
434    response->SetString("action", action);
435    response->SetString("user", user);
436
437    if (action == "start")
438      status = delegate_->RegistrationStart(user);
439
440    if (action == "getClaimToken") {
441      std::string token;
442      std::string claim_url;
443      status = delegate_->RegistrationGetClaimToken(user, &token, &claim_url);
444      response->SetString("token", token);
445      response->SetString("claim_url", claim_url);
446    }
447
448    if (action == "complete") {
449      std::string device_id;
450      status = delegate_->RegistrationComplete(user, &device_id);
451      response->SetString("device_id", device_id);
452    }
453
454    if (action == "cancel")
455      status = delegate_->RegistrationCancel(user);
456  }
457
458  if (status != REG_ERROR_OK)
459    response.reset();
460
461  ProcessRegistrationStatus(status, &response);
462  *status_code = net::HTTP_OK;
463  return response.Pass();
464}
465
466void PrivetHttpServer::ProcessRegistrationStatus(
467    RegistrationErrorStatus status,
468    scoped_ptr<base::DictionaryValue>* current_response) const {
469  switch (status) {
470    case REG_ERROR_OK:
471      DCHECK(*current_response) << "Response shouldn't be empty.";
472      break;
473
474    case REG_ERROR_INVALID_PARAMS:
475      *current_response = CreateError("invalid_params");
476      break;
477    case REG_ERROR_DEVICE_BUSY:
478      *current_response = CreateErrorWithTimeout("device_busy",
479                                                 kDeviceBusyTimeout);
480      break;
481    case REG_ERROR_PENDING_USER_ACTION:
482      *current_response = CreateErrorWithTimeout("pending_user_action",
483                                                 kPendingUserActionTimeout);
484      break;
485    case REG_ERROR_USER_CANCEL:
486      *current_response = CreateError("user_cancel");
487      break;
488    case REG_ERROR_CONFIRMATION_TIMEOUT:
489      *current_response = CreateError("confirmation_timeout");
490      break;
491    case REG_ERROR_INVALID_ACTION:
492      *current_response = CreateError("invalid_action");
493      break;
494    case REG_ERROR_OFFLINE:
495      *current_response = CreateError("offline");
496      break;
497
498    case REG_ERROR_SERVER_ERROR: {
499      std::string description;
500      delegate_->GetRegistrationServerError(&description);
501      *current_response = CreateErrorWithDescription("server_error",
502                                                     description);
503      break;
504    }
505
506    default:
507      NOTREACHED();
508  };
509}
510