]>
git.ipfire.org Git - thirdparty/pdns.git/blob - modules/remotebackend/httpconnector.cc
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 #include "remotebackend.hh"
26 #include <sys/socket.h>
31 #include "pdns/lock.hh"
34 #define UNIX_PATH_MAX 108
37 HTTPConnector::HTTPConnector(std::map
<std::string
,std::string
> options
) {
38 this->d_url
= options
.find("url")->second
;
39 if (options
.find("url-suffix") != options
.end()) {
40 this->d_url_suffix
= options
.find("url-suffix")->second
;
42 this->d_url_suffix
= "";
46 this->d_post_json
= false;
47 this->d_socket
= NULL
;
49 if (options
.find("timeout") != options
.end()) {
50 this->timeout
= std::stoi(options
.find("timeout")->second
)/1000;
52 if (options
.find("post") != options
.end()) {
53 std::string val
= options
.find("post")->second
;
54 if (val
== "yes" || val
== "true" || val
== "on" || val
== "1") {
58 if (options
.find("post_json") != options
.end()) {
59 std::string val
= options
.find("post_json")->second
;
60 if (val
== "yes" || val
== "true" || val
== "on" || val
== "1") {
61 this->d_post_json
= true;
66 HTTPConnector::~HTTPConnector() {
71 void HTTPConnector::addUrlComponent(const Json
¶meters
, const string
& element
, std::stringstream
& ss
) {
73 if (parameters
[element
] != Json())
74 ss
<< "/" << asString(parameters
[element
]);
77 std::string
HTTPConnector::buildMemberListArgs(std::string prefix
, const Json
& args
) {
78 std::stringstream stream
;
80 for(const auto& pair
: args
.object_items()) {
81 if (pair
.second
.is_bool()) {
82 stream
<< (pair
.second
.bool_value()?"1":"0");
83 } else if (pair
.second
.is_null()) {
84 stream
<< prefix
<< "[" << pair
.first
<< "]=";
86 stream
<< prefix
<< "[" << pair
.first
<< "]=" << this->asString(pair
.second
);
91 return stream
.str().substr(0, stream
.str().size()-1); // snip the trailing &
94 // builds our request (near-restful)
95 void HTTPConnector::restful_requestbuilder(const std::string
&method
, const Json
& parameters
, YaHTTP::Request
& req
)
101 // special names are qname, name, zonename, kind, others go to headers
107 // add the url components, if found, in following order.
108 // id must be first due to the fact that the qname/name can be empty
110 addUrlComponent(parameters
, "id", ss
);
111 addUrlComponent(parameters
, "domain_id", ss
);
112 addUrlComponent(parameters
, "zonename", ss
);
113 addUrlComponent(parameters
, "qname", ss
);
114 addUrlComponent(parameters
, "name", ss
);
115 addUrlComponent(parameters
, "kind", ss
);
116 addUrlComponent(parameters
, "qtype", ss
);
118 // set the correct type of request based on method
119 if (method
== "activateDomainKey" || method
== "deactivateDomainKey") {
120 // create an empty post
123 } else if (method
== "setTSIGKey") {
124 req
.POST()["algorithm"] = parameters
["algorithm"].string_value();
125 req
.POST()["content"] = parameters
["content"].string_value();
128 } else if (method
== "deleteTSIGKey") {
130 } else if (method
== "addDomainKey") {
131 const Json
& param
= parameters
["key"];
132 req
.POST()["flags"] = asString(param
["flags"]);
133 req
.POST()["active"] = (param
["active"].bool_value() ? "1" : "0");
134 req
.POST()["content"] = param
["content"].string_value();
137 } else if (method
== "isMaster") {
138 addUrlComponent(parameters
, "ip", ss
);
140 } else if (method
== "superMasterBackend") {
141 std::stringstream ss2
;
142 addUrlComponent(parameters
, "ip", ss
);
143 addUrlComponent(parameters
, "domain", ss
);
144 // then we need to serialize rrset payload into POST
145 for(size_t index
= 0; index
< parameters
["nsset"].array_items().size(); index
++) {
146 ss2
<< buildMemberListArgs("nsset[" + std::to_string(index
) + "]", parameters
["nsset"][index
]) << "&";
148 req
.body
= ss2
.str().substr(0, ss2
.str().size()-1);
149 req
.headers
["content-type"] = "application/x-www-form-urlencoded; charset=utf-8";
150 req
.headers
["content-length"] = std::to_string(req
.body
.size());
152 } else if (method
== "createSlaveDomain") {
153 addUrlComponent(parameters
, "ip", ss
);
154 addUrlComponent(parameters
, "domain", ss
);
155 if (parameters
["account"].is_null() == false && parameters
["account"].is_string()) {
156 req
.POST()["account"] = parameters
["account"].string_value();
160 } else if (method
== "replaceRRSet") {
161 std::stringstream ss2
;
162 for(size_t index
= 0; index
< parameters
["rrset"].array_items().size(); index
++) {
163 ss2
<< buildMemberListArgs("rrset[" + std::to_string(index
) + "]", parameters
["rrset"][index
]);
165 req
.body
= ss2
.str();
166 req
.headers
["content-type"] = "application/x-www-form-urlencoded; charset=utf-8";
167 req
.headers
["content-length"] = std::to_string(req
.body
.size());
169 } else if (method
== "feedRecord") {
170 addUrlComponent(parameters
, "trxid", ss
);
171 req
.body
= buildMemberListArgs("rr", parameters
["rr"]);
172 req
.headers
["content-type"] = "application/x-www-form-urlencoded; charset=utf-8";
173 req
.headers
["content-length"] = std::to_string(req
.body
.size());
175 } else if (method
== "feedEnts") {
176 std::stringstream ss2
;
177 addUrlComponent(parameters
, "trxid", ss
);
178 for(const auto& param
: parameters
["nonterm"].array_items()) {
179 ss2
<< "nonterm[]=" << YaHTTP::Utility::encodeURL(param
.string_value(), false) << "&";
181 for(const auto& param
: parameters
["auth"].array_items()) {
182 ss2
<< "auth[]=" << (param
["auth"].bool_value()?"1":"0") << "&";
184 req
.body
= ss2
.str().substr(0, ss2
.str().size()-1);
185 req
.headers
["content-type"] = "application/x-www-form-urlencoded; charset=utf-8";
186 req
.headers
["content-length"] = std::to_string(req
.body
.size());
188 } else if (method
== "feedEnts3") {
189 std::stringstream ss2
;
190 addUrlComponent(parameters
, "domain", ss
);
191 addUrlComponent(parameters
, "trxid", ss
);
192 ss2
<< "times=" << parameters
["times"].int_value() << "&salt=" << YaHTTP::Utility::encodeURL(parameters
["salt"].string_value(), false) << "&narrow=" << (parameters
["narrow"].bool_value() ? 1 : 0) << "&";
193 for(const auto& param
: parameters
["nonterm"].array_items()) {
194 ss2
<< "nonterm[]=" << YaHTTP::Utility::encodeURL(param
.string_value(), false) << "&";
196 for(const auto& param
: parameters
["auth"].array_items()) {
197 ss2
<< "auth[]=" << (param
["auth"].bool_value()?"1":"0") << "&";
199 req
.body
= ss2
.str().substr(0, ss2
.str().size()-1);
200 req
.headers
["content-type"] = "application/x-www-form-urlencoded; charset=utf-8";
201 req
.headers
["content-length"] = std::to_string(req
.body
.size());
203 } else if (method
== "startTransaction") {
204 addUrlComponent(parameters
, "domain", ss
);
205 addUrlComponent(parameters
, "trxid", ss
);
208 } else if (method
== "commitTransaction" || method
== "abortTransaction") {
209 addUrlComponent(parameters
, "trxid", ss
);
212 } else if (method
== "calculateSOASerial") {
213 addUrlComponent(parameters
, "domain", ss
);
214 req
.body
= buildMemberListArgs("sd", parameters
["sd"]);
215 req
.headers
["content-type"] = "application/x-www-form-urlencoded; charset=utf-8";
216 req
.headers
["content-length"] = std::to_string(req
.body
.size());
218 } else if (method
== "setDomainMetadata") {
219 // copy all metadata values into post
220 std::stringstream ss2
;
221 // this one has values too
222 if (parameters
["value"].is_array()) {
223 for(const auto& val
: parameters
["value"].array_items()) {
224 ss2
<< "value[]=" << YaHTTP::Utility::encodeURL(val
.string_value(), false) << "&";
227 req
.body
= ss2
.str().substr(0, ss2
.str().size()-1);
228 req
.headers
["content-type"] = "application/x-www-form-urlencoded; charset=utf-8";
229 req
.headers
["content-length"] = std::to_string(req
.body
.size());
231 } else if (method
== "removeDomainKey") {
232 // this one is delete
234 } else if (method
== "setNotified") {
235 req
.POST()["serial"] = std::to_string(parameters
["serial"].number_value());
238 } else if (method
== "directBackendCmd") {
239 req
.POST()["query"] = parameters
["query"].string_value();
242 } else if (method
== "searchRecords" || method
== "searchComments") {
243 req
.GET()["pattern"] = parameters
["pattern"].string_value();
244 req
.GET()["maxResults"] = std::to_string(parameters
["maxResults"].int_value());
246 } else if (method
== "getAllDomains") {
247 req
.GET()["includeDisabled"] = (parameters
["include_disabled"].bool_value()?"true":"false");
250 // perform normal get
254 // put everything else into headers
255 for(const auto& pair
: parameters
.object_items()) {
256 std::string member
= pair
.first
;
257 // whitelist header parameters
258 if ((member
== "trxid" ||
260 member
== "remote" ||
261 member
== "real-remote" ||
262 member
== "zone-id")) {
263 std::string hdr
= "x-remotebackend-" + member
;
264 req
.headers
[hdr
] = asString(pair
.second
);
268 // finally add suffix and store url
271 req
.setup(verb
, ss
.str());
272 req
.headers
["accept"] = "application/json";
275 void HTTPConnector::post_requestbuilder(const Json
& input
, YaHTTP::Request
& req
) {
276 if (this->d_post_json
) {
277 std::string out
= input
.dump();
278 req
.setup("POST", d_url
);
279 // simple case, POST JSON into url. nothing fancy.
280 req
.headers
["Content-Type"] = "text/javascript; charset=utf-8";
281 req
.headers
["Content-Length"] = std::to_string(out
.size());
282 req
.headers
["accept"] = "application/json";
285 std::stringstream url
,content
;
286 // call url/method.suffix
287 url
<< d_url
<< "/" << input
["method"].string_value() << d_url_suffix
;
288 req
.setup("POST", url
.str());
289 // then build content
290 req
.POST()["parameters"] = input
["parameters"].dump();
292 req
.headers
["accept"] = "application/json";
296 int HTTPConnector::send_message(const Json
& input
) {
299 std::vector
<std::string
> members
;
301 std::ostringstream out
;
307 post_requestbuilder(input
, req
);
309 restful_requestbuilder(input
["method"].string_value(), input
["parameters"], req
);
312 req
.headers
["connection"] = "Keep-Alive"; // see if we can streamline requests (not needed, strictly speaking)
316 // try sending with current socket, if it fails retry with new socket
317 if (this->d_socket
!= NULL
) {
318 fd
= this->d_socket
->getHandle();
319 // there should be no data waiting
320 if (waitForRWData(fd
, true, 0, 1000) < 1) {
322 d_socket
->writenWithTimeout(out
.str().c_str(), out
.str().size(), timeout
);
324 } catch (NetworkError
& ne
) {
325 g_log
<<Logger::Error
<<"While writing to HTTP endpoint "<<d_addr
.toStringWithPort()<<": "<<ne
.what()<<std::endl
;
327 g_log
<<Logger::Error
<<"While writing to HTTP endpoint "<<d_addr
.toStringWithPort()<<": exception caught"<<std::endl
;
332 if (rv
== 1) return rv
;
334 delete this->d_socket
;
335 this->d_socket
= NULL
;
337 if (req
.url
.protocol
== "unix") {
338 // connect using unix socket
341 struct addrinfo
*gAddr
, *gAddrPtr
, hints
;
342 std::string sPort
= std::to_string(req
.url
.port
);
343 memset(&hints
,0,sizeof hints
);
344 hints
.ai_family
= AF_UNSPEC
;
345 hints
.ai_flags
= AI_ADDRCONFIG
;
346 hints
.ai_socktype
= SOCK_STREAM
;
347 hints
.ai_protocol
= 6; // tcp
348 if ((ec
= getaddrinfo(req
.url
.host
.c_str(), sPort
.c_str(), &hints
, &gAddr
)) == 0) {
349 // try to connect to each address.
354 d_socket
= new Socket(gAddrPtr
->ai_family
, gAddrPtr
->ai_socktype
, gAddrPtr
->ai_protocol
);
355 d_addr
.setSockaddr(gAddrPtr
->ai_addr
, gAddrPtr
->ai_addrlen
);
356 d_socket
->connect(d_addr
);
357 d_socket
->setNonBlocking();
358 d_socket
->writenWithTimeout(out
.str().c_str(), out
.str().size(), timeout
);
360 } catch (NetworkError
& ne
) {
361 g_log
<<Logger::Error
<<"While writing to HTTP endpoint "<<d_addr
.toStringWithPort()<<": "<<ne
.what()<<std::endl
;
363 g_log
<<Logger::Error
<<"While writing to HTTP endpoint "<<d_addr
.toStringWithPort()<<": exception caught"<<std::endl
;
369 gAddrPtr
= gAddrPtr
->ai_next
;
374 g_log
<<Logger::Error
<<"Unable to resolve " << req
.url
.host
<< ": " << gai_strerror(ec
) << std::endl
;
381 int HTTPConnector::recv_message(Json
& output
) {
382 YaHTTP::AsyncResponseLoader arl
;
383 YaHTTP::Response resp
;
385 if (d_socket
== NULL
) return -1; // cannot receive :(
391 arl
.initialize(&resp
);
394 t0
= time((time_t*)NULL
);
395 while(arl
.ready() == false && (labs(time((time_t*)NULL
) - t0
) <= timeout
)) {
396 rd
= d_socket
->readWithTimeout(buffer
, sizeof(buffer
), timeout
);
398 throw NetworkError("EOF while reading");
400 throw NetworkError(std::string(strerror(rd
)));
401 arl
.feed(std::string(buffer
, rd
));
404 if (arl
.ready() == false)
405 throw NetworkError("timeout");
406 } catch (NetworkError
&ne
) {
407 g_log
<<Logger::Error
<<"While reading from HTTP endpoint "<<d_addr
.toStringWithPort()<<": "<<ne
.what()<<std::endl
;
412 g_log
<<Logger::Error
<<"While reading from HTTP endpoint "<<d_addr
.toStringWithPort()<<": exception caught"<<std::endl
;
423 if (resp
.status
< 200 || resp
.status
>= 400) {
430 output
= Json::parse(resp
.body
, err
);
431 if (output
!= nullptr) return resp
.body
.size();
432 g_log
<<Logger::Error
<<"Cannot parse JSON reply: "<<err
<<endl
;