TFCweb  1.0.4 $Rev: 483 $
TFC Primavera 2012: Nucli d'un servidor web
Servidor.cc
Veure la documentació d'aquest fitxer.
1 
8 /*
9  * Copyright (c) 2012 Toni Corvera
10  *
11  * This file is part of TFCWeb.
12  *
13  * TFCWeb is free software: you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation, either version 3 of the License, or
16  * (at your option) any later version.
17  *
18  * TFCWeb is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with TFCWeb. If not, see <http://www.gnu.org/licenses/>.
25  */
26 
27 #include "AutoIndex.h"
28 #include "Base64.h"
29 #include "CGI.h"
30 #include "CodisEstatHTTP.h"
31 #include "Configuracio.h"
32 #include "DataHTTP.h"
33 #include "Log.h"
34 #include "PeticioHTTP.h"
35 #include "Recurs.h"
36 #include "RespostaHTTP.h"
37 #include "Servidor.h"
38 #include "utils.h"
39 
40 #include <array>
41 #include <cassert>
42 #include <iostream>
43 #include <map>
44 #include <memory>
45 #include <sstream>
46 #include <stdexcept>
47 #include <string>
48 #include <vector>
49 #include <boost/algorithm/string.hpp>
50 #include <boost/filesystem.hpp>
51 #include <boost/lexical_cast.hpp>
52 #include <boost/regex.hpp>
53 
54 using namespace boost;
55 using namespace boost::asio::ip;
56 using namespace std;
57 using namespace tfc;
58 namespace fs = boost::filesystem;
59 
60 namespace {
61  // TODO: canvi a Boost Regex
62  vector<string> split(const string & str, char delim = ' ') {
63  stringstream ss(str);
64  string ostr;
65  vector<string> l;
66  while (getline(ss, ostr, delim)) {
67  l.push_back(ostr);
68  }
69  return l;
70  }
71 
72  // TODO: Apache: + "Server at <domini> Port <n>"
74 
76  public:
77  ErrorRutaRelativaEnPeticio(const string & w = "Petició amb ruta relativa",
78  CodiEstatHTTP codi = CODI_400)
79  : ErrorHTTPBadRequest(w, codi)
80  {
81  // buit
82  }
83  };
84 
86  public:
87  explicit ErrorHTTP11SenseHost(const string & w, CodiEstatHTTP codi)
88  : ErrorHTTPBadRequest(w, codi)
89  {
90  // buit
91  }
92  };
93 
94  class ErrorAutenticacio : public ErrorTFC {
95  public:
96  explicit ErrorAutenticacio(const string & msg = "Error d'autenticació",
97  const string & realm = "")
98  : ErrorTFC(msg), realme(realm)
99  {
100  // buit
101  }
102  virtual ~ErrorAutenticacio() throw() {}
103  const string realme;
104  };
105 
106  // Drecera
107  inline bool te_capsalera(const PeticioHTTP & p, const string & nom) {
108  bool e;
109  p.capsalera(nom, &e);
110  return e;
111  }
112 
113  // Usat en asserts
115  return s->is_open();
116  }
117 
118  const regex regex_capsalera("^([\\w-]+): (.*)$", boost::regbase::perl);
119 
122  public:
123  explicit ErrorTimeout(const string & w = "Temps exhaurit esperant dades")
124  : ErrorRebentDades(w)
125  {
126  // buit
127  }
128  };
129 
132  public:
133  explicit ErrorConnexioTancada(const string & w = "Connexió tancada prematurament")
134  : ErrorRebentDades(w)
135  {
136  // buit
137  }
138  };
139 
140  enum {
141  // Amb la implementació actual es col·loca tota la petició rebuda en
142  // memòria, aquest valor defineix una mida límit per al cos de la
143  // petició. Peticions més grans es rebutjaran.
144  MIDA_MAXIMA_COS_PETICIO = 8<<20 // 8 MiB
145  };
146  // XXX: G++ 4.4 té problemes si la classe es defineix dins el mètode
153  asio::mutable_buffers_1 & buf;
154  system::error_code & ec;
156  asio::mutable_buffers_1 & b,
157  system::error_code & e)
158  : socket(sock), buf(b), ec(e)
159  {}
160  size_t operator()() {
161  return socket.read_some(buf, ec);
162  }
163  };
164 
165 } // ns anònim
166 
167 namespace tfc {
168 
169 Servidor::~Servidor() {
170  if (socket_->is_open()) {
171  socket_->cancel();
172  socket_->close();
173  }
174 }
175 
176 // FIXME: Re-anomenar. Product name? veure RFC per "Server:"
177 // static
178 const string& Servidor::signatura() {
179  return SIGNATURA; // Servidor/Versió
180 }
181 
185 PeticioHTTP Servidor::analitza_peticio() throw (ErrorHTTP) {
186  string pet;
187  // Primera linia
188  // RFC 2068, pg30: "servers SHOULD ignore any empty line(s)
189  // received where a Request-Line is expected"
190  while (buffer_.tellg() < buffer_.tellp() &&
191  ( pet == "" || pet == "\r" ) )
192  {
193  getline(buffer_, pet, '\n');
194  }
195 
196  const vector<string> tokens = split(pet, ' ');
197  if (tokens.size() != 3) {
198  throw ErrorHTTPBadRequest("Format de petició incorrecte o desconegut");
199  }
200 
201  const string & verb = tokens[0];
202  string path = tokens[1];
203 
204  if ('\r' != * tokens[2].rbegin()) {
205  // Línea acabada en \n. FIXME: L'hauríem d'acceptar??
206  throw ErrorHTTPBadRequest("Petició mal formada");
207  }
208  const string versio_txt = tokens[2].substr(0, tokens[2].length()-1);
209  VersioHTTP versio = HTTP_DESCONEGUT;
210  // TODO: 0.9
211  if ("HTTP/1.0" == versio_txt) {
212  versio = HTTP_1_0;
213  }
214  else if ("HTTP/1.1" == versio_txt) {
215  versio = HTTP_1_1;
216  }
217  else {
218  regex re("HTTP/(\\d+)\\.(\\d+)");
219  smatch m;
220  if (regex_match(versio_txt, m, re)) {
221  try {
222  const int major = lexical_cast<int>(m[1]);
223  //const int minor = lexical_cast<int>(m[2]);
224  switch (major) {
225  case 0: // Tècnicament incorrecte
226  versio = HTTP_DESCONEGUT;
227  break;
228  case 1:
229  versio = HTTP_1_X;
230  break;
231  default: assert( major > 1 );
232  versio = HTTP_SUPERIOR;
233  break;
234  }
235  }
236  catch (const bad_lexical_cast &) {
237  // Queda sense definir
238  }
239  }
240  }
241 
242  try {
244  assert( tipus != desconegut ); // POSTCONDICIÓ
245  // En proxies es fa servir ruta completa (amb esquema i host), en servidors
246  // finals només es fa servir path absolut, però:
247  // RFC 2068, pàg. 35 "
248  // To allow for transition to absoluteURIs in all requests in future
249  // versions of HTTP, all HTTP/1.1 servers MUST accept the absoluteURI
250  // form in requests, even though HTTP/1.1 clients will only generate
251  // them in requests to proxies.
252  // "
253  // No poden contindre ".." FIXME: Vist en algun moment, però no ho trobo a l'RFC
254  // Sanejament de ruta
255  // TODO: ruta pot ser '*' si el mètode ho permet (OPTIONS, algun altre?)
256  // TODO: rutes com // confondràn l'analitzador de Uris, una opció seria
257  // passar 'http://example.com' + ruta i després esborrar esquema i autoritat
258  // Sanejament de ruta
259  Uri ruta(path); // throws ErrorAnalisiUrl
260  if (!ruta.te_ruta_absoluta()) {
261  if (path == "*" && OPTIONS != tipus) { // Cas especial: Mètode options accepta '*'
262  throw ErrorRutaRelativaEnPeticio();
263  }
264  }
265  ruta.autoritat("");
266  ruta.esquema("");
267  assert( OPTIONS == tipus || path != "*" );
268  return PeticioHTTP(tipus, ruta, versio);
269  }
270  catch (const ErrorAnalisiUrl & e) {
271  throw ErrorHTTPBadRequest("URL de petició incorrecta");
272  }
273  catch (const ErrorMetodeDesconegut & e) {
274  // RFC 2068, pàg. 35
275  // "[...] 501 (Not Implemented) if the method is unrecognized or
276  // not implemented by the server"
277  throw ErrorHTTP(e.what(), CODI_501);
278  }
279  assert( !"Error intern, linia inabastable" );
280 }
281 
282 size_t Servidor::llegeix_socket(stringstream & buffer,
283  unsigned int timeout_ms) throw (ErrorRebentDades)
284 {
285  tcp::socket & socket = *socket_; // FIXME: <- Usat per ajudar IDE
286  if (!socket.is_open()) {
287  throw ErrorConnexioTancada();
288  }
289  boost::array<char, MIDA_BUFFER_LECTURA> tmp;
290  asio::mutable_buffers_1 buf(asio::buffer(tmp));
291  system::error_code ec;
292 
293  // El mode síncron d'Asio no permet indicar timeouts directament, i read_some
294  // bloca fins que hi ha algun byte o fins que es tanca el socket
295  // Per a solucinar-ho utilitzem un "future", una variable que es computa
296  // de manera asíncrona, d'aquesta manera es pot indicar un timeout
297  // sense passar a utilitzar les interfícies asíncrones d'Asio.
298  packaged_task<size_t> pt(FunctorReadSome(socket,buf,ec));
299  unique_future<size_t> fut_llegits = pt.get_future(); // associació
300  // Fem la lectura en un fil separat...
301  boost::thread task(boost::move(pt));
302  // Llegim del socket ó fallem en cas d'un timeout.
303  // En aquest cas, un timeout implica que read_some() s'ha quedat esperant
304  // a rebre algun byte sense èxit.
305  fut_llegits.timed_wait(posix_time::millisec(timeout_ms));
306 
307  if (!fut_llegits.is_ready()) {
308  throw ErrorTimeout();
309  }
310  if (fut_llegits.has_exception()) {
311  throw ErrorRebentDades();
312  }
313  const size_t llegits = fut_llegits.get();
314 
315  if (llegits > 0) {
316  buffer.write(tmp.cbegin(), llegits);
317  }
318  if (ec == asio::error::eof) {
319  throw ErrorConnexioTancada();
320  }
321  else if (ec) {
322  throw ErrorRebentDades(); // XXX: Passa alguna vegada?
323  }
324  return llegits;
325 }
326 
327 size_t Servidor::omple_buffer(unsigned int timeout_ms) throw (ErrorRebentDades) {
328  if (buffer_.tellg() < buffer_.tellp()) {
329  return 0;
330  }
331  return llegeix_socket(timeout_ms);
332 }
333 
334 // TODO: Els servidors reals solen acceptar \n sense \r
335 void Servidor::extreu_capsaleres(PeticioHTTP & peticio) throw (ErrorHTTP, ErrorRebentDades) {
336  // Processament de (totes) les capçaleres
337  bool final_capsaleres = false;
338  // Cas especial ja s'ha buidat el buffer <-> sense capçaleres
339  final_capsaleres = ( buffer_.tellg() == buffer_.tellp() );
340  do {
341  // Exhaurim el buffer...
342  string linia;
343  // FIXME: Cas especial: Última lectura del buffer <-> linia incompleta
344  while (!final_capsaleres && buffer_.tellg() < buffer_.tellp()) {
345  getline(buffer_, linia, '\n');
346  // Les capçaleres han d'acabar en \r\n
347  if (linia.empty() || '\r' != utils::final(linia)) {
348  log() << NivellError
349  << "Capçalera mal formada (sense \\r) [" << linia << "]" << commit;
350  continue;
351  }
352  if (1 == linia.size()) {
353  assert( "\r" == linia );
354  // Capçalera buida <-> Final de capçaleres
355  log() << NivellDebug << "Final de capçaleres assolit" << commit;
356  final_capsaleres = true;
357  break;
358  }
359  linia.erase( linia.size() - 1 );
360  assert( '\r' != utils::final(linia) ); // XXX: Pot una capçalera contenir '\r'??
361  smatch m;
362  // FIXME: RFC 1945
363  // * Each header field consists of a name followed immediately by a colon (":"),
364  // a single space (SP) character, and the field value
365  // * Field names are case insensitive
366  // * Header fields can be extended over multiple lines by preceding each extra line
367  // with at least one SP or HT (→ \t), though this is not recommended
368  if (regex_match(linia, m, regex_capsalera)) {
369  const string nom = m[1],
370  valor = m[2];
371  peticio.afegeix_capsalera(nom, valor);
372  }
373  else {
374  log() << NivellError
375  << "Capçalera mal formada [" << linia <<"]" << commit;
376  }
377  }
378  // Si encara queden capçaleres per rebre, re-omplim el buffer
379  if (!final_capsaleres) {
380  llegeix_socket();
381  }
382  } while(!final_capsaleres);
383  assert( final_capsaleres );
384 }
385 
386 Servidor::Servidor(const Configuracio & c, socket_ptr &s) throw ()
387  : cfg(c), socket_(s), buffer_()
388 {
389  // PRECONDICIÓ
390  assert( es_connectat(socket_) );
391 }
392 
393 void Servidor::aten() throw (ErrorHTTP) {
394  // --- Ajuda a l'hora de produïr errors ---
396  // (i per tant no està definida)
397  static PeticioHTTP peticio_erronia(desconegut, Uri(""), HTTP_DESCONEGUT);
398 
399  // Functor per a simplificar la gestió d'errors:
400  // si la petició està definida, la retorna, altrament retorna peticio_erronia
401  struct PeticioEnError {
402  PeticioEnError(std::unique_ptr<PeticioHTTP> & peticio) : peticio_(peticio) {}
403  const PeticioHTTP& operator()() {
404  if (0 != peticio_.get()) {
405  return *peticio_;
406  }
407  return peticio_erronia;
408  }
409  const std::unique_ptr<PeticioHTTP> & peticio_;
410  };
411  // --- Final de l'ajuda ---
412 
413  std::unique_ptr<RespostaHTTP> resposta;
414  std::unique_ptr<PeticioHTTP> peticio;
415  assert( 0 == resposta.get() && 0 == peticio.get());
416 
417  TipusPeticioHTTP tipus_peticio = desconegut;
418 
419  PeticioEnError e_peticio(peticio);
420 
421  try {
422  const Uri host("http://" + lexical_cast<string>(socket_->local_endpoint()));
423 
424  // TODO: (forat de seguretat) No llegir tot en memòria
425  omple_buffer(); // throws ErrorRebentDades
426 
427  peticio.reset( new PeticioHTTP(analitza_peticio()) ); // throws ErrorHTTP
428  assert( peticio->tipus() != desconegut ); // POSTCONDICIÓ
429  tipus_peticio = peticio->tipus();
430  log() << NivellInfo
431  << NomTipusPeticioHTTP::nom(peticio->tipus())
432  << " "
433  << peticio->ruta()
434  << " (" << peticio->versio() << ")"
435  << ", des de "
436  << socket_->remote_endpoint().address() << commit;
437 
438  Uri url_completa(host);
439  url_completa.ruta(peticio->ruta().ruta()); // URL de la petició
440 
441  this->extreu_capsaleres(*peticio); //-> POSTCONDICIÓ: Totes les capçaleres llegides
442  // TODO: Cal enviar un 100 en HTTP/1.1 abans de llegir el cos?
443  this->extreu_cos(*peticio);
444 
445  if (CONNECT == peticio->tipus()) {
446  // El mètode CONNECT és reservat per l'RFC 2616, per a ús en
447  // proxies que puguin canviar a mode túnel, però no detalla com funciona.
448  // En qualsevol cas en aquest projecte no s'implementa la funcionalitat
449  // de proxy, per tant no s'implementa el mètode CONNECT
450  throw ErrorHTTPIntern("Aquest servidor no implementa el mètode CONNECT", CODI_501);
451  }
452 
453  // TODO: Permisos:
454  fs::path ruta_virtual(cfg.arrel() / fs::path(peticio->ruta().ruta()));
455  if (fs::exists(ruta_virtual)) {
456  ruta_virtual = fs::canonical(ruta_virtual);
457  }
458  CodiEstatHTTP permisos = cfg.cerber().acces(ruta_virtual, peticio->tipus());
459 
460  if (CODI_403 == permisos) {
461  throw ErrorHTTPBadRequest("No es permet l'accés a "+peticio->ruta().str(),CODI_403);
462  }
463  if (CODI_405 == permisos) {
464  throw ErrorHTTPBadRequest("Mètode no permés a "+peticio->ruta().str(),CODI_405);
465  }
466 
467  if (CODI_401 == permisos) { // Directori protegit
468  // Ha enviat credencials?
469  const string auth = peticio->capsalera("Authorization");
470  if (auth.empty()) {
471  throw ErrorAutenticacio("Cal autenticació", cfg.realme());
472  }
473  smatch m;
474  // L'altre mètode especificat és el Digest, especificat en l'RFC 2069
475  // No s'implementarà en aquest projecte
476  if (!regex_match(auth, m, regex("^Basic\\s+(.*)", regbase::perl))) {
477  throw ErrorHTTPBadRequest("Mètode d'autenticació desconegut", CODI_401);
478  }
479  const string & credencials = Base64::descodifica(m[1]);
480  // Credencials: nom_usuari:contrasenya
481  // El nom d'usuari no pot contenir ':' mentre que la contrasenya sí
482  // (RFC 2068, pàg. 67)
483  if (!regex_match(credencials, m, regex("^([^:]*):(.*)$"))) {
484  throw ErrorHTTPBadRequest("Format de credencials incorrecte", CODI_401);
485  }
486  const string & usuari = m[1], & pass = m[2];
487  if (usuari != cfg.usuari() || pass != cfg.contrasenya()) {
488  throw ErrorAutenticacio("Credencials incorrectes", cfg.realme());
489  }
490  // Autenticat, podem seguir normalment
491  }
492 
493  // RFC 2068, pàg. 35: GET i HEAD són els únics mètodes d'implementació obligatòria
494 
495  assert( desconegut != peticio->tipus() );
496  if (PUT == peticio->tipus() || METODE_DELETE == peticio->tipus()) {
497  resposta = aten_peticio_escriptura(url_completa, *peticio);
498  }
499  else if (TRACE == peticio->tipus() || OPTIONS == peticio->tipus()) {
500  resposta = aten_peticio_informacio(url_completa, *peticio);
501  }
502  else {
503  assert( GET == peticio->tipus()
504  || HEAD == peticio->tipus()
505  || POST == peticio->tipus() );
506  resposta = aten_peticio_lectura(url_completa, *peticio);
507  }
508  }
509  catch (const ErrorHTTP & e) {
510  resposta.reset(new RespostaHTTP(e.codi()));
511  std::shared_ptr<CosResposta> spcr(new CosPaginaError(e_peticio(), e.codi(), e.what()));
512  resposta->defineix_cos(spcr);
513  }
514  catch (const ErrorAutenticacio & e) {
515  assert( 0 != peticio.get() );
516  resposta.reset(new RespostaHTTP(CODI_401));
517  std::shared_ptr<CosResposta> spcr(new CosPaginaError(*peticio, CODI_401, e.what()));
518  resposta->defineix_cos(spcr);
519  resposta->afegeix_capsalera("WWW-Authenticate", "Basic realm=\""+e.realme+"\"");
520  }
521  catch (const ErrorTimeout & e) {
522  if (0 == peticio.get()) {
523  // Timeout durant la petició, no té sentit respondre
524  log() << NivellError
525  << "Temps exhaurit esperant petició, es tanca la connexió" << commit;
526  socket_->close();
527  return;
528  }
529  assert( 0 != peticio.get() );
530  resposta.reset(new RespostaHTTP(CODI_408));
531  std::shared_ptr<CosResposta> spcr(new CosPaginaError(*peticio, CODI_408, e.what()));
532  resposta->defineix_cos(spcr);
533  }
534  catch (const ErrorRebentDades & e) {
535  if (0 == peticio.get()) {
536  // Tancament de socket durant la petició, no té sentit respondre
537  log() << NivellError
538  << "Socket tancat esperant petició" << commit;
539  socket_->close();
540  return;
541  }
542  assert( 0 != peticio.get() );
543  if (!socket_->is_open()) { // XXX: Per alguna raó is_open() és cert
544  // encara que el client el tanqui
545  return;
546  }
547  resposta.reset(new RespostaHTTP(CODI_400));
548  std::shared_ptr<CosResposta> spcr(new CosPaginaError(*peticio, CODI_400, e.what()));
549  resposta->defineix_cos(spcr);
550  }
551  catch (const runtime_error & e) { // també ErrorTFC
552  // Com en el catch(...), evitem fallar cas de trobar una excepció inesperada
553  // en aquest cas a més podem obtenir un missatge d'error
554  log() << NivellError << "Excepció inesperada: " << e.what() << commit;
555  resposta.reset(new RespostaHTTP(CODI_500));
556  const char * msg = "Error intern del servidor";
557  std::shared_ptr<CosResposta> spcr(new CosPaginaError(e_peticio(), resposta->codi(), msg));
558  resposta->defineix_cos(spcr);
559  }
560  catch (...) { // Evitem que el programa falli si es llença alguna excepció no control·lada
561  log() << NivellError << "@@@ Error inesperat @@@" << commit;
562  resposta.reset(new RespostaHTTP(CODI_500));
563  const char * msg = "Error intern del servidor";
564  std::shared_ptr<CosResposta> spcr(new CosPaginaError(e_peticio(), resposta->codi(), msg));
565  resposta->defineix_cos(spcr);
566  }
567  // TODO: Provocar error. Vist que error 500 mostra missatge, i després capçaleres
568  assert( 0 != resposta.get() );
569 
570  // TODO: Aplicar filtres
571 
572  // Capçaleres generals
573  resposta->afegeix_capsalera("Server", signatura());
574  resposta->afegeix_capsalera("Date", data_http());
575  // Si es produeix un error rebent dades pot ser que no tinguem una petició
576  // en condicions
577  const bool peticio_rebuda = (0 != peticio.get());
578  bool pipeline = peticio_rebuda; // No té sentit escoltar més sense petició
579 
580  if (peticio_rebuda) {
581  // Mantenim la connexió oberta?
582  // Format de connection (RFC 2068, 14.10): 1 o més tokens, separats per comes
583  const string connection = peticio->capsalera("Connection");
584  if ("close" == to_lower_copy(connection)) { // TODO: Extreure tokens individuals
585  // S'ha sol·licitat tancar-la expliícitament
586  pipeline = false;
587  }
588  else {
589  // En HTTP/1.1 el comportament per defecte és mantenir-la oberta
590  // no cal fer res
591  if (peticio->versio() < HTTP_1_1) {
592  // En altres versions optem per tancar la connexió
593  pipeline = false;
594  }
595  }
596  if (!pipeline) {
597  resposta->afegeix_capsalera("Connection", "close");
598  }
599  }
600 #if !defined(NDEBUG)
601  // Per ajudar en les proves durant el desenvolupament, afegim ID del fil
602  resposta->afegeix_capsalera("X-Thread-Id", lexical_cast<string>(this_thread::get_id()));
603 #endif
604  // Capçaleres específiques del cos
605  if (resposta->te_cos()) {
606  const auto capsaleres_cos = resposta->cos().lock()->capsaleres();
607  resposta->afegeix_capsaleres(capsaleres_cos.begin(), capsaleres_cos.end());
608  }
609  // Accept-Ranges s'associa a un recurs
610  resposta->afegeix_capsalera("Accept-Ranges", "none"); // TODO: "bytes");
611  // RFC 2616: pàg. 106: "
612  // An Allow header field MUST be present in a 405 (Method Not Allowed) response.
613  // "
614  if (resposta->codi() < CODI_300 || resposta->codi() == CODI_405) {
615  string allow = "GET, HEAD, POST, TRACE, OPTIONS";
616  //static string m[] = { "GET", "HEAD" };
617 
618  const fs::path r = cfg.arrel() / peticio->ruta().ruta();
619  if (CODI_200 == cfg.cerber().acces(r, PUT)) {
620  allow += ", PUT";
621  }
622  if (CODI_200 == cfg.cerber().acces(r, METODE_DELETE)) {
623  allow += ", DELETE";
624  }
625  resposta->afegeix_capsalera("Allow", allow);
626  }
627 
628  // No fem cas a les capçaleres Accept-*:
629  // RFC 2068, pàg. 61:
630  // "HTTP/1.1 servers are allowed to return responses which are
631  // not acceptable according to the accept headers sent in the request.
632  // In some cases, this may even be preferable to sending a 406
633  // response."
634 
635  try {
636  this->envia_resposta(tipus_peticio, *resposta.get());
637  }
638  catch (const ErrorEscrivintEnSocket & e) {
639  // Error en el socket, alguna cosa ha fallat
640  log() << NivellError << "Error en enviar resposta: " << e.what() << commit;
641  return;
642  }
643 
644  if (pipeline && socket_->is_open()) {
645  log() << NivellInfo << "Reutilitzant socket " << socket_->remote_endpoint() << commit;
646  aten();
647  }
648 }
649 
650 void Servidor::extreu_cos(PeticioHTTP & peticio) throw (ErrorHTTP, ErrorRebentDades) {
651  // TODO: No col·locar en memòria
652  // TODO: Respondre interinament amb 100 en HTTP/1.1
653  // Cal enviar una resposta final igualment I no cal que esperi a rebre-la,
654  // pot enviar el cos i ignorar la resposta interina
655 
656  // Capçaleres llegides. Cal llegir cos?
657 
658  // RFC 2068: L'existència d'un cos en un petició bé indicada per la
659  // presència de la capçalera Content-Length ó Transfer-Encoding
660  // FIXME: Els mètodes que no permeten cos NO HAURIEN de tenir-lo (RFC2068, p32)
661  // TODO: Veure RFC 2068, 4.4: Gestió de longitud i codificació de cos
662  // - Si no es pot determinar la mida del missatge es pot respondre amb 400
663  // ó amb 411 (sol·licitar re-enviament amb longitud)
664  //
665  const bool te_cos = te_capsalera(peticio, "Content-Length") ||
666  te_capsalera(peticio, "Transfer-Encoding");
667  if (!te_cos) {
668  return;
669  }
670 
671  // RFC 2616, 8.2.3
672  const string expect = peticio.capsalera("Expect");
673  if (peticio.versio() >= HTTP_1_1) {
674  bool envia_100 = false;
675  if (HTTP_1_1 == peticio.versio()) {
676  // RFC 2616, 8.2.3, pàg. 49
677  // Excepció especial per compatibilitat amb l'RFC 2068 <=> només en HTTP 1.1
678  if (PUT == peticio.tipus() || METODE_DELETE == peticio.tipus()) {
679  envia_100 = true;
680  }
681  }
682  if (!envia_100 && !expect.empty()) {
683  envia_100 = (expect == "100-continue");
684  }
685  if (envia_100) {
686  RespostaHTTP continua(CODI_100);
687  envia_resposta(peticio.tipus(), continua);
688  }
689  }
690 
691  stringstream buffer_cos; // El cos s'escriurà aquí
692  std::array<char, MIDA_BUFFER_LECTURA> tempbuf; // buffer temporal de lectura
693  // Exhaurim el buffer de recepció...
694  while (buffer_.tellp() > buffer_.tellg()) {
695  const size_t llegits = buffer_.readsome(tempbuf.data(), tempbuf.size());
696  buffer_cos.write(tempbuf.data(), llegits);
697  }
698 
699  // TODO: RFC 2068, pàg. 33: "
700  // Messages MUST NOT include both a Content-Length header field and the
701  // "chunked" transfer coding. If both are received, the Content-Length
702  // MUST be ignored.
703  // "
704  if (!te_capsalera(peticio, "Content-Length")) {
705  assert( te_capsalera(peticio, "Transfer-Encoding") );
706  // RFC 2068, pàg.33: "
707  // For compatibility with HTTP/1.0 applications, HTTP/1.1 requests
708  // containing a message-body MUST include a valid Content-Length header
709  // field unless the server is known to be HTTP/1.1 compliant. If a
710  // request contains a message-body and a Content-Length is not given,
711  // the server SHOULD respond with 400 (bad request) if it cannot
712  // determine the length of the message, or with 411 (length required) if
713  // it wishes to insist on receiving a valid Content-Length.
714  // "
715  // RFC 2616, pàg. 24: "
716  // Whenever a transfer-coding is applied to a message-body, the set of
717  // transfer-codings MUST include "chunked", unless the message is
718  // terminated by closing the connection. When the "chunked" transfer-
719  // coding is used, it MUST be the last transfer-coding applied to the
720  // message-body.
721  // " <=> no té sentit que els clients tanquin per finalitzar, per tant
722  // HAN d'incloure chunked
723  // [...] "
724  // A server which receives an entity-body with a transfer-coding it does
725  // not understand SHOULD return 501 (Unimplemented), and close the
726  // connection. A server MUST NOT send transfer-codings to an HTTP/1.0
727  // client.
728  // "
729  const string te = to_lower_copy(peticio.capsalera("Transfer-Encoding"));
730  // Només acceptem "chunked"
731  if ("chunked" != te) {
732  throw ErrorHTTP("Codificació de missatge no suportada", CODI_501);
733  }
734  assert( "chunked" == te );
735 
736  TransformacioChunked chunked;
737  // XXX: Idealment caldria implementar destransforma() sense necessitar el
738  // cos sencer i/o poder aprofitar un "AplicadorTransformacio"
739  // Comprovem si es pot descodificar, és a dir, si està complet
740  // Com que la transformació inversa només està definida per a missatges
741  // sencers caldrà haver completat el missatge abans de poder aconseguir-ho
743  string descodificat = chunked.destransforma(buffer_cos.str(), estat);
744  // Si el format no és acceptable no cal continuar
745  if (FORMAT_INCORRECTE == estat) {
746  throw ErrorHTTPBadRequest("Format codificat no reconegut", CODI_411);
747  }
748  while (TRANSFORMAT != estat) {
749  // Cal seguir extraient
750  const size_t llegits = llegeix_socket(buffer_cos);
751  if (0 == llegits) { // XXX: Pot passar?
752  throw ErrorTimeout();
753  }
754  descodificat = chunked.destransforma(buffer_cos.str(), estat);
755  if (FORMAT_INCORRECTE == estat) {
756  throw ErrorHTTPBadRequest("Format codificat no reconegut", CODI_411);
757  }
758  }
759  cerr << "Final de transformació" << endl;
760  assert( TRANSFORMAT == estat );
761 
762  log() << NivellDebug << "Petició amb cos (chunked), mida: "
763  << descodificat.length() << commit;
764  buffer_cos.str(descodificat); // Re-emplaçem el codificat pel descodificat
765  }
766  else {
767  size_t length;
768  const string content_length = peticio.capsalera("Content-Length");
769  // Comprovació de valor ben format
770  if (!regex_match(content_length, regex("^\\d+$"))) {
771  // Mal format
772  throw ErrorHTTPBadRequest("Capçalera Content-Length mal formada");
773  }
774  length = boost::lexical_cast<size_t>(content_length);
775  log() << NivellDebug << "Petició amb cos, mida indicada: " << length << commit;
776  // XXX: Salvaguarda ja que es col·loca en memòria
777  if (length > MIDA_MAXIMA_COS_PETICIO) {
778  throw ErrorHTTPBadRequest("Petició massa gran", CODI_413);
779  }
780 
781  while (length > buffer_cos.str().length()) {
782  llegeix_socket(buffer_cos); // throws en error o timeout
783  }
784  const size_t rebuts = buffer_cos.str().length();
785  if (rebuts != length) {
786  throw ErrorHTTPBadRequest("La mida de l'entitat rebuda ("+
787  lexical_cast<string>(rebuts)+
788  ") difereix de la indicada ("+
789  lexical_cast<string>(length)+")",
790  CODI_400);
791  }
792  }
793 
794  const string & cos = buffer_cos.str();
795  std::shared_ptr<CosPeticio> cos_peticio(new CosPeticio(cos.length()));
796  cos_peticio->afegeix(cos);
797  assert( cos_peticio->mida() == cos.length() );
798  peticio.defineix_cos(cos_peticio);
799  log() << NivellDebug << "Cos rebut, mida: " << cos_peticio->mida() << commit;
800 }
801 
802 Servidor::resposta_uptr Servidor::aten_peticio_lectura(const Uri & url_peticio,
803  PeticioHTTP & peticio)
804  throw (ErrorHTTP)
805 {
806  assert( HEAD == peticio.tipus()
807  || GET == peticio.tipus()
808  || POST == peticio.tipus() ); // PRECONDICIÓ
809  resposta_uptr resposta;
810  try {
811 #if 0
812  cerr << "- Petició " << string(62, '-') << "\n"
813  << peticio << "\n"
814  << string(72, '-') << endl;
815 #endif
816 
817  if (peticio.versio() >= HTTP_1_1) {
818  // HTTP/1.1 imposa que s'envïi una capçalera Host en totes les peticions
819  if (!te_capsalera(peticio, "Host")) {
820  throw ErrorHTTP11SenseHost("Rebuda petició HTTP/1.1 sense capçalera host",
821  CODI_400);
822  }
823  }
824 
825  Recurs r(peticio, cfg.arrel()); // throws ErrorRutaInexistent
826  FlagsPermis perms = cfg.cerber().permis(r.fisica());
827  assert( 0 != (perms & LECTURA)); // PRECONDICIÓ
828  resposta.reset(new RespostaHTTP(CODI_200));
829 
830  log() << NivellDebug << "Recurs (real) sol·licitat: " << r.fisica() << commit;
831 
832  // Ajusta a índex per defecte
833  if (is_directory(r.fisica())) {
834  // XXX: index.html Configurable
835  const filesystem::path index_path(r.fisica() / cfg.index());
836  // is_regular_file és true per symlinks
837  if (exists(index_path) && is_regular_file(index_path)) {
838  // Re-escrivim la petició amb la ruta modificada
839  Uri nova_ruta(peticio.ruta());
840  nova_ruta.ruta(nova_ruta.ruta()+"/"+cfg.index());
841  peticio = PeticioHTTP(peticio.tipus(), nova_ruta, peticio.versio());
842  r = Recurs(peticio, cfg.arrel());
843  // Re-analitzem
844  perms = cfg.cerber().permis(r.fisica());
845  }
846  }
847 
848  // TODO: No generar autoindex si és un directori cgi-bin
849  if (is_directory(r.fisica())) {
850  assert( canonical(r.fisica()) == r.fisica() );
851  // FIXME: Passar Host: com a endpoint si present
852  // FIXME: Canonical vs Server path
853  AutoIndex * ai = new AutoIndex(peticio, socket_->local_endpoint(), url_peticio, r);
854  resposta->defineix_cos(std::shared_ptr<CosResposta>(ai));
855  }
856  else if (is_regular_file(r.fisica())) {
857  // L'interfície per afegir un cos a la resposta fa servir punters
858  // intel·ligentsm per tant cal crear un nou cos amb la implementació
859  // concreta
860  CosResposta * copia = 0;
861  const bool ex = (0 != (perms & EXECUCIO)); // Es permet executar
862  if (ex) { // => Ruta configurada com executable
863  // TODO: com es comprova ownership!?
864  if (!portabilitat::permis_execucio(r.fisica())) {
865  // A les rutes configurades per CGIs no es permet accedir
866  // a fitxers no executables. Protecció per no expossar el codi
867  // font d'scripts amb permissos incorrectes
868  throw ErrorHTTPIntern("Configuració de permisos incorrecta",CODI_500);
869  }
870  copia = new ProgramaCGI(peticio, cfg.arrel());
871  }
872  else {
873  // Ruta "normal"
874  copia = new Recurs(peticio, cfg.arrel());
875  }
876  resposta->defineix_cos(std::shared_ptr<CosResposta>(copia));
877  }
878  else {
879  // Altres tipus de fitxers (pipes, sockets, dispositius...)
880  // No es permet l'accés en qualsevol cas
881  throw ErrorHTTPBadRequest("No es permet l'accés a "+r.fisica().string(),CODI_403);
882  }
883  }
884  catch (const ErrorRutaInexistent &) {
885  throw ErrorHTTP("No s'ha trobat en el servidor: "+peticio.ruta().ruta(), CODI_404);
886  }
887  assert( 0 != resposta.get() );
888  return resposta;
889 } // aten_peticio_lectura()
890 
891 Servidor::resposta_uptr Servidor::aten_peticio_escriptura(const Uri & u, PeticioHTTP & p)
892  throw (ErrorHTTP)
893 {
894  using namespace boost::filesystem;
895  using boost::system::error_code;
896  assert( p.tipus() == PUT || p.tipus() == METODE_DELETE ); // PRECONDICIÓ
897  resposta_uptr resposta;
898 
899  if (p.versio() != HTTP_1_1) {
900  // Només suportem aquestes peticions en HTTP/1.1
901  // Futures versions no haurien de canviar la semàntica, però anem sobre segur
902  throw ErrorHTTP("El mètode " + NomTipusPeticioHTTP::nom(p.tipus()) +
903  " només està implementat en HTTP/1.1",
904  CODI_501);
905  }
906  // TODO: Comprova permís
907 
908  const path ruta_real(cfg.arrel() / path(p.ruta().ruta()));
909 
910  const bool existeix = exists(ruta_real);
911  // No fer coses rares amb elements especials de l'SO
912  // TODO: Hi ha alguna forma de crear directoris amb PUT?
913  if (existeix && !is_regular_file(ruta_real)) {
914  throw ErrorHTTP(p.ruta().ruta()+" no és un fitxer vàlid", CODI_501);
915  }
916 
917  if (p.tipus() == PUT) {
918  // Si el recurs existeix, se sobreescriu.
919  // Si no existeix i és permissible, es crea
920  // Respostes:
921  // 201 -> S'ha creat
922  // 200 ó 204 -> S'ha modificat
923  if (!p.te_cos()) {
924  // XXX: Sense cos, s'hauria de crear un recurs buit?
925  throw ErrorHTTPBadRequest("Petició PUT sense contingut", CODI_400);
926  }
927  // FIXME:
928  // Si la petició conté capçaleres Content-* que no s'entenen o no es
929  // suporten NO S'HAN D'IGNORAR, i s'ha de respondre amb 501
930  // Escrivim a un fitxer temporal
931  size_t content_length = 0;
932  for (auto it = p.capsaleres().begin(); it != p.capsaleres().end(); ++it) {
933  if ("content-length" == to_lower_copy(it->nom())) {
934  try {
935  content_length = lexical_cast<size_t>(it->valor());
936  break;
937  }
938  catch (const boost::bad_lexical_cast & e) {
939  // FIXME: ó 411? ó 400?
940  throw ErrorHTTPBadRequest("La mida indicada en les capçaleres no s'entén", CODI_411);
941  }
942  }
943  }
944  if (content_length == 0) {
945  throw ErrorHTTPBadRequest("Petició PUT sense mida", CODI_411);
946  }
947  // TODO: Utilitzar fitxer intermig temporal
948  // codi portable d'exemple:
949  // http://lists.boost.org/Archives/boost/2005/09/index.php#93150
950  // http://web.archive.org/web/http://psycho.pl/pub/src/tmpfile.cc
951  ofstream ofs(ruta_real.string().c_str(), ios::out|ios::binary|ios::trunc);
952  if (ofs.fail()) {
953  throw ErrorHTTPIntern("Error en obrir `"+p.ruta().ruta()+"` per escriure'n",
954  CODI_503); // 503 no és 100% adequada, però crec que acceptable
955  }
956  const auto pCos = p.cos().lock();
957  ofs.write(pCos->contingut().c_str(), pCos->mida());
958  if (ofs.bad()) {
959  throw ErrorHTTPIntern("Error en escriure a `"+p.ruta().ruta()+"`", CODI_500);
960  }
961  ofs.close();
962  log() << NivellDebug << ruta_real << " escrit mitjançant PUT" << commit;
963  const CodiEstatHTTP codi = (existeix ? CODI_204 : CODI_201);
964  resposta.reset(new RespostaHTTP(codi));
965  }
966  else { assert( p.tipus() == METODE_DELETE );
967  // Possibles respostes (RFC 2068, pàg. 53)
968  // 2xx només si es pretén esborrar (es faci realment o no)
969  // 202 -> OK, però encara no s'ha esborrat
970  // 200 -> OK amb entitat missatge d'estat
971  // 204 -> OK sense missatge
972  // DELETE es pot implementar com esborrat o com moviment a una
973  // ruta inaccessible
974  if (!existeix) {
975  throw ErrorHTTPBadRequest("El recurs "+p.ruta().ruta()+" no existeix", CODI_404);
976  }
977  assert( existeix );
978  // XXX: remove() esborra symlinks sense seguir-los. És el funcionament dessitjable?
979  error_code ec;
980  remove(ruta_real.string(), ec); // retorna true si ruta_real existeix, no si s'esborra
981  if (ec) {
982  throw ErrorHTTP("Error esborrant el recurs "+p.ruta().ruta()+": "+
983  ec.category().message(ec.value())
984  , CODI_503); // ó 500
985  }
986  log() << NivellDebug << ruta_real << " esborrat" << commit;
987  resposta.reset(new RespostaHTTP(CODI_204));
988  }
989  return resposta;
990 } // aten_peticio_escriptura()
991 
992 Servidor::resposta_uptr Servidor::aten_peticio_informacio(const Uri & u, PeticioHTTP & p)
993  throw (ErrorHTTP)
994 {
995  assert( TRACE == p.tipus() || OPTIONS == p.tipus() ); // PRECONDICIÓ
996  resposta_uptr resposta;
997 
998  if (TRACE == p.tipus()) {
999  // RFC 2068, 9.8 TRACE, pàg. 53
1000  // "The final recipient of the request SHOULD reflect the message received
1001  // back to the client as the entity-body of a 200 (OK) response."
1002  if (p.te_cos()) {
1003  // "A TRACE request MUST NOT include an entity."
1004  throw ErrorHTTP("Petició TRACE amb cos", CODI_400);
1005  }
1006  resposta.reset(new RespostaHTTP(CODI_200));
1007  resposta->afegeix_capsalera("Content-Type", "text/html");
1008  resposta->afegeix_capsalera("Content-Length",
1009  lexical_cast<string>(buffer_.str().length()));
1010  CosTextual * pCos = new CosTextual(p, buffer_.str());
1011  std::shared_ptr<CosResposta> sptrCos(pCos);
1012  resposta->defineix_cos(sptrCos);
1013  }
1014  else { assert( OPTIONS == p.tipus() );
1015  // RFC 2616: 9.2, pàg. 52: "
1016  // A 200 response SHOULD include any header fields that indicate
1017  // optional features implemented by the server and applicable to that
1018  // resource (e.g., Allow)"
1019  // Allow s'inclou sempre, altres com la gestió de caché no estàn implementades,
1020  // per tant no cal fer res.
1021  // " The response body, if any, SHOULD also include
1022  // information about the communication options. The format for such a
1023  // body is not defined by this specification, but might be defined by
1024  // future extensions to HTTP. [...] If no response body is included, the
1025  // response MUST include a Content-Length field with a field-value of
1026  // "0"."
1027  resposta.reset(new RespostaHTTP(CODI_200));
1028  resposta->afegeix_capsalera("Content-Length", "0");
1029  }
1030 
1031  return resposta;
1032 } // aten_peticio_informacio()
1033 
1034 void Servidor::envia_resposta(TipusPeticioHTTP tipus, RespostaHTTP & r) const
1035  throw (ErrorEscrivintEnSocket)
1036 {
1037  log() << NivellDebug << "Enviant resposta (" << r.codi() << ")" << commit;
1038 
1039  // FIXME: HEAD + error 404, p.e., provoca un Content-Length per el text del missatge,
1040  // és correcte? Almenys Winie té problemes amb això
1041  if (HEAD == tipus) {
1042  r.elimina_capsalera("Content-Length");
1043  }
1044 
1045  // Enviament de capçaleres
1046  {
1047  const string & frase = FrasesEstatHTTP::frase(r.codi());
1048  ostringstream oss_resposta;
1049  // Les respostes haurien de ser en la mateixa versió que el client
1050  // però no és obligatori i a la pràctica no hi ha cap diferència perquè es permeten
1051  // capçaleres posteriors a la versió d'HTTP
1052  oss_resposta << "HTTP/1.1 " << lexical_cast<string>(r.codi())
1053  << " " << frase << "\r\n";
1054  r.imprimeix_capsaleres(oss_resposta, "\r\n");
1055  // Final de capçaleres.
1056  // Cas especial: Els CGIs també poden generar capçaleres i per
1057  // tant són ells els encarregats d'enviar el separador de capçaleres i cos.
1058  // Altres tipus de recursos dinàmics també podrien, teòricament.
1059  // Vegeu CosResposta::pot_generar_capsaleres()
1060  if (HEAD != tipus && r.te_cos()) {
1061  if (!r.cos().lock()->pot_generar_capsaleres()) {
1062  oss_resposta << "\r\n";
1063  }
1064  }
1065  else { // no te cos, o té cos però és un HEAD
1066  oss_resposta << "\r\n";
1067  }
1068  try {
1069  asio::write(*socket_, asio::buffer(oss_resposta.str()));
1070  }
1071  catch (const boost::system::system_error & e) {
1072  throw ErrorEscrivintEnSocket(e.what());
1073  }
1074  }
1075 
1076  assert( tipus != desconegut || r.codi() >= 400 );
1077  if (HEAD != tipus) {
1078  if (r.te_cos()) {
1079  assert( !r.cos().expired() );
1080  auto cos = r.cos().lock();
1081  try {
1082  cos->envia(*socket_); // throws runtime_error
1083  cos->envia_eom(*socket_); // throws ErrorEscrivintEnSocket
1084  }
1085  catch (const ErrorEscrivintEnSocket & e) {
1086  throw;
1087  }
1088  catch (const std::runtime_error & e) {
1089  throw ErrorEscrivintEnSocket(e.what());
1090  }
1091  log() << NivellDebug << "Cos enviat" << commit;
1092  }
1093  else {
1094  log() << NivellDebug << "Resposta sense cos" << commit;
1095  }
1096  }
1097 }
1098 
1099 void Servidor::tancar_sessio() {
1100  socket_->close();
1101 }
1102 
1103 } // ns tfc
1104 
1105 // vim:set ts=4 et ai: //