webserver: support HTTP OPTIONS requests

This is a port of IMPALA-8982 to Kudu, including:
- Updating squeasel to a newer revision that handles OPTIONS rather than
  responding with an HTTP 500 error code.
- Modifying the webserver to let squeasel handle OPTIONS, bypassing SPNEGO
  authentication.

Change-Id: Icc2328d41ebb30c9ab1b25cb28fcc7e56cc8a25b
Reviewed-on: http://gerrit.cloudera.org:8080/14439
Reviewed-by: Alexey Serbin <aserbin@cloudera.com>
Tested-by: Adar Dembo <adar@cloudera.com>
Reviewed-by: Thomas Tauber-Marshall <tmarshall@cloudera.com>
diff --git a/src/kudu/server/webserver-test.cc b/src/kudu/server/webserver-test.cc
index cb6253f..82ee16b 100644
--- a/src/kudu/server/webserver-test.cc
+++ b/src/kudu/server/webserver-test.cc
@@ -110,6 +110,14 @@
     url_ = Substitute("http://$0", addr_.ToString());
   }
 
+  void RunTestOptions() {
+    curl_.set_custom_method("OPTIONS");
+    curl_.set_return_headers(true);
+    ASSERT_OK(curl_.FetchURL(url_, &buf_));
+    ASSERT_STR_CONTAINS(buf_.ToString(),
+                        "Allow: GET, POST, HEAD, OPTIONS, PROPFIND, MKCOL");
+  }
+
  protected:
   virtual void MaybeSetupSpnego(WebserverOptions* /*opts*/) {}
 
@@ -285,6 +293,12 @@
   } while (!token.empty());
 }
 
+// Tests that even if we don't provide adequate authentication information in
+// an OPTIONS request, the server still honors it.
+TEST_F(SpnegoWebserverTest, TestAuthNotRequiredForOptions) {
+  NO_FATALS(RunTestOptions());
+}
+
 TEST_F(WebserverTest, TestIndexPage) {
   curl_.set_return_headers(true);
   ASSERT_OK(curl_.FetchURL(url_, &buf_));
@@ -449,6 +463,11 @@
   ASSERT_EQ("Remote error: HTTP 403", s.ToString());
 }
 
+// Test that HTTP OPTIONS requests are permitted.
+TEST_F(WebserverTest, TestHttpOptions) {
+  NO_FATALS(RunTestOptions());
+}
+
 class WebserverAdvertisedAddressesTest : public KuduTest {
  public:
   void SetUp() override {
diff --git a/src/kudu/server/webserver.cc b/src/kudu/server/webserver.cc
index 7e34cd5..5efcf1d 100644
--- a/src/kudu/server/webserver.cc
+++ b/src/kudu/server/webserver.cc
@@ -459,6 +459,12 @@
 sq_callback_result_t Webserver::BeginRequestCallback(
     struct sq_connection* connection,
     struct sq_request_info* request_info) {
+  if (strncmp("OPTIONS", request_info->request_method, 7) == 0) {
+    // Let Squeasel deal with the request. OPTIONS requests should not require
+    // authentication, so do this before doing SPNEGO.
+    return SQ_CONTINUE_HANDLING;
+  }
+
   if (opts_.require_spnego) {
     const char* authz_header = sq_get_header(connection, "Authorization");
     string resp_header, authn_princ;
diff --git a/src/kudu/util/curl_util.cc b/src/kudu/util/curl_util.cc
index e80d481..14e247c 100644
--- a/src/kudu/util/curl_util.cc
+++ b/src/kudu/util/curl_util.cc
@@ -21,6 +21,8 @@
 #include <cstdint>
 #include <mutex>
 #include <ostream>
+#include <string>
+#include <vector>
 
 #include <curl/curl.h>
 #include <glog/logging.h>
@@ -30,6 +32,9 @@
 #include "kudu/util/faststring.h"
 #include "kudu/util/scoped_cleanup.h"
 
+using std::string;
+using std::vector;
+
 namespace kudu {
 
 namespace {
@@ -74,21 +79,21 @@
   curl_easy_cleanup(curl_);
 }
 
-Status EasyCurl::FetchURL(const std::string& url, faststring* dst,
-                          const std::vector<std::string>& headers) {
+Status EasyCurl::FetchURL(const string& url, faststring* dst,
+                          const vector<string>& headers) {
   return DoRequest(url, nullptr, dst, headers);
 }
 
-Status EasyCurl::PostToURL(const std::string& url,
-                           const std::string& post_data,
+Status EasyCurl::PostToURL(const string& url,
+                           const string& post_data,
                            faststring* dst) {
   return DoRequest(url, &post_data, dst);
 }
 
-Status EasyCurl::DoRequest(const std::string& url,
-                           const std::string* post_data,
+Status EasyCurl::DoRequest(const string& url,
+                           const string* post_data,
                            faststring* dst,
-                           const std::vector<std::string>& headers) {
+                           const vector<string>& headers) {
   CHECK_NOTNULL(dst)->clear();
 
   if (!verify_peer_) {
@@ -135,6 +140,15 @@
                                                   post_data->c_str())));
   }
 
+  // Done after CURLOPT_POSTFIELDS in case that resets the method (the docs[1]
+  // are unclear on whether that happens).
+  //
+  // 1. https://curl.haxx.se/libcurl/c/CURLOPT_POSTFIELDS.html
+  if (!custom_method_.empty()) {
+    RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST,
+                                                  custom_method_.c_str())));
+  }
+
   RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_HTTPAUTH, CURLAUTH_ANY)));
   if (timeout_.Initialized()) {
     RETURN_NOT_OK(TranslateError(curl_easy_setopt(curl_, CURLOPT_NOSIGNAL, 1)));
diff --git a/src/kudu/util/curl_util.h b/src/kudu/util/curl_util.h
index 6b4ea9e..86f01da 100644
--- a/src/kudu/util/curl_util.h
+++ b/src/kudu/util/curl_util.h
@@ -18,6 +18,7 @@
 #define KUDU_UTIL_CURL_UTIL_H
 
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "kudu/gutil/macros.h"
@@ -77,6 +78,11 @@
     verbose_ = v;
   }
 
+  // Overrides curl's HTTP method handling with a custom method string.
+  void set_custom_method(std::string m) {
+    custom_method_ = std::move(m);
+  }
+
  private:
   // Do a request. If 'post_data' is non-NULL, does a POST.
   // Otherwise, does a GET.
@@ -86,6 +92,8 @@
                    const std::vector<std::string>& headers = {});
   CURL* curl_;
 
+  std::string custom_method_;
+
   // Whether to verify the server certificate.
   bool verify_peer_ = true;
 
diff --git a/thirdparty/vars.sh b/thirdparty/vars.sh
index 1c386c0..1b007f7 100644
--- a/thirdparty/vars.sh
+++ b/thirdparty/vars.sh
@@ -97,7 +97,7 @@
 #  export NAME=squeasel-$(git rev-parse HEAD)
 #  git archive HEAD --prefix=$NAME/ -o /tmp/$NAME.tar.gz
 #  s3cmd put -P /tmp/$NAME.tar.gz s3://cloudera-thirdparty-libs/$NAME.tar.gz
-SQUEASEL_VERSION=7973705170f4744d1806e32695f7ea1e8308ee95
+SQUEASEL_VERSION=36dc66b8723980feb32196a94b46734eb5eafbf9
 SQUEASEL_NAME=squeasel-$SQUEASEL_VERSION
 SQUEASEL_SOURCE=$TP_SOURCE_DIR/$SQUEASEL_NAME