1 /******************************************************************************
2  *
3  * Project:  WMS Client Driver
4  * Purpose:  Implementation of Dataset and RasterBand classes for WMS
5  *           and other similar services.
6  * Author:   Adam Nowacki, nowak@xpam.de
7  *
8  ******************************************************************************
9  * Copyright (c) 2007, Adam Nowacki
10  * Copyright (c) 2007-2013, Even Rouault <even dot rouault at spatialys.com>
11  * Copyright (c) 2016, Lucian Plesea
12  *
13  * Permission is hereby granted, free of charge, to any person obtaining a
14  * copy of this software and associated documentation files (the "Software"),
15  * to deal in the Software without restriction, including without limitation
16  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
17  * and/or sell copies of the Software, and to permit persons to whom the
18  * Software is furnished to do so, subject to the following conditions:
19  *
20  * The above copyright notice and this permission notice shall be included
21  * in all copies or substantial portions of the Software.
22  *
23  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
24  * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
26  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
29  * DEALINGS IN THE SOFTWARE.
30  ****************************************************************************/
31 
32 #include "wmsdriver.h"
33 #include <algorithm>
34 
35 #if !CURL_AT_LEAST_VERSION(7,28,0)
36 // Needed for curl_multi_wait()
37 #error Need libcurl version 7.28.0 or newer
38 // 7.28 was released in Oct 2012
39 #endif
40 
WriteFunc(void * buffer,size_t count,size_t nmemb,void * req)41 static size_t WriteFunc(void *buffer, size_t count, size_t nmemb, void *req) {
42     WMSHTTPRequest *psRequest = reinterpret_cast<WMSHTTPRequest *>(req);
43     size_t size = count * nmemb;
44 
45     if (size == 0) return 0;
46 
47     const size_t required_size = psRequest->nDataLen + size + 1;
48     if (required_size > psRequest->nDataAlloc) {
49         size_t new_size = required_size * 2;
50         if (new_size < 512) new_size = 512;
51         psRequest->nDataAlloc = new_size;
52         GByte * pabyNewData = reinterpret_cast<GByte *>(VSIRealloc(psRequest->pabyData, new_size));
53         if (pabyNewData == nullptr) {
54             VSIFree(psRequest->pabyData);
55             psRequest->pabyData = nullptr;
56             psRequest->Error.Printf("Out of memory allocating %u bytes for HTTP data buffer.",
57                 static_cast<unsigned int>(new_size));
58             psRequest->nDataAlloc = 0;
59             psRequest->nDataLen = 0;
60             return 0;
61         }
62         psRequest->pabyData = pabyNewData;
63     }
64     memcpy(psRequest->pabyData + psRequest->nDataLen, buffer, size);
65     psRequest->nDataLen += size;
66     psRequest->pabyData[psRequest->nDataLen] = 0;
67     return nmemb;
68 }
69 
70 // Process curl errors
ProcessCurlErrors(CURLMsg * msg,WMSHTTPRequest * pasRequest,int nRequestCount)71 static void ProcessCurlErrors(CURLMsg* msg, WMSHTTPRequest* pasRequest, int nRequestCount)
72 {
73     CPLAssert(msg != nullptr);
74     CPLAssert(msg->msg == CURLMSG_DONE);
75 
76     // in case of local file error: update status code
77     if (msg->data.result == CURLE_FILE_COULDNT_READ_FILE) {
78         // identify current request
79         for (int current_req_i = 0; current_req_i < nRequestCount; ++current_req_i) {
80             WMSHTTPRequest* const psRequest = &pasRequest[current_req_i];
81             if (psRequest->m_curl_handle != msg->easy_handle)
82                 continue;
83 
84             // sanity check for local files
85             if (STARTS_WITH(psRequest->URL.c_str(), "file://")) {
86                 psRequest->nStatus = 404;
87                 break;
88             }
89         }
90     }
91 }
92 
93 // Builds a curl request
WMSHTTPInitializeRequest(WMSHTTPRequest * psRequest)94 void WMSHTTPInitializeRequest(WMSHTTPRequest *psRequest) {
95     psRequest->nStatus = 0;
96     psRequest->pabyData = nullptr;
97     psRequest->nDataLen = 0;
98     psRequest->nDataAlloc = 0;
99 
100     psRequest->m_curl_handle = curl_easy_init();
101     if (psRequest->m_curl_handle == nullptr) {
102         CPLError(CE_Fatal, CPLE_AppDefined, "CPLHTTPInitializeRequest(): Unable to create CURL handle.");
103         // This should return somehow?
104     }
105 
106     if (!psRequest->Range.empty())
107     {
108         CPL_IGNORE_RET_VAL(
109             curl_easy_setopt(psRequest->m_curl_handle, CURLOPT_RANGE, psRequest->Range.c_str()));
110     }
111 
112     CPL_IGNORE_RET_VAL(curl_easy_setopt(psRequest->m_curl_handle, CURLOPT_WRITEDATA, psRequest));
113     CPL_IGNORE_RET_VAL(curl_easy_setopt(psRequest->m_curl_handle, CURLOPT_WRITEFUNCTION, WriteFunc));
114 
115     psRequest->m_curl_error.resize(CURL_ERROR_SIZE + 1);
116     CPL_IGNORE_RET_VAL(curl_easy_setopt(psRequest->m_curl_handle, CURLOPT_ERRORBUFFER, &psRequest->m_curl_error[0]));
117 
118     psRequest->m_headers = static_cast<struct curl_slist*>(
119             CPLHTTPSetOptions(psRequest->m_curl_handle, psRequest->URL.c_str(), psRequest->options));
120     const char* pszAccept = CSLFetchNameValue(psRequest->options, "ACCEPT");
121     if( pszAccept )
122     {
123         psRequest->m_headers = curl_slist_append(psRequest->m_headers,
124                                         CPLSPrintf("Accept: %s", pszAccept));
125     }
126     if( psRequest->m_headers != nullptr )
127     {
128         CPL_IGNORE_RET_VAL(
129             curl_easy_setopt(psRequest->m_curl_handle, CURLOPT_HTTPHEADER,
130                          psRequest->m_headers));
131     }
132 
133 }
134 
~WMSHTTPRequest()135 WMSHTTPRequest::~WMSHTTPRequest() {
136     if (m_curl_handle != nullptr)
137         curl_easy_cleanup(m_curl_handle);
138     if( m_headers != nullptr )
139         curl_slist_free_all(m_headers);
140     if (pabyData != nullptr)
141         CPLFree(pabyData);
142 }
143 
144 //
145 // Like CPLHTTPFetch, but multiple requests in parallel
146 // By default it uses 5 connections
147 //
WMSHTTPFetchMulti(WMSHTTPRequest * pasRequest,int nRequestCount)148 CPLErr WMSHTTPFetchMulti(WMSHTTPRequest *pasRequest, int nRequestCount) {
149     CPLErr ret = CE_None;
150     CURLM *curl_multi = nullptr;
151     int max_conn;
152     int i, conn_i;
153 
154     CPLAssert(nRequestCount >= 0);
155     if (nRequestCount == 0)
156         return CE_None;
157 
158     const char *max_conn_opt = CSLFetchNameValue(const_cast<char **>(pasRequest->options), "MAXCONN");
159     max_conn = (max_conn_opt == nullptr) ? 5 : MAX(1, MIN(atoi(max_conn_opt), 1000));
160 
161     // If the first url starts with vsimem, assume all do and defer to CPLHTTPFetch
162     if( STARTS_WITH(pasRequest[0].URL.c_str(), "/vsimem/") &&
163         /* Disabled by default for potential security issues */
164         CPLTestBool(CPLGetConfigOption("CPL_CURL_ENABLE_VSIMEM", "FALSE")) )
165     {
166         for(i = 0; i< nRequestCount;i++)
167         {
168             CPLHTTPResult* psResult = CPLHTTPFetch(pasRequest[i].URL.c_str(),
169                                                     const_cast<char**>(pasRequest[i].options));
170             pasRequest[i].pabyData = psResult->pabyData;
171             pasRequest[i].nDataLen = psResult->nDataLen;
172             pasRequest[i].Error = psResult->pszErrBuf ? psResult->pszErrBuf : "";
173             // Conventions are different between this module and cpl_http...
174             if( psResult->pszErrBuf != nullptr &&
175                 strcmp(psResult->pszErrBuf, "HTTP error code : 404") == 0 )
176                 pasRequest[i].nStatus = 404;
177             else
178                 pasRequest[i].nStatus = 200;
179             pasRequest[i].ContentType = psResult->pszContentType ? psResult->pszContentType : "";
180             // took ownership of content, we're done with the rest
181             psResult->pabyData = nullptr;
182             psResult->nDataLen = 0;
183             CPLHTTPDestroyResult(psResult);
184         }
185         return CE_None;
186     }
187 
188     curl_multi = curl_multi_init();
189     if (curl_multi == nullptr) {
190         CPLError(CE_Fatal, CPLE_AppDefined, "CPLHTTPFetchMulti(): Unable to create CURL multi-handle.");
191     }
192 
193     // add at most max_conn requests
194     int torun = std::min(nRequestCount, max_conn);
195     for (conn_i = 0; conn_i < torun; ++conn_i) {
196         WMSHTTPRequest *const psRequest = &pasRequest[conn_i];
197         CPLDebug("HTTP", "Requesting [%d/%d] %s", conn_i + 1, nRequestCount,
198             pasRequest[conn_i].URL.c_str());
199         curl_multi_add_handle(curl_multi, psRequest->m_curl_handle);
200     }
201 
202     void* old_handler = CPLHTTPIgnoreSigPipe();
203     int still_running;
204     do {
205         CURLMcode mc;
206         do {
207             mc = curl_multi_perform(curl_multi, &still_running);
208         } while (CURLM_CALL_MULTI_PERFORM == mc);
209 
210         // Pick up messages, clean up the completed ones, add more
211         int msgs_in_queue = 0;
212         do {
213             CURLMsg *m = curl_multi_info_read(curl_multi, &msgs_in_queue);
214             if (m && (m->msg == CURLMSG_DONE)) {
215                 ProcessCurlErrors(m, pasRequest, nRequestCount);
216 
217                 curl_multi_remove_handle(curl_multi, m->easy_handle);
218                 if (conn_i < nRequestCount) {
219                     auto psRequest = &pasRequest[conn_i];
220                     CPLDebug("HTTP", "Requesting [%d/%d] %s", conn_i + 1,
221                         nRequestCount, pasRequest[conn_i].URL.c_str());
222                     curl_multi_add_handle(curl_multi, psRequest->m_curl_handle);
223                     ++conn_i;
224                     still_running = 1; // Still have request pending
225                 }
226             }
227         } while (msgs_in_queue);
228 
229         if (CURLM_OK == mc) {
230             int numfds;
231             curl_multi_wait(curl_multi, nullptr, 0, 100, &numfds);
232         }
233     } while (still_running || conn_i != nRequestCount);
234 
235     // process any message still in queue
236     CURLMsg* msg;
237     int msgs_in_queue;
238     do {
239         msg = curl_multi_info_read(curl_multi, &msgs_in_queue);
240         if (msg != nullptr) {
241             if (msg->msg == CURLMSG_DONE) {
242                 ProcessCurlErrors(msg, pasRequest, nRequestCount);
243             }
244         }
245     } while (msg != nullptr);
246 
247     CPLHTTPRestoreSigPipeHandler(old_handler);
248 
249     if (conn_i != nRequestCount) { // something gone really really wrong
250         // oddly built libcurl or perhaps absence of network interface
251         CPLError(CE_Failure, CPLE_AppDefined,
252                  "WMSHTTPFetchMulti(): conn_i != nRequestCount, this should never happen ...");
253         nRequestCount = conn_i;
254         ret = CE_Failure;
255     }
256 
257     for (i = 0; i < nRequestCount; ++i) {
258         WMSHTTPRequest *const psRequest = &pasRequest[i];
259 
260         long response_code;
261         curl_easy_getinfo(psRequest->m_curl_handle, CURLINFO_RESPONSE_CODE, &response_code);
262         // for local files, don't update the status code if one is already set
263         if(!(psRequest->nStatus != 0 && STARTS_WITH(psRequest->URL.c_str(), "file://")))
264             psRequest->nStatus = static_cast<int>(response_code);
265 
266         char *content_type = nullptr;
267         curl_easy_getinfo(psRequest->m_curl_handle, CURLINFO_CONTENT_TYPE, &content_type);
268         psRequest->ContentType = content_type ? content_type : "";
269 
270         if (psRequest->Error.empty())
271             psRequest->Error = &psRequest->m_curl_error[0];
272 
273         /* In the case of a file:// URL, curl will return a status == 0, so if there's no */
274         /* error returned, patch the status code to be 200, as it would be for http:// */
275         if (psRequest->nStatus == 0 && psRequest->Error.empty() && STARTS_WITH(psRequest->URL.c_str(), "file://"))
276             psRequest->nStatus = 200;
277 
278         // If there is an error with no error message, use the content if it is text
279         if (psRequest->Error.empty()
280             && psRequest->nStatus != 0
281             && psRequest->nStatus != 200
282             && strstr(psRequest->ContentType, "text")
283             && psRequest->pabyData != nullptr )
284             psRequest->Error = reinterpret_cast<const char *>(psRequest->pabyData);
285 
286         CPLDebug("HTTP", "Request [%d] %s : status = %d, type = %s, error = %s",
287                  i, psRequest->URL.c_str(), psRequest->nStatus,
288                  !psRequest->ContentType.empty() ? psRequest->ContentType.c_str() : "(null)",
289                  !psRequest->Error.empty() ? psRequest->Error.c_str() : "(null)");
290 
291         curl_multi_remove_handle(curl_multi, pasRequest->m_curl_handle);
292     }
293 
294     curl_multi_cleanup(curl_multi);
295 
296     return ret;
297 }
298