[webkitpy] Remove openssl command dependency.
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 12 Mar 2018 18:05:36 +0000 (18:05 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 12 Mar 2018 18:05:36 +0000 (18:05 +0000)
https://bugs.webkit.org/show_bug.cgi?id=183494

Patch by Basuke Suzuki <Basuke.Suzuki@sony.com> on 2018-03-12
Reviewed by Ryosuke Niwa.

Added Python implementation of PEM file perser and switched to use that
from external `openssl` command.

* Scripts/webkitpy/common/system/pemfile.py: Added.
(load):
(BadFormatError):
(Pem):
(Pem.__init__):
(Pem.get):
(Pem.get_all):
(Pem.certificate):
(Pem.private_key):
(Pem.csr):
(Pem.certificate_request):
(Pem.certificate_signing_request):
(_parse_pem_format):
(_parse_pem_format.find_begin):
(_parse_pem_format.find_end):
(_parse_pem_format.sections):
* Scripts/webkitpy/common/system/pemfile_unittest.py: Added.
(PemFileTest):
(PemFileTest.test_parse):
(PemFileTest.test_parse_bad_format):
* Scripts/webkitpy/port/base.py:
(Port.start_websocket_server):
(Port._extract_certificate_from_pem): Deleted.
(Port._extract_private_key_from_pem): Deleted.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@229541 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Tools/ChangeLog
Tools/Scripts/webkitpy/common/system/pemfile.py [new file with mode: 0644]
Tools/Scripts/webkitpy/common/system/pemfile_unittest.py [new file with mode: 0644]
Tools/Scripts/webkitpy/port/base.py

index aea86bd..6517a10 100644 (file)
@@ -1,3 +1,38 @@
+2018-03-12  Basuke Suzuki  <Basuke.Suzuki@sony.com>
+
+        [webkitpy] Remove openssl command dependency.
+        https://bugs.webkit.org/show_bug.cgi?id=183494
+
+        Reviewed by Ryosuke Niwa.
+
+        Added Python implementation of PEM file perser and switched to use that
+        from external `openssl` command.
+
+        * Scripts/webkitpy/common/system/pemfile.py: Added.
+        (load):
+        (BadFormatError):
+        (Pem):
+        (Pem.__init__):
+        (Pem.get):
+        (Pem.get_all):
+        (Pem.certificate):
+        (Pem.private_key):
+        (Pem.csr):
+        (Pem.certificate_request):
+        (Pem.certificate_signing_request):
+        (_parse_pem_format):
+        (_parse_pem_format.find_begin):
+        (_parse_pem_format.find_end):
+        (_parse_pem_format.sections):
+        * Scripts/webkitpy/common/system/pemfile_unittest.py: Added.
+        (PemFileTest):
+        (PemFileTest.test_parse):
+        (PemFileTest.test_parse_bad_format):
+        * Scripts/webkitpy/port/base.py:
+        (Port.start_websocket_server):
+        (Port._extract_certificate_from_pem): Deleted.
+        (Port._extract_private_key_from_pem): Deleted.
+
 2018-03-12  Javier Fernandez  <jfernandez@igalia.com>
 
         Remove GridLayout runtime flag
diff --git a/Tools/Scripts/webkitpy/common/system/pemfile.py b/Tools/Scripts/webkitpy/common/system/pemfile.py
new file mode 100644 (file)
index 0000000..9a0aa02
--- /dev/null
@@ -0,0 +1,139 @@
+# Copyright (C) 2018 Sony Interactive Entertainment Inc.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+import re
+
+
+def load(filesystem, path):
+    """Load PEM file and return PEM object"""
+    return Pem(filesystem.read_text_file(path))
+
+
+class BadFormatError(Exception):
+    """Bad format error"""
+    pass
+
+
+class Pem(object):
+    """
+    Container for certificate related information.
+    Each section in PEM file can be accessible by get().
+    e.g.
+    pem = pemfile.load(filesystem, "/path/to/sample.pem")
+    assert pem.certificate.startswith("-----BEGIN CERTIFICATE-----")
+    """
+
+    def __init__(self, content):
+        self._contents = _parse_pem_format(content)
+        if not self._contents:
+            raise BadFormatError("Cannot find any sections in this file.")
+
+    def get(self, kind):
+        """Return requested information or None if not found."""
+        items = self.get_all(kind)
+        if not items:
+            raise KeyError("{} is not in this PEM".format(kind))
+        return items[0]
+
+    def get_all(self, kind):
+        """Return all matching requested information"""
+        return [content for (key, content) in self._contents if key == kind]
+
+    @property
+    def certificate(self):
+        """Return certificate"""
+        return self.get(CERTIFICATE)
+
+    @property
+    def private_key(self):
+        """Return private key"""
+        return self.get(RSA_PRIVATE_KEY)
+
+    @property
+    def csr(self):
+        """Return certificate request"""
+        return self.get(CERTIFICATE_REQUEST)
+
+    @property
+    def certificate_request(self):
+        """Alias for csr()"""
+        return self.csr
+
+    @property
+    def certificate_signing_request(self):
+        """Alias for csr()"""
+        return self.csr
+
+
+MARKER = "-----"
+BEGIN_MARKER = "BEGIN "
+END_MARKER = "END"
+
+CERTIFICATE_REQUEST = "CERTIFICATE REQUEST"
+RSA_PRIVATE_KEY = "RSA PRIVATE KEY"
+CERTIFICATE = "CERTIFICATE"
+
+BEGIN_PATTERN = re.compile("^{}BEGIN (.+){}$".format(MARKER, MARKER))
+
+
+def _parse_pem_format(content):
+    lines = content.split("\n")
+
+    def find_begin(lines):
+        """
+        Find first matching BEGIN marker.
+        @returns found key and rest of lines | None
+        """
+        while lines:
+            matched = BEGIN_PATTERN.match(lines[0])
+            if matched:
+                return (matched.group(1), lines)
+            lines = lines[1:]
+        return None
+
+    def find_end(kind, lines):
+        """
+        Find END marker.
+        @returns key, found contents and rest of lines.
+        @raise BadFormatError
+        """
+        end_marker = "{}END {}{}".format(MARKER, kind, MARKER)
+
+        try:
+            index = lines.index(end_marker)
+        except ValueError, e:
+            raise BadFormatError("Cannot find section end: {}".format(end_marker))
+
+        return kind, "\n".join(lines[0:index + 1]) + "\n", lines[index + 1:]
+
+    def sections(lines):
+        """Section Generator"""
+        while lines:
+            result = find_begin(lines)
+            if not result:
+                break
+            key, body, lines = find_end(*result)
+            yield (key, body)
+
+    return [section for section in sections(lines)]
diff --git a/Tools/Scripts/webkitpy/common/system/pemfile_unittest.py b/Tools/Scripts/webkitpy/common/system/pemfile_unittest.py
new file mode 100644 (file)
index 0000000..ca6c5d8
--- /dev/null
@@ -0,0 +1,164 @@
+# Copyright (C) 2018 Sony Interactive Entertainment Inc.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+import unittest
+import sys
+
+from webkitpy.common.system.pemfile import Pem, BadFormatError
+
+
+class PemFileTest(unittest.TestCase):
+    """Test Pem file parsing"""
+    def test_parse(self):
+        pem = Pem(self.pem_contents)
+
+        self.assertEqual(pem.private_key, self.private_key)
+        self.assertEqual(pem.certificate, self.certificate)
+
+        with self.assertRaises(KeyError):
+            pem.csr
+
+        with self.assertRaises(KeyError):
+            pem.get("FOO BAR")
+
+    def test_parse_bad_format(self):
+        with self.assertRaises(BadFormatError):
+            # Partial contents raises format error
+            Pem(trim("""-----BEGIN RSA PRIVATE KEY-----
+            MIICXQIBAAKBgQCmcXbusrr8zQr8snIb0OVQibVfgv7zPjh/5xdcrKOejJzp3epA
+            AF4TITeFR9vzWIwkmkcRoY+IbQNhh7kefGUYD47bvVamJMtq5cGYVs0HngT+KTMa
+            NGH/G44KkFIOaz/b5d/JNKONrlqwxqXS+m6IY4l/E1Ff25ZjND5TaEvI1wIDAQAB
+            """) + "\n")
+
+        with self.assertRaises(BadFormatError):
+            # No section content raises format error
+            Pem("""HELLO WORLD
+            HOW'S GOING????
+            """)
+
+    def test_parse_custom_content(self):
+        pem = Pem(trim("""Hi, please take this.
+        -----BEGIN FOOBAR-----
+        HELLO/WORKD===
+        -----END FOOBAR-----
+        regards,
+        kind of mail signature comes here
+        """) + "\n")
+
+        self.assertEqual(
+            trim(pem.get("FOOBAR")),
+            trim("""-----BEGIN FOOBAR-----
+                    HELLO/WORKD===
+                    -----END FOOBAR-----"""))
+
+    def setUp(self):
+        self.pem_contents = trim("""-----BEGIN RSA PRIVATE KEY-----
+        MIICXQIBAAKBgQCmcXbusrr8zQr8snIb0OVQibVfgv7zPjh/5xdcrKOejJzp3epA
+        AF4TITeFR9vzWIwkmkcRoY+IbQNhh7kefGUYD47bvVamJMtq5cGYVs0HngT+KTMa
+        NGH/G44KkFIOaz/b5d/JNKONrlqwxqXS+m6IY4l/E1Ff25ZjND5TaEvI1wIDAQAB
+        AoGBAIcDv4A9h6UOBv2ZGyspNvsv2erSblGOhXJrWO4aNNemJJspIp4sLiPCbDE3
+        a1po17XRWBkbPz1hgL6axDXQnoeo++ebfrvRSed+Fys4+6SvuSrPOv6PmWTBT/Wa
+        GpO+tv48JUNxC/Dy8ROixBXOViuIBEFq3NfVH4HU3+RG20NhAkEA1L3RAhdfPkLI
+        82luSOYE3Eq44lICb/yZi+JEihwSeZTJKdZHwYD8KVCjOtjGrOmyEyvThrcIACQz
+        JLEreVh33wJBAMhJm9pzJJNkIyBgiXA66FAwbhdDzSTPx0OBjoVWoj6u7jzGvIFT
+        Cn1aiTBYzzsiMCaCx+W3e6pK/DcvHSwKrgkCQHZMcxwBmSHLC2lnmD8LQWqqVnLr
+        fZV+VnfVw501DQT0uoP8NvygWBg1Uf9YKepfLXnBpidEQjup5ZKivnUEv+sCQA8N
+        6VcMHI2vkyxV1T7ITrnoSf4ZrIu9yl56mHnRPzSy9VlAHt8hnMI7UeB+bGUndrMO
+        VXQgzHzKUhbbxbePvfECQQDTtkOuhJyKDfHCxLDcwNpi+T6OWTEfCw/cq9ZWDbA7
+        yCX81pQxfZkfMIS1YFIOGHovK0rMMTraCe+iDNYtVz/L
+        -----END RSA PRIVATE KEY-----
+        -----BEGIN CERTIFICATE-----
+        MIIB9zCCAWACCQDjWWTeC6BQvTANBgkqhkiG9w0BAQQFADBAMQswCQYDVQQGEwJB
+        VTETMBEGA1UECBMKU29tZS1TdGF0ZTEcMBoGA1UEChMTV2ViS2l0IExheW91dCBU
+        ZXN0czAeFw0wNzA3MTMxMjUxMzJaFw03MTA1MTMwNjIzMTZaMEAxCzAJBgNVBAYT
+        AkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRwwGgYDVQQKExNXZWJLaXQgTGF5b3V0
+        IFRlc3RzMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmcXbusrr8zQr8snIb
+        0OVQibVfgv7zPjh/5xdcrKOejJzp3epAAF4TITeFR9vzWIwkmkcRoY+IbQNhh7ke
+        fGUYD47bvVamJMtq5cGYVs0HngT+KTMaNGH/G44KkFIOaz/b5d/JNKONrlqwxqXS
+        +m6IY4l/E1Ff25ZjND5TaEvI1wIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAAfbUbgD
+        01O8DoZA02c1MUMbMHRPSb/qdok2pyWoCPa/BSaOIaNPePc8auPRbrS2XsVWSMft
+        CTXiXmrK2Xx1+fJuZLAp0CUng4De4cDH5c8nvlocYmXo+1x53S9DfD0KPryjBRI7
+        9LnJq2ysHAUawiqFXlwBag6mXawD8YjzcYat
+        -----END CERTIFICATE-----
+        """) + "\n"
+
+        self.private_key = trim("""-----BEGIN RSA PRIVATE KEY-----
+        MIICXQIBAAKBgQCmcXbusrr8zQr8snIb0OVQibVfgv7zPjh/5xdcrKOejJzp3epA
+        AF4TITeFR9vzWIwkmkcRoY+IbQNhh7kefGUYD47bvVamJMtq5cGYVs0HngT+KTMa
+        NGH/G44KkFIOaz/b5d/JNKONrlqwxqXS+m6IY4l/E1Ff25ZjND5TaEvI1wIDAQAB
+        AoGBAIcDv4A9h6UOBv2ZGyspNvsv2erSblGOhXJrWO4aNNemJJspIp4sLiPCbDE3
+        a1po17XRWBkbPz1hgL6axDXQnoeo++ebfrvRSed+Fys4+6SvuSrPOv6PmWTBT/Wa
+        GpO+tv48JUNxC/Dy8ROixBXOViuIBEFq3NfVH4HU3+RG20NhAkEA1L3RAhdfPkLI
+        82luSOYE3Eq44lICb/yZi+JEihwSeZTJKdZHwYD8KVCjOtjGrOmyEyvThrcIACQz
+        JLEreVh33wJBAMhJm9pzJJNkIyBgiXA66FAwbhdDzSTPx0OBjoVWoj6u7jzGvIFT
+        Cn1aiTBYzzsiMCaCx+W3e6pK/DcvHSwKrgkCQHZMcxwBmSHLC2lnmD8LQWqqVnLr
+        fZV+VnfVw501DQT0uoP8NvygWBg1Uf9YKepfLXnBpidEQjup5ZKivnUEv+sCQA8N
+        6VcMHI2vkyxV1T7ITrnoSf4ZrIu9yl56mHnRPzSy9VlAHt8hnMI7UeB+bGUndrMO
+        VXQgzHzKUhbbxbePvfECQQDTtkOuhJyKDfHCxLDcwNpi+T6OWTEfCw/cq9ZWDbA7
+        yCX81pQxfZkfMIS1YFIOGHovK0rMMTraCe+iDNYtVz/L
+        -----END RSA PRIVATE KEY-----
+        """) + "\n"
+
+        self.certificate = trim("""-----BEGIN CERTIFICATE-----
+        MIIB9zCCAWACCQDjWWTeC6BQvTANBgkqhkiG9w0BAQQFADBAMQswCQYDVQQGEwJB
+        VTETMBEGA1UECBMKU29tZS1TdGF0ZTEcMBoGA1UEChMTV2ViS2l0IExheW91dCBU
+        ZXN0czAeFw0wNzA3MTMxMjUxMzJaFw03MTA1MTMwNjIzMTZaMEAxCzAJBgNVBAYT
+        AkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRwwGgYDVQQKExNXZWJLaXQgTGF5b3V0
+        IFRlc3RzMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmcXbusrr8zQr8snIb
+        0OVQibVfgv7zPjh/5xdcrKOejJzp3epAAF4TITeFR9vzWIwkmkcRoY+IbQNhh7ke
+        fGUYD47bvVamJMtq5cGYVs0HngT+KTMaNGH/G44KkFIOaz/b5d/JNKONrlqwxqXS
+        +m6IY4l/E1Ff25ZjND5TaEvI1wIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAAfbUbgD
+        01O8DoZA02c1MUMbMHRPSb/qdok2pyWoCPa/BSaOIaNPePc8auPRbrS2XsVWSMft
+        CTXiXmrK2Xx1+fJuZLAp0CUng4De4cDH5c8nvlocYmXo+1x53S9DfD0KPryjBRI7
+        9LnJq2ysHAUawiqFXlwBag6mXawD8YjzcYat
+        -----END CERTIFICATE-----
+        """) + "\n"
+
+
+# Formal docstring indentation handling from PEP
+# https://www.python.org/dev/peps/pep-0257/
+def trim(docstring):
+    if not docstring:
+        return ''
+    # Convert tabs to spaces (following the normal Python rules)
+    # and split into a list of lines:
+    lines = docstring.expandtabs().splitlines()
+    # Determine minimum indentation (first line doesn't count):
+    indent = sys.maxint
+    for line in lines[1:]:
+        stripped = line.lstrip()
+        if stripped:
+            indent = min(indent, len(line) - len(stripped))
+    # Remove indentation (first line is special):
+    trimmed = [lines[0].strip()]
+    if indent < sys.maxint:
+        for line in lines[1:]:
+            trimmed.append(line[indent:].rstrip())
+    # Strip off trailing and leading blank lines:
+    while trimmed and not trimmed[-1]:
+        trimmed.pop()
+    while trimmed and not trimmed[0]:
+        trimmed.pop(0)
+    # Return a single string:
+    return '\n'.join(trimmed)
index 1da06c7..f077c9a 100644 (file)
@@ -47,7 +47,7 @@ from webkitpy.common import find_files
 from webkitpy.common import read_checksum_from_png
 from webkitpy.common.memoized import memoized
 from webkitpy.common.prettypatch import PrettyPatch
-from webkitpy.common.system import path
+from webkitpy.common.system import path, pemfile
 from webkitpy.common.system.executive import ScriptError
 from webkitpy.common.version_name_map import PUBLIC_TABLE, INTERNAL_TABLE, VersionNameMap
 from webkitpy.common.wavediff import WaveDiff
@@ -991,12 +991,6 @@ class Port(object):
             return True
         return web_platform_test_server.is_wpt_server_running(self)
 
-    def _extract_certificate_from_pem(self, pem_file, destination_certificate_file):
-        return self._executive.run_command(['openssl', 'x509', '-outform', 'pem', '-in', pem_file, '-out', destination_certificate_file], return_exit_code=True) == 0
-
-    def _extract_private_key_from_pem(self, pem_file, destination_private_key_file):
-        return self._executive.run_command(['openssl', 'rsa', '-in', pem_file, '-out', destination_private_key_file], return_exit_code=True) == 0
-
     def start_websocket_server(self):
         """Start a web server. Raise an error if it can't start or is already running.
 
@@ -1007,16 +1001,20 @@ class Port(object):
         server.start()
         self._websocket_server = server
 
-        pem_file = self._filesystem.join(self.layout_tests_dir(), "http", "conf", "webkit-httpd.pem")
         websocket_server_temporary_directory = self._filesystem.mkdtemp(prefix='webkitpy-websocket-server')
+        self._websocket_server_temporary_directory = websocket_server_temporary_directory
+
+        pem_file = self._filesystem.join(self.layout_tests_dir(), "http", "conf", "webkit-httpd.pem")
+        pem = pemfile.load(self._filesystem, pem_file)
         certificate_file = self._filesystem.join(str(websocket_server_temporary_directory), 'webkit-httpd.crt')
+        self._filesystem.write_text_file(certificate_file, pem.certificate)
         private_key_file = self._filesystem.join(str(websocket_server_temporary_directory), 'webkit-httpd.key')
-        self._websocket_server_temporary_directory = websocket_server_temporary_directory
-        if self._extract_certificate_from_pem(pem_file, certificate_file) and self._extract_private_key_from_pem(pem_file, private_key_file):
-            secure_server = self._websocket_secure_server = websocket_server.PyWebSocket(self, self.results_directory(),
-                                use_tls=True, port=websocket_server.PyWebSocket.DEFAULT_WSS_PORT, private_key=private_key_file, certificate=certificate_file)
-            secure_server.start()
-            self._websocket_secure_server = secure_server
+        self._filesystem.write_text_file(private_key_file, pem.private_key)
+
+        secure_server = self._websocket_secure_server = websocket_server.PyWebSocket(self, self.results_directory(),
+            use_tls=True, port=websocket_server.PyWebSocket.DEFAULT_WSS_PORT, private_key=private_key_file, certificate=certificate_file)
+        secure_server.start()
+        self._websocket_secure_server = secure_server
 
     def start_web_platform_test_server(self, additional_dirs=None, number_of_servers=None):
         assert not self._web_platform_test_server, 'Already running a Web Platform Test server.'