// file : bbot/agent/http-service.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file #include #include #include using namespace std; using namespace butl; namespace bbot { namespace http_service { result post (const agent_options& o, const string& u, const parameters& params) { tracer trace ("http_service::post"); using parser = manifest_parser; using parsing = manifest_parsing; using name_value = manifest_name_value; // The overall plan is to post the data using the curl program, read // the HTTP response status and content type, read and parse the body // according to the content type, and obtain the result message and // optional reference in case of both the request success and failure. // // The successful request response (HTTP status code 200) is expected to // contain the result manifest (text/manifest content type). The faulty // response (HTTP status code other than 200) can either contain the // result manifest or a plain text error description (text/plain content // type) or some other content (for example text/html). We will return // the manifest message value, if available or the first line of the // plain text error description or, as a last resort, construct the // message from the HTTP status code and reason phrase. We will also // return the error description if anything goes wrong with the HTTP // request or the response manifest status value is not 200. // string message; optional status; // Request result manifest status value. optional reference; vector body; optional error; // None of the 3XX redirect code semantics assume automatic re-posting. // We will treat all such codes as failures, adding the location header // value to the message for troubleshooting. // optional location; // Convert the submit arguments to curl's --form* options and cache the // pointer to the file_text parameter value, if present, for writing // into curl's stdin. // strings fos; const string* file_text (nullptr); for (const parameter& p: params) { if (p.type == parameter::file_text) { assert (file_text == nullptr); file_text = &p.value; } fos.emplace_back (p.type == parameter::file || p.type == parameter::file_text ? "--form" : "--form-string"); fos.emplace_back ( p.type == parameter::file ? p.name + "=@" + p.value : p.type == parameter::file_text ? p.name + "=@-" : p.name + '=' + p.value); } // Note that we prefer the low-level process API for running curl over // using butl::curl because in this context it is restrictive and // inconvenient. // // Start curl program. // // Text mode seems appropriate. // fdpipe in_pipe; fdpipe out_pipe; process pr; try { in_pipe = fdopen_pipe (); out_pipe = (file_text != nullptr ? fdopen_pipe () : fdpipe {fdopen_null (), nullfd}); pr = process_start_callback (trace, out_pipe.in.get () /* stdin */, in_pipe /* stdout */, 2 /* stderr */, "curl", // Include the response headers in the // output so we can get the status // code/reason, content type, and the // redirect location. // "--include", "--max-time", o.request_timeout (), "--connect-timeout", o.connect_timeout (), fos, u); // Shouldn't throw, unless something is severely damaged. // in_pipe.out.close (); out_pipe.in.close (); } catch (const process_error& e) { fail << "unable to execute curl: " << e; } catch (const io_error& e) { fail << "unable to open pipe: " << e; } auto finish = [&pr, &error] (bool io_read = false, bool io_write = false) { if (!pr.wait ()) error = "curl " + to_string (*pr.exit); else if (io_read) error = "error reading curl output"; else if (io_write) error = "error writing curl input"; }; bool io_write (false); bool io_read (false); try { // First we read the HTTP response status line and headers. At this // stage we will read until the empty line (containing just CRLF). Not // being able to reach such a line is an error, which is the reason // for the exception mask choice. // ifdstream is ( move (in_pipe.in), fdstream_mode::skip, ifdstream::badbit | ifdstream::failbit | ifdstream::eofbit); if (file_text != nullptr) { ofdstream os (move (out_pipe.out)); os << *file_text; os.close (); // Indicate to the potential IO error handling that we are done with // writing. // file_text = nullptr; } auto bad_response = [] (const string& d) {throw runtime_error (d);}; curl::http_status rs; try { rs = curl::read_http_status (is, false /* skip_headers */); } catch (const invalid_argument& e) { bad_response ( string ("unable to read HTTP response status line: ") + e.what ()); } // Read through the response headers until the empty line is // encountered and obtain the content type and/or the redirect // location, if present. // optional ctype; // Check if the line contains the specified header and return its // value if that's the case. Return nullopt otherwise. // // Note that we don't expect the header values that we are interested // in to span over multiple lines. // string l; auto header = [&l] (const char* name) -> optional { size_t n (string::traits_type::length (name)); if (!(icasecmp (name, l, n) == 0 && l[n] == ':')) return nullopt; string r; size_t p (l.find_first_not_of (' ', n + 1)); // The value begin. if (p != string::npos) { size_t e (l.find_last_not_of (' ')); // The value end. assert (e != string::npos && e >= p); r = string (l, p, e - p + 1); } return optional (move (r)); }; while (!(l = curl::read_http_response_line (is)).empty ()) { if (optional v = header ("Content-Type")) ctype = move (v); else if (optional v = header ("Location")) { if ((rs.code >= 301 && rs.code <= 303) || rs.code == 307) location = move (v); } } assert (!eof (is)); // Would have already failed otherwise. // Now parse the response payload if the content type is specified and // is recognized (text/manifest or text/plain), skip it (with the // ifdstream's close() function) otherwise. // // Note that eof and getline() fail conditions are not errors anymore, // so we adjust the exception mask accordingly. // is.exceptions (ifdstream::badbit); if (ctype) { if (icasecmp ("text/manifest", *ctype, 13) == 0) { parser p (is, "manifest"); name_value nv (p.next ()); if (nv.empty ()) bad_response ("empty manifest"); const string& n (nv.name); string& v (nv.value); // The format version pair is verified by the parser. // assert (n.empty () && v == "1"); body.push_back (move (nv)); // Save the format version pair. auto bad_value = [&p, &nv] (const string& d) { throw parsing (p.name (), nv.value_line, nv.value_column, d);}; // Get and verify the HTTP status. // nv = p.next (); if (n != "status") bad_value ("no status specified"); uint16_t c (curl::parse_http_status_code (v)); if (c == 0) bad_value ("invalid HTTP status '" + v + '\''); if (c != rs.code) bad_value ("status " + v + " doesn't match HTTP response " "code " + to_string (rs.code)); // Get the message. // nv = p.next (); if (n != "message" || v.empty ()) bad_value ("no message specified"); message = move (v); // Try to get an optional reference. // nv = p.next (); if (n == "reference") { if (v.empty ()) bad_value ("empty reference specified"); reference = move (v); nv = p.next (); } // Save the remaining name/value pairs. // for (; !nv.empty (); nv = p.next ()) body.push_back (move (nv)); status = c; } else if (icasecmp ("text/plain", *ctype, 10) == 0) getline (is, message); // Can result in the empty message. } is.close (); // Detect errors. // The only meaningful result we expect is the manifest (status code // is not necessarily 200). We unable to interpret any other cases and // so report them as a bad response. // if (!status) { if (rs.code == 200) bad_response ("manifest expected"); if (message.empty ()) { message = "HTTP status code " + to_string (rs.code); if (!rs.reason.empty ()) message += " (" + lcase (rs.reason) + ')'; } if (location) message += ", new location: " + *location; bad_response ("bad server response"); } } catch (const io_error&) { // Presumably the child process failed and issued diagnostics so let // finish() try to deal with that first. // (file_text != nullptr ? io_write : io_read) = true; } // Handle all parsing errors, including the manifest_parsing exception // that inherits from the runtime_error exception. // // Note that the io_error class inherits from the runtime_error class, // so this catch-clause must go last. // catch (const runtime_error& e) { finish (); // Sets the error variable on process failure. if (!error) error = e.what (); } if (!error) finish (io_read, io_write); assert (error || (status && !message.empty ())); if (!error && *status != 200) error = "status code " + to_string (*status); return result { move (error), move (message), move (reference), move (body)}; } } }