aboutsummaryrefslogtreecommitdiff
path: root/bbot/agent/http-service.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'bbot/agent/http-service.cxx')
-rw-r--r--bbot/agent/http-service.cxx364
1 files changed, 364 insertions, 0 deletions
diff --git a/bbot/agent/http-service.cxx b/bbot/agent/http-service.cxx
new file mode 100644
index 0000000..1921edc
--- /dev/null
+++ b/bbot/agent/http-service.cxx
@@ -0,0 +1,364 @@
+// file : bbot/agent/http-service.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <bbot/agent/http-service.hxx>
+
+#include <libbutl/curl.hxx>
+
+#include <bbot/diagnostics.hxx>
+
+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<uint16_t> status; // Request result manifest status value.
+ optional<string> reference;
+ vector<name_value> body;
+ optional<string> 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<string> 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<string> 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<string>
+ {
+ 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<string> (move (r));
+ };
+
+ while (!(l = curl::read_http_response_line (is)).empty ())
+ {
+ if (optional<string> v = header ("Content-Type"))
+ ctype = move (v);
+ else if (optional<string> 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)};
+ }
+ }
+}