Bug 1310703 - Introduce the pingsender executable; r?ted draft
authorGabriele Svelto <gsvelto@mozilla.com>
Mon, 16 Jan 2017 17:39:22 +0100
changeset 482803 9f8cf41a95a387a7b9cd67bf4d861cd1db0d698c
parent 482778 3c300355a94d24e0c1c156f9de4a6e9c60beaa28
child 482804 630ea8a52f31e1e308f37f5c515be78a9066f5a0
push id45169
push usergsvelto@mozilla.com
push dateMon, 13 Feb 2017 13:46:14 +0000
reviewersted
bugs1310703
milestone54.0a1
Bug 1310703 - Introduce the pingsender executable; r?ted MozReview-Commit-ID: HxU41s0yamZ
browser/app/macbuild/Contents/MacOS-files.in
browser/installer/package-manifest.in
browser/installer/windows/nsis/shared.nsh
python/mozbuild/mozbuild/artifacts.py
toolkit/components/telemetry/docs/internals/index.rst
toolkit/components/telemetry/docs/internals/pingsender.rst
toolkit/components/telemetry/moz.build
toolkit/components/telemetry/pingsender/moz.build
toolkit/components/telemetry/pingsender/pingsender.cpp
toolkit/components/telemetry/pingsender/pingsender.h
toolkit/components/telemetry/pingsender/pingsender_unix_common.cpp
toolkit/components/telemetry/pingsender/pingsender_win.cpp
--- a/browser/app/macbuild/Contents/MacOS-files.in
+++ b/browser/app/macbuild/Contents/MacOS-files.in
@@ -1,9 +1,10 @@
 /*.app/***
 /*.dylib
 /certutil
 /firefox-bin
 /gtest/***
+/pingsender
 /pk12util
 /ssltunnel
 /xpcshell
 /XUL
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -765,28 +765,32 @@ bin/libfreebl_32int64_3.so
 #ifdef MOZ_CRASHREPORTER
 @RESPATH@/components/CrashService.manifest
 @RESPATH@/components/CrashService.js
 @RESPATH@/components/toolkit_crashservice.xpt
 #ifdef XP_MACOSX
 @BINPATH@/crashreporter.app/
 #else
 @BINPATH@/crashreporter@BIN_SUFFIX@
+@RESPATH@/crashreporter.ini
 @BINPATH@/minidump-analyzer@BIN_SUFFIX@
-@RESPATH@/crashreporter.ini
 #ifdef XP_UNIX
 @RESPATH@/Throbber-small.gif
 #endif
 #endif
 @RESPATH@/browser/crashreporter-override.ini
 #ifdef MOZ_CRASHREPORTER_INJECTOR
 @BINPATH@/breakpadinjector.dll
 #endif
 #endif
 
+; [ Ping Sender ]
+;
+@BINPATH@/pingsender@BIN_SUFFIX@
+
 @RESPATH@/components/dom_audiochannel.xpt
 
 ; Shutdown Terminator
 @RESPATH@/components/nsTerminatorTelemetry.js
 @RESPATH@/components/terminator.manifest
 
 #if defined(CLANG_CXX)
 #if defined(MOZ_ASAN) || defined(MOZ_TSAN)
--- a/browser/installer/windows/nsis/shared.nsh
+++ b/browser/installer/windows/nsis/shared.nsh
@@ -1130,16 +1130,17 @@
   Push "freebl3.dll"
   Push "nssckbi.dll"
   Push "nspr4.dll"
   Push "nssdbm3.dll"
   Push "mozsqlite3.dll"
   Push "xpcom.dll"
   Push "crashreporter.exe"
   Push "minidump-analyzer.exe"
+  Push "pingsender.exe"
   Push "updater.exe"
   Push "${FileMainEXE}"
 !macroend
 !define PushFilesToCheck "!insertmacro PushFilesToCheck"
 
 
 ; Pushes the string "true" to the top of the stack if the Firewall service is
 ; running and pushes the string "false" to the top of the stack if it isn't.
--- a/python/mozbuild/mozbuild/artifacts.py
+++ b/python/mozbuild/mozbuild/artifacts.py
@@ -262,16 +262,17 @@ class LinuxArtifactJob(ArtifactJob):
 
     package_artifact_patterns = {
         'firefox/application.ini',
         'firefox/crashreporter',
         'firefox/dependentlibs.list',
         'firefox/firefox',
         'firefox/firefox-bin',
         'firefox/minidump-analyzer',
+        'firefox/pingsender',
         'firefox/platform.ini',
         'firefox/plugin-container',
         'firefox/updater',
         'firefox/**/*.so',
         'firefox/**/interfaces.xpt',
     }
 
     def process_package_artifact(self, filename, processed_filename):
@@ -347,16 +348,17 @@ class MacArtifactJob(ArtifactJob):
                 'libnssckbi.dylib',
                 'libnssdbm3.dylib',
                 'libplugin_child_interpose.dylib',
                 # 'libreplace_jemalloc.dylib',
                 # 'libreplace_malloc.dylib',
                 'libmozavutil.dylib',
                 'libmozavcodec.dylib',
                 'libsoftokn3.dylib',
+                'pingsender',
                 'plugin-container.app/Contents/MacOS/plugin-container',
                 'updater.app/Contents/MacOS/org.mozilla.updater',
                 # 'xpcshell',
                 'XUL',
             ])
 
             # These get copied into dist/bin with the path, so "root/a/b/c" -> "dist/bin/a/b/c".
             paths_keep_path = ('Contents/Resources', [
--- a/toolkit/components/telemetry/docs/internals/index.rst
+++ b/toolkit/components/telemetry/docs/internals/index.rst
@@ -2,8 +2,9 @@
 Internals
 =========
 
 .. toctree::
    :maxdepth: 2
    :titlesonly:
 
    preferences
+   pingsender
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/docs/internals/pingsender.rst
@@ -0,0 +1,22 @@
+Ping Sender
+===========
+
+The ping sender is a minimalistic program whose sole purpose is to deliver a
+telemetry ping. It accepts a single parameter which is the URL the ping will
+be sent to (as an HTTP POST command) and once started it will wait to read the
+ping contents on its stdin stream. Once the ping has been read from stdin the
+ping sender will try to post it once, exiting with a non-zero value if it
+fails.
+
+The ping sender relies on libcurl for Linux and Mac build and on WinInet for
+Windows ones for its HTTP functionality. It currently ignores Firefox or the
+system proxy configuration.
+
+In non-debug mode the ping sender doesn't print anything, not even on error,
+this is done deliberately to prevent startling the user on architectures such
+as Windows that would open a seperate console window just to display the
+program output. If you need runtime information to be printed out compile the
+ping sender with debuggging enabled.
+
+The pingsender is not supported on Firefox for Android at the moment
+(see `bug 1335917 <https://bugzilla.mozilla.org/show_bug.cgi?id=1335917>`_)
--- a/toolkit/components/telemetry/moz.build
+++ b/toolkit/components/telemetry/moz.build
@@ -5,16 +5,20 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 HAS_MISC_RULE = True
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 
+DIRS = [
+    'pingsender',
+]
+
 DEFINES['MOZ_APP_VERSION'] = '"%s"' % CONFIG['MOZ_APP_VERSION']
 
 LOCAL_INCLUDES += [
     '/xpcom/build',
     '/xpcom/threads',
 ]
 
 SPHINX_TREES['telemetry'] = 'docs'
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/moz.build
@@ -0,0 +1,34 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if CONFIG['OS_TARGET'] != 'Android':
+    Program('pingsender')
+
+    UNIFIED_SOURCES += [
+        'pingsender.cpp',
+    ]
+
+    LOCAL_INCLUDES += [
+        '/toolkit/crashreporter/google-breakpad/src',
+    ]
+
+    if CONFIG['OS_TARGET'] == 'WINNT':
+        OS_LIBS += [
+            'wininet',
+        ]
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+    UNIFIED_SOURCES += [
+        'pingsender_win.cpp',
+    ]
+elif CONFIG['OS_ARCH'] == 'Darwin' or CONFIG['OS_ARCH'] == 'Linux':
+    UNIFIED_SOURCES += [
+        'pingsender_unix_common.cpp',
+    ]
+
+# Don't use the STL wrappers; we don't link with -lmozalloc, and it really
+# doesn't matter here anyway.
+DISABLE_STL_WRAPPING = True
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/pingsender.cpp
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <cstdlib>
+#include <string>
+#include <cstdio>
+
+#include "pingsender.h"
+
+using std::string;
+
+namespace PingSender {
+
+const char* kUserAgent = "pingsender/1.0";
+
+/**
+ * Read the ping contents from stdin as a string
+ */
+static std::string ReadPingFromStdin()
+{
+  const size_t kBufferSize = 32768;
+  char buff[kBufferSize];
+  string ping;
+  size_t readBytes = 0;
+
+  do {
+    readBytes = fread(buff, 1, kBufferSize, stdin);
+    ping.append(buff, readBytes);
+  } while (!feof(stdin) && !ferror(stdin));
+
+  if (ferror(stdin)) {
+    PINGSENDER_LOG("ERROR: Could not read from stdin\n");
+    return "";
+  }
+
+  return ping;
+}
+
+} // namespace PingSender
+
+using namespace PingSender;
+
+int main(int argc, char* argv[])
+{
+  string url;
+
+  if (argc == 2) {
+    url = argv[1];
+  } else {
+    PINGSENDER_LOG("Usage: pingsender URL\n"
+                   "Send the payload passed via stdin to the specified URL "
+                   "using an HTTP POST message\n");
+    exit(EXIT_FAILURE);
+  }
+
+  if (url.empty()) {
+    PINGSENDER_LOG("ERROR: No URL was provided\n");
+    exit(EXIT_FAILURE);
+  }
+
+  string ping(ReadPingFromStdin());
+
+  if (ping.empty()) {
+    PINGSENDER_LOG("ERROR: Ping payload is empty\n");
+    exit(EXIT_FAILURE);
+  }
+
+  if (!Post(url, ping)) {
+    exit(EXIT_FAILURE);
+  }
+
+  exit(EXIT_SUCCESS);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/pingsender.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <string>
+
+#ifdef DEBUG
+#define PINGSENDER_LOG(s, ...) printf(s, ##__VA_ARGS__)
+#else
+#define PINGSENDER_LOG(s, ...)
+#endif // DEBUG
+
+namespace PingSender {
+
+// System-specific function to make an HTTP POST operation
+bool Post(const std::string& url, const std::string& payload);
+
+} // namespace PingSender
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/pingsender_unix_common.cpp
@@ -0,0 +1,170 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <cerrno>
+#include <cstring>
+#include <string>
+
+#include <dlfcn.h>
+#include <unistd.h>
+
+#include "third_party/curl/curl.h"
+
+namespace PingSender {
+
+using std::string;
+
+/**
+ * A simple wrapper around libcurl "easy" functions. Provides RAII opening
+ * and initialization of the curl library
+ */
+class CurlWrapper
+{
+public:
+  CurlWrapper();
+  ~CurlWrapper();
+  bool Init();
+  bool Post(const string& url, const string& payload);
+
+  // libcurl functions
+  CURL* (*easy_init)(void);
+  CURLcode (*easy_setopt)(CURL*, CURLoption, ...);
+  CURLcode (*easy_perform)(CURL*);
+  CURLcode (*easy_getinfo)(CURL*, CURLINFO, ...);
+  const char* (*easy_strerror)(CURLcode);
+  void (*easy_cleanup)(CURL*);
+
+private:
+  void* mLib;
+  void* mCurl;
+};
+
+CurlWrapper::CurlWrapper()
+  : easy_init(nullptr)
+  , easy_setopt(nullptr)
+  , easy_perform(nullptr)
+  , easy_getinfo(nullptr)
+  , easy_strerror(nullptr)
+  , easy_cleanup(nullptr)
+  , mLib(nullptr)
+  , mCurl(nullptr)
+{}
+
+CurlWrapper::~CurlWrapper()
+{
+  if(mLib) {
+    if(mCurl && easy_cleanup) {
+      easy_cleanup(mCurl);
+    }
+
+    dlclose(mLib);
+  }
+}
+
+bool
+CurlWrapper::Init()
+{
+  // libcurl might show up under different names, try them all until we find it
+  const char* libcurlNames[] = {
+    "libcurl.so",
+    "libcurl.so.4",
+    // Debian gives libcurl a different name when it is built against GnuTLS
+    "libcurl-gnutls.so.4",
+    // Older libcurl if we can't find anything better
+    "libcurl.so.3",
+#ifndef HAVE_64BIT_BUILD
+    // 32-bit versions on 64-bit hosts
+    "/usr/lib32/libcurl.so",
+    "/usr/lib32/libcurl.so.4",
+    "/usr/lib32/libcurl-gnutls.so.4",
+    "/usr/lib32/libcurl.so.3",
+#endif
+    // macOS
+    "libcurl.dylib",
+    "libcurl.4.dylib",
+    "libcurl.3.dylib"
+  };
+
+  for (const char* libname : libcurlNames) {
+    mLib = dlopen(libname, RTLD_NOW);
+
+    if (mLib) {
+      break;
+    }
+  }
+
+  if (!mLib) {
+    PINGSENDER_LOG("ERROR: Could not find libcurl\n");
+    return false;
+  }
+
+  *(void**) (&easy_init) = dlsym(mLib, "curl_easy_init");
+  *(void**) (&easy_setopt) = dlsym(mLib, "curl_easy_setopt");
+  *(void**) (&easy_perform) = dlsym(mLib, "curl_easy_perform");
+  *(void**) (&easy_getinfo) = dlsym(mLib, "curl_easy_getinfo");
+  *(void**) (&easy_strerror) = dlsym(mLib, "curl_easy_strerror");
+  *(void**) (&easy_cleanup) = dlsym(mLib, "curl_easy_cleanup");
+
+  if (!easy_init ||
+      !easy_setopt ||
+      !easy_perform ||
+      !easy_getinfo ||
+      !easy_strerror ||
+      !easy_cleanup) {
+    PINGSENDER_LOG("ERROR: libcurl is missing one of the required symbols\n");
+    return false;
+  }
+
+  mCurl = easy_init();
+
+  if (!mCurl) {
+    PINGSENDER_LOG("ERROR: Could not initialize libcurl\n");
+    return false;
+  }
+
+  return true;
+}
+
+bool
+CurlWrapper::Post(const string& url, const string& payload)
+{
+  easy_setopt(mCurl, CURLOPT_URL, url.c_str());
+  easy_setopt(mCurl, CURLOPT_USERAGENT, kUserAgent);
+
+  // Set the size of the POST data
+  easy_setopt(mCurl, CURLOPT_POSTFIELDSIZE, payload.length());
+
+  // Set the contents of the POST data
+  easy_setopt(mCurl, CURLOPT_POSTFIELDS, payload.c_str());
+
+  // Fail if the server returns a 4xx code
+  easy_setopt(mCurl, CURLOPT_FAILONERROR, 1);
+
+  // Block until the operation is performend. Ignore the response, if the POST
+  // fails we can't do anything about it.
+  CURLcode err = easy_perform(mCurl);
+
+  if (err != CURLE_OK) {
+    PINGSENDER_LOG("ERROR: Failed to send HTTP request, %s\n",
+                   easy_strerror(err));
+    return false;
+  }
+
+  return true;
+}
+
+bool
+Post(const string& url, const string& payload)
+{
+  CurlWrapper curl;
+
+  if (!curl.Init()) {
+    return false;
+  }
+
+  return curl.Post(url, payload);
+}
+
+} // namespace PingSender
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/pingsender_win.cpp
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <string>
+
+#include <windows.h>
+#include <wininet.h>
+
+namespace PingSender {
+
+using std::string;
+
+/**
+ * A helper to automatically close internet handles when they go out of scope
+ */
+class ScopedHInternet {
+public:
+  explicit ScopedHInternet(HINTERNET handle) : mHandle(handle) {}
+
+  ~ScopedHInternet() {
+    if (mHandle) {
+      InternetCloseHandle(mHandle);
+    }
+  }
+
+  bool empty() { return (mHandle == nullptr); }
+  HINTERNET get() { return mHandle; }
+
+private:
+  HINTERNET mHandle;
+};
+
+const size_t kUrlComponentsSchemeLength = 256;
+const size_t kUrlComponentsHostLength = 256;
+const size_t kUrlComponentsPathLength = 256;
+
+/**
+ * Post the specified payload to a telemetry server
+ *
+ * @param url The URL of the telemetry server
+ * @param payload The ping payload
+ */
+bool
+Post(const string& url, const string& payload)
+{
+  char scheme[kUrlComponentsSchemeLength];
+  char host[kUrlComponentsHostLength];
+  char path[kUrlComponentsPathLength];
+
+  URL_COMPONENTS components = {};
+  components.dwStructSize = sizeof(components);
+  components.lpszScheme = scheme;
+  components.dwSchemeLength = kUrlComponentsSchemeLength;
+  components.lpszHostName = host;
+  components.dwHostNameLength = kUrlComponentsHostLength;
+  components.lpszUrlPath = path;
+  components.dwUrlPathLength = kUrlComponentsPathLength;
+
+  if (!InternetCrackUrl(url.c_str(), url.size(), 0, &components)) {
+    PINGSENDER_LOG("ERROR: Could not separate the URL components\n");
+    return false;
+  }
+
+  ScopedHInternet internet(InternetOpen(kUserAgent,
+                                        INTERNET_OPEN_TYPE_PRECONFIG,
+                                        /* lpszProxyName */ NULL,
+                                        /* lpszProxyBypass */ NULL,
+                                        /* dwFlags */ 0));
+
+  if (internet.empty()) {
+    PINGSENDER_LOG("ERROR: Could not open wininet internet handle\n");
+    return false;
+  }
+
+  ScopedHInternet connection(InternetConnect(internet.get(),
+                                             host, components.nPort,
+                                             /* lpszUsername */ NULL,
+                                             /* lpszPassword */ NULL,
+                                             INTERNET_SERVICE_HTTP,
+                                             /* dwFlags */ 0,
+                                             /* dwContext */ NULL));
+
+  if (connection.empty()) {
+    PINGSENDER_LOG("ERROR: Could not connect\n");
+    return false;
+  }
+
+  DWORD flags = ((strcmp(scheme, "https") == 0) ? INTERNET_FLAG_SECURE : 0) |
+                INTERNET_FLAG_NO_COOKIES;
+  ScopedHInternet request(HttpOpenRequest(connection.get(), "POST", path,
+                                          /* lpszVersion */ NULL,
+                                          /* lpszReferer */ NULL,
+                                          /* lplpszAcceptTypes */ NULL,
+                                          flags,
+                                          /* dwContext */ NULL));
+
+  if (request.empty()) {
+    PINGSENDER_LOG("ERROR: Could not open HTTP POST request\n");
+    return false;
+  }
+
+  bool rv = HttpSendRequest(request.get(),
+                            /* lpszHeaders */ nullptr,
+                            /* dwHeadersLength */ 0,
+                            (LPVOID)payload.c_str(),
+                            payload.size());
+  if (!rv) {
+    PINGSENDER_LOG("ERROR: Could not execute HTTP POST request\n");
+  }
+
+  return rv;
+}
+
+} // namespace PingSender