Mercurial > hg > orthanc
comparison OrthancFramework/Sources/HttpServer/HttpServer.cpp @ 4228:c8c0bbaaace3
write access to webdav
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 06 Oct 2020 12:45:11 +0200 |
parents | 7fff7e683d65 |
children | 013d6c6b2387 |
comparison
equal
deleted
inserted
replaced
4227:7fff7e683d65 | 4228:c8c0bbaaace3 |
---|---|
43 # include <civetweb.h> | 43 # include <civetweb.h> |
44 # define MONGOOSE_USE_CALLBACKS 1 | 44 # define MONGOOSE_USE_CALLBACKS 1 |
45 # if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE) | 45 # if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE) |
46 # error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined | 46 # error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined |
47 # endif | 47 # endif |
48 | 48 # if !defined(CIVETWEB_HAS_WEBDAV_WRITING) |
49 # error Macro CIVETWEB_HAS_WEBDAV_WRITING must be defined | |
50 # endif | |
49 #else | 51 #else |
50 # error "Either Mongoose or Civetweb must be enabled to compile this file" | 52 # error "Either Mongoose or Civetweb must be enabled to compile this file" |
51 #endif | 53 #endif |
52 | 54 |
53 #include <algorithm> | 55 #include <algorithm> |
341 | 343 |
342 return PostDataStatus_Success; | 344 return PostDataStatus_Success; |
343 } | 345 } |
344 | 346 |
345 | 347 |
348 static PostDataStatus ReadBodyWithoutContentLength(std::string& body, | |
349 struct mg_connection *connection) | |
350 { | |
351 // Store the individual chunks in a temporary file, then read it | |
352 // back into the memory buffer "body" | |
353 FileBuffer buffer; | |
354 | |
355 std::string tmp(1024 * 1024, 0); | |
356 | |
357 for (;;) | |
358 { | |
359 int r = mg_read(connection, &tmp[0], tmp.size()); | |
360 if (r < 0) | |
361 { | |
362 return PostDataStatus_Failure; | |
363 } | |
364 else if (r == 0) | |
365 { | |
366 break; | |
367 } | |
368 else | |
369 { | |
370 buffer.Append(tmp.c_str(), r); | |
371 } | |
372 } | |
373 | |
374 buffer.Read(body); | |
375 | |
376 return PostDataStatus_Success; | |
377 } | |
378 | |
379 | |
346 static PostDataStatus ReadBodyToString(std::string& body, | 380 static PostDataStatus ReadBodyToString(std::string& body, |
347 struct mg_connection *connection, | 381 struct mg_connection *connection, |
348 const IHttpHandler::Arguments& headers) | 382 const IHttpHandler::Arguments& headers) |
349 { | 383 { |
350 IHttpHandler::Arguments::const_iterator contentLength = headers.find("content-length"); | 384 IHttpHandler::Arguments::const_iterator contentLength = headers.find("content-length"); |
354 // "Content-Length" is available | 388 // "Content-Length" is available |
355 return ReadBodyWithContentLength(body, connection, contentLength->second); | 389 return ReadBodyWithContentLength(body, connection, contentLength->second); |
356 } | 390 } |
357 else | 391 else |
358 { | 392 { |
359 // No Content-Length. Store the individual chunks in a temporary | 393 // No Content-Length |
360 // file, then read it back into the memory buffer "body" | 394 return ReadBodyWithoutContentLength(body, connection); |
361 FileBuffer buffer; | |
362 | |
363 std::string tmp(1024 * 1024, 0); | |
364 | |
365 for (;;) | |
366 { | |
367 int r = mg_read(connection, &tmp[0], tmp.size()); | |
368 if (r < 0) | |
369 { | |
370 return PostDataStatus_Failure; | |
371 } | |
372 else if (r == 0) | |
373 { | |
374 break; | |
375 } | |
376 else | |
377 { | |
378 buffer.Append(tmp.c_str(), r); | |
379 } | |
380 } | |
381 | |
382 buffer.Read(body); | |
383 | |
384 return PostDataStatus_Success; | |
385 } | 395 } |
386 } | 396 } |
387 | 397 |
388 | 398 |
389 static PostDataStatus ReadBodyToStream(IHttpHandler::IChunkedRequestReader& stream, | 399 static PostDataStatus ReadBodyToStream(IHttpHandler::IChunkedRequestReader& stream, |
694 } | 704 } |
695 } | 705 } |
696 } | 706 } |
697 | 707 |
698 | 708 |
699 bool HttpServer::HandleWebDav(HttpOutput& output, | 709 #if ORTHANC_ENABLE_PUGIXML == 1 |
700 const std::string& method, | 710 |
701 const IHttpHandler::Arguments& headers, | 711 # if CIVETWEB_HAS_WEBDAV_WRITING == 0 |
702 const std::string& uri) | 712 static void AnswerWebDavReadOnly(HttpOutput& output, |
703 { | 713 const std::string& uri) |
714 { | |
715 LOG(ERROR) << "Orthanc was compiled without support for read-write access to WebDAV: " << uri; | |
716 output.SendStatus(HttpStatus_403_Forbidden); | |
717 } | |
718 # endif | |
719 | |
720 static bool HandleWebDav(HttpOutput& output, | |
721 const HttpServer::WebDavBuckets& buckets, | |
722 const std::string& method, | |
723 const IHttpHandler::Arguments& headers, | |
724 const std::string& uri, | |
725 struct mg_connection *connection /* to read the PUT body if need be */) | |
726 { | |
727 if (buckets.empty()) | |
728 { | |
729 return false; // Speed up things if WebDAV is not used | |
730 } | |
731 | |
732 /** | |
733 * The "buckets" maps an URI relative to the root of the | |
734 * bucket, to the content of the bucket. The root URI does *not* | |
735 * contain a trailing slash. | |
736 **/ | |
737 | |
704 if (method == "OPTIONS") | 738 if (method == "OPTIONS") |
705 { | 739 { |
706 // Remove the trailing slash, if any (necessary for davfs2) | 740 // Remove the trailing slash, if any (necessary for davfs2) |
707 std::string s = uri; | 741 std::string s = uri; |
708 if (!s.empty() && | 742 if (!s.empty() && |
709 s[s.size() - 1] == '/') | 743 s[s.size() - 1] == '/') |
710 { | 744 { |
711 s.resize(s.size() - 1); | 745 s.resize(s.size() - 1); |
712 } | 746 } |
713 | 747 |
714 WebDavBuckets::const_iterator bucket = webDavBuckets_.find(s); | 748 HttpServer::WebDavBuckets::const_iterator bucket = buckets.find(s); |
715 if (bucket == webDavBuckets_.end()) | 749 if (bucket == buckets.end()) |
716 { | 750 { |
717 return false; | 751 return false; |
718 } | 752 } |
719 else | 753 else |
720 { | 754 { |
721 output.AddHeader("DAV", "1"); | 755 output.AddHeader("DAV", "1,2"); // Necessary for Windows XP |
722 output.AddHeader("Allow", "GET, POST, PUT, DELETE, PROPFIND"); | 756 |
757 #if CIVETWEB_HAS_WEBDAV_WRITING == 1 | |
758 output.AddHeader("Allow", "GET, PUT, OPTIONS, PROPFIND, HEAD, LOCK, UNLOCK, PROPPATCH, MKCOL"); | |
759 #else | |
760 output.AddHeader("Allow", "GET, PUT, OPTIONS, PROPFIND, HEAD"); | |
761 #endif | |
762 | |
723 output.SendStatus(HttpStatus_200_Ok); | 763 output.SendStatus(HttpStatus_200_Ok); |
724 return true; | 764 return true; |
725 } | 765 } |
726 } | 766 } |
727 else if (method == "PROPFIND") | 767 else if (method == "GET" || |
728 { | 768 method == "PROPFIND" || |
729 IHttpHandler::Arguments::const_iterator i = headers.find("depth"); | 769 method == "PROPPATCH" || |
730 if (i == headers.end()) | 770 method == "PUT" || |
731 { | 771 method == "HEAD" || |
732 throw OrthancException(ErrorCode_NetworkProtocol, "WebDAV PROPFIND without depth"); | 772 method == "LOCK" || |
733 } | 773 method == "UNLOCK" || |
734 | 774 method == "MKCOL") |
735 int depth = boost::lexical_cast<int>(i->second); | 775 { |
736 if (depth != 0 && | 776 // Locate the WebDAV bucket of interest, if any |
737 depth != 1) | 777 for (HttpServer::WebDavBuckets::const_iterator bucket = buckets.begin(); |
738 { | 778 bucket != buckets.end(); ++bucket) |
739 throw OrthancException( | |
740 ErrorCode_NetworkProtocol, | |
741 "WebDAV PROPFIND at unsupported depth (can only be 0 or 1): " + i->second); | |
742 } | |
743 | |
744 for (WebDavBuckets::const_iterator bucket = webDavBuckets_.begin(); | |
745 bucket != webDavBuckets_.end(); ++bucket) | |
746 { | 779 { |
747 assert(!bucket->first.empty() && | 780 assert(!bucket->first.empty() && |
748 bucket->first[bucket->first.size() - 1] != '/' && | 781 bucket->first[bucket->first.size() - 1] != '/' && |
749 bucket->second != NULL); | 782 bucket->second != NULL); |
750 | 783 |
758 } | 791 } |
759 | 792 |
760 std::vector<std::string> path; | 793 std::vector<std::string> path; |
761 Toolbox::SplitUriComponents(path, s); | 794 Toolbox::SplitUriComponents(path, s); |
762 | 795 |
763 std::string answer; | 796 |
797 /** | |
798 * WebDAV - PROPFIND | |
799 **/ | |
764 | 800 |
765 if (depth == 0) | 801 if (method == "PROPFIND") |
766 { | 802 { |
767 if (bucket->second->IsExistingFolder(path)) | 803 IHttpHandler::Arguments::const_iterator i = headers.find("depth"); |
804 if (i == headers.end()) | |
805 { | |
806 throw OrthancException(ErrorCode_NetworkProtocol, "WebDAV PROPFIND without depth"); | |
807 } | |
808 | |
809 int depth = boost::lexical_cast<int>(i->second); | |
810 if (depth != 0 && | |
811 depth != 1) | |
812 { | |
813 throw OrthancException( | |
814 ErrorCode_NetworkProtocol, | |
815 "WebDAV PROPFIND at unsupported depth (can only be 0 or 1): " + i->second); | |
816 } | |
817 | |
818 std::string answer; | |
819 | |
820 if (depth == 0) | |
821 { | |
822 MimeType mime; | |
823 std::string content; | |
824 boost::posix_time::ptime modificationTime; | |
825 | |
826 if (bucket->second->IsExistingFolder(path)) | |
827 { | |
828 IWebDavBucket::Collection c; | |
829 c.AddResource(new IWebDavBucket::Folder("")); | |
830 c.Format(answer, uri); | |
831 } | |
832 else if (!path.empty() && | |
833 bucket->second->GetFileContent(mime, content, modificationTime, path)) | |
834 { | |
835 std::unique_ptr<IWebDavBucket::File> f(new IWebDavBucket::File(path.back())); | |
836 f->SetContentLength(content.size()); | |
837 f->SetModificationTime(modificationTime); | |
838 f->SetMimeType(mime); | |
839 | |
840 IWebDavBucket::Collection c; | |
841 c.AddResource(f.release()); | |
842 | |
843 std::vector<std::string> p; | |
844 Toolbox::SplitUriComponents(p, uri); | |
845 if (p.empty()) | |
846 { | |
847 throw OrthancException(ErrorCode_InternalError); | |
848 } | |
849 | |
850 p.resize(p.size() - 1); | |
851 c.Format(answer, Toolbox::FlattenUri(p)); | |
852 } | |
853 else | |
854 { | |
855 output.SendStatus(HttpStatus_404_NotFound); | |
856 return true; | |
857 } | |
858 } | |
859 else if (depth == 1) | |
768 { | 860 { |
769 IWebDavBucket::Collection c; | 861 IWebDavBucket::Collection c; |
770 c.AddResource(new IWebDavBucket::Folder("")); | 862 c.AddResource(new IWebDavBucket::Folder("")); // Necessary for empty folders |
863 | |
864 if (!bucket->second->ListCollection(c, path)) | |
865 { | |
866 output.SendStatus(HttpStatus_404_NotFound); | |
867 return true; | |
868 } | |
869 | |
771 c.Format(answer, uri); | 870 c.Format(answer, uri); |
772 } | 871 } |
773 else | 872 else |
774 { | 873 { |
775 output.SendStatus(HttpStatus_404_NotFound); | 874 throw OrthancException(ErrorCode_InternalError); |
776 return true; | |
777 } | 875 } |
876 | |
877 output.AddHeader("Content-Type", "application/xml; charset=UTF-8"); | |
878 output.SendStatus(HttpStatus_207_MultiStatus, answer); | |
879 return true; | |
778 } | 880 } |
779 else if (depth == 1) | 881 |
882 | |
883 /** | |
884 * WebDAV - GET and HEAD | |
885 **/ | |
886 | |
887 else if (method == "GET" || | |
888 method == "HEAD") | |
780 { | 889 { |
781 IWebDavBucket::Collection c; | 890 MimeType mime; |
782 if (!bucket->second->ListCollection(c, path)) | 891 std::string content; |
892 boost::posix_time::ptime modificationTime; | |
893 | |
894 if (bucket->second->GetFileContent(mime, content, modificationTime, path)) | |
895 { | |
896 output.AddHeader("Content-Type", EnumerationToString(mime)); | |
897 | |
898 // "Last-Modified" is necessary on Windows XP. The "Z" | |
899 // suffix is mandatory on Windows >= 7. | |
900 output.AddHeader("Last-Modified", boost::posix_time::to_iso_extended_string(modificationTime) + "Z"); | |
901 | |
902 if (method == "GET") | |
903 { | |
904 output.Answer(content); | |
905 } | |
906 else | |
907 { | |
908 output.SendStatus(HttpStatus_200_Ok); | |
909 } | |
910 } | |
911 else | |
783 { | 912 { |
784 output.SendStatus(HttpStatus_404_NotFound); | 913 output.SendStatus(HttpStatus_404_NotFound); |
785 return true; | |
786 } | 914 } |
787 | 915 |
788 c.Format(answer, uri); | 916 return true; |
917 } | |
918 | |
919 | |
920 /** | |
921 * WebDAV - PUT | |
922 **/ | |
923 | |
924 else if (method == "PUT") | |
925 { | |
926 #if CIVETWEB_HAS_WEBDAV_WRITING == 1 | |
927 std::string body; | |
928 if (ReadBodyToString(body, connection, headers) == PostDataStatus_Success) | |
929 { | |
930 if (bucket->second->StoreFile(body, path)) | |
931 { | |
932 //output.SendStatus(HttpStatus_200_Ok); | |
933 output.SendStatus(HttpStatus_201_Created); | |
934 } | |
935 else | |
936 { | |
937 output.SendStatus(HttpStatus_403_Forbidden); | |
938 } | |
939 } | |
940 else | |
941 { | |
942 LOG(ERROR) << "Cannot read the content of a file to be stored in WebDAV"; | |
943 output.SendStatus(HttpStatus_400_BadRequest); | |
944 } | |
945 #else | |
946 AnswerWebDavReadOnly(output, uri); | |
947 #endif | |
948 | |
949 return true; | |
950 } | |
951 | |
952 | |
953 /** | |
954 * WebDAV - MKCOL | |
955 **/ | |
956 | |
957 else if (method == "MKCOL") | |
958 { | |
959 #if CIVETWEB_HAS_WEBDAV_WRITING == 1 | |
960 if (bucket->second->CreateFolder(path)) | |
961 { | |
962 //output.SendStatus(HttpStatus_200_Ok); | |
963 output.SendStatus(HttpStatus_201_Created); | |
964 } | |
965 else | |
966 { | |
967 output.SendStatus(HttpStatus_403_Forbidden); | |
968 } | |
969 #else | |
970 AnswerWebDavReadOnly(output, uri); | |
971 #endif | |
972 | |
973 return true; | |
974 } | |
975 | |
976 | |
977 /** | |
978 * WebDAV - Faking PROPPATCH, LOCK and UNLOCK | |
979 **/ | |
980 | |
981 else if (method == "PROPPATCH") | |
982 { | |
983 #if CIVETWEB_HAS_WEBDAV_WRITING == 1 | |
984 IWebDavBucket::AnswerFakedProppatch(output, uri); | |
985 #else | |
986 AnswerWebDavReadOnly(output, uri); | |
987 #endif | |
988 return true; | |
989 } | |
990 else if (method == "LOCK") | |
991 { | |
992 #if CIVETWEB_HAS_WEBDAV_WRITING == 1 | |
993 IWebDavBucket::AnswerFakedLock(output, uri); | |
994 #else | |
995 AnswerWebDavReadOnly(output, uri); | |
996 #endif | |
997 return true; | |
998 } | |
999 else if (method == "UNLOCK") | |
1000 { | |
1001 #if CIVETWEB_HAS_WEBDAV_WRITING == 1 | |
1002 IWebDavBucket::AnswerFakedUnlock(output); | |
1003 #else | |
1004 AnswerWebDavReadOnly(output, uri); | |
1005 #endif | |
1006 return true; | |
789 } | 1007 } |
790 else | 1008 else |
791 { | 1009 { |
792 throw OrthancException(ErrorCode_InternalError); | 1010 throw OrthancException(ErrorCode_InternalError); |
793 } | 1011 } |
794 | |
795 output.AddHeader("DAV", "1"); | |
796 output.AddHeader("Content-Type", "application/xml"); | |
797 output.SendStatus(HttpStatus_207_MultiStatus, answer); | |
798 return true; | |
799 } | 1012 } |
800 } | 1013 } |
801 | 1014 |
802 return false; | 1015 return false; |
803 } | 1016 } |
804 else | 1017 else |
805 { | 1018 { |
1019 /** | |
1020 * WebDAV - Unapplicable method (such as POST and DELETE) | |
1021 **/ | |
1022 | |
806 return false; | 1023 return false; |
807 } | 1024 } |
808 } | 1025 } |
809 | 1026 #endif /* ORTHANC_ENABLE_PUGIXML == 1 */ |
810 | 1027 |
1028 | |
811 static void InternalCallback(HttpOutput& output /* out */, | 1029 static void InternalCallback(HttpOutput& output /* out */, |
812 HttpMethod& method /* out */, | 1030 HttpMethod& method /* out */, |
813 HttpServer& server, | 1031 HttpServer& server, |
814 struct mg_connection *connection, | 1032 struct mg_connection *connection, |
815 const struct mg_request_info *request) | 1033 const struct mg_request_info *request) |
908 | 1126 |
909 | 1127 |
910 // Compute the HTTP method, taking method faking into consideration | 1128 // Compute the HTTP method, taking method faking into consideration |
911 method = HttpMethod_Get; | 1129 method = HttpMethod_Get; |
912 | 1130 |
1131 #if ORTHANC_ENABLE_PUGIXML == 1 | |
913 bool isWebDav = false; | 1132 bool isWebDav = false; |
1133 #endif | |
1134 | |
914 HttpMethod filterMethod; | 1135 HttpMethod filterMethod; |
1136 | |
915 | 1137 |
916 if (ExtractMethod(method, request, headers, argumentsGET)) | 1138 if (ExtractMethod(method, request, headers, argumentsGET)) |
917 { | 1139 { |
918 LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri); | 1140 LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri); |
919 filterMethod = method; | 1141 filterMethod = method; |
920 } | 1142 } |
1143 #if ORTHANC_ENABLE_PUGIXML == 1 | |
921 else if (!strcmp(request->request_method, "OPTIONS") || | 1144 else if (!strcmp(request->request_method, "OPTIONS") || |
922 !strcmp(request->request_method, "PROPFIND")) | 1145 !strcmp(request->request_method, "PROPFIND") || |
923 { | 1146 !strcmp(request->request_method, "HEAD")) |
924 LOG(INFO) << "Incoming WebDAV request: " << request->request_method << " " << requestUri; | 1147 { |
1148 LOG(INFO) << "Incoming read-only WebDAV request: " | |
1149 << request->request_method << " " << requestUri; | |
925 filterMethod = HttpMethod_Get; | 1150 filterMethod = HttpMethod_Get; |
926 isWebDav = true; | 1151 isWebDav = true; |
927 } | 1152 } |
1153 else if (!strcmp(request->request_method, "PROPPATCH") || | |
1154 !strcmp(request->request_method, "LOCK") || | |
1155 !strcmp(request->request_method, "UNLOCK") || | |
1156 !strcmp(request->request_method, "MKCOL")) | |
1157 { | |
1158 LOG(INFO) << "Incoming read-write WebDAV request: " | |
1159 << request->request_method << " " << requestUri; | |
1160 filterMethod = HttpMethod_Put; | |
1161 isWebDav = true; | |
1162 } | |
1163 #endif /* ORTHANC_ENABLE_PUGIXML == 1 */ | |
928 else | 1164 else |
929 { | 1165 { |
930 LOG(INFO) << "Unknown HTTP method: " << request->request_method; | 1166 LOG(INFO) << "Unknown HTTP method: " << request->request_method; |
931 output.SendStatus(HttpStatus_400_BadRequest); | 1167 output.SendStatus(HttpStatus_400_BadRequest); |
932 return; | 1168 return; |
947 return; | 1183 return; |
948 } | 1184 } |
949 } | 1185 } |
950 | 1186 |
951 | 1187 |
952 if (server.HandleWebDav(output, request->request_method, headers, requestUri)) | 1188 #if ORTHANC_ENABLE_PUGIXML == 1 |
1189 if (HandleWebDav(output, server.GetWebDavBuckets(), request->request_method, | |
1190 headers, requestUri, connection)) | |
953 { | 1191 { |
954 return; | 1192 return; |
955 } | 1193 } |
956 else if (isWebDav) | 1194 else if (isWebDav) |
957 { | 1195 { |
958 LOG(INFO) << "No WebDAV bucket is registered against URI: " | 1196 LOG(INFO) << "No WebDAV bucket is registered against URI: " |
959 << request->request_method << " " << requestUri; | 1197 << request->request_method << " " << requestUri; |
960 output.SendStatus(HttpStatus_404_NotFound); | 1198 output.SendStatus(HttpStatus_404_NotFound); |
961 return; | 1199 return; |
962 } | 1200 } |
1201 #endif | |
963 | 1202 |
964 | 1203 |
965 bool found = false; | 1204 bool found = false; |
966 | 1205 |
967 // Extract the body of the request for PUT and POST, or process | 1206 // Extract the body of the request for PUT and POST, or process |
1379 throw OrthancException(ErrorCode_HttpPortInUse, | 1618 throw OrthancException(ErrorCode_HttpPortInUse, |
1380 " (port = " + boost::lexical_cast<std::string>(port_) + ")"); | 1619 " (port = " + boost::lexical_cast<std::string>(port_) + ")"); |
1381 } | 1620 } |
1382 } | 1621 } |
1383 | 1622 |
1623 #if ORTHANC_ENABLE_PUGIXML == 1 | |
1624 for (WebDavBuckets::iterator it = webDavBuckets_.begin(); it != webDavBuckets_.end(); ++it) | |
1625 { | |
1626 assert(it->second != NULL); | |
1627 it->second->Start(); | |
1628 } | |
1629 #endif | |
1630 | |
1384 LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber() | 1631 LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber() |
1385 << " (HTTPS encryption is " | 1632 << " (HTTPS encryption is " |
1386 << (IsSslEnabled() ? "enabled" : "disabled") | 1633 << (IsSslEnabled() ? "enabled" : "disabled") |
1387 << ", remote access is " | 1634 << ", remote access is " |
1388 << (IsRemoteAccessAllowed() ? "" : "not ") | 1635 << (IsRemoteAccessAllowed() ? "" : "not ") |
1393 void HttpServer::Stop() | 1640 void HttpServer::Stop() |
1394 { | 1641 { |
1395 if (IsRunning()) | 1642 if (IsRunning()) |
1396 { | 1643 { |
1397 mg_stop(pimpl_->context_); | 1644 mg_stop(pimpl_->context_); |
1645 | |
1646 #if ORTHANC_ENABLE_PUGIXML == 1 | |
1647 for (WebDavBuckets::iterator it = webDavBuckets_.begin(); it != webDavBuckets_.end(); ++it) | |
1648 { | |
1649 assert(it->second != NULL); | |
1650 it->second->Stop(); | |
1651 } | |
1652 #endif | |
1653 | |
1398 pimpl_->context_ = NULL; | 1654 pimpl_->context_ = NULL; |
1399 } | 1655 } |
1400 } | 1656 } |
1401 | 1657 |
1402 | 1658 |