Merge "Add workaround for OSError raised by Popen.communicate()"
This commit is contained in:
@@ -21,6 +21,7 @@ If set_subprocess() is not called, this module will pick Python's subprocess
|
||||
or eventlet.green.subprocess based on if os module is patched by eventlet.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
@@ -57,6 +58,46 @@ def set_subprocess(_subprocess=None):
|
||||
subprocess = _subprocess
|
||||
|
||||
|
||||
def _check_files_accessible(files):
|
||||
err = None
|
||||
try:
|
||||
for try_file in files:
|
||||
with open(try_file, 'r'):
|
||||
pass
|
||||
except IOError as e:
|
||||
# Catching IOError means there is an issue with
|
||||
# the given file.
|
||||
err = ('Hit OSError in _process_communicate_handle_oserror()\n'
|
||||
'Likely due to %s: %s') % (try_file, e.strerror)
|
||||
|
||||
return err
|
||||
|
||||
|
||||
def _process_communicate_handle_oserror(process, text, files):
|
||||
"""Wrapper around process.communicate that checks for OSError."""
|
||||
|
||||
try:
|
||||
output, err = process.communicate(text)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EPIPE:
|
||||
raise
|
||||
# OSError with EPIPE only occurs with Python 2.6.x/old 2.7.x
|
||||
# http://bugs.python.org/issue10963
|
||||
|
||||
# The quick exit is typically caused by the openssl command not being
|
||||
# able to read an input file, so check ourselves if can't read a file.
|
||||
err = _check_files_accessible(files)
|
||||
if process.stderr:
|
||||
err += process.stderr.read()
|
||||
|
||||
output = ""
|
||||
retcode = -1
|
||||
else:
|
||||
retcode = process.poll()
|
||||
|
||||
return output, err, retcode
|
||||
|
||||
|
||||
def cms_verify(formatted, signing_cert_file_name, ca_file_name):
|
||||
"""Verifies the signature of the contents IAW CMS syntax.
|
||||
|
||||
@@ -73,8 +114,8 @@ def cms_verify(formatted, signing_cert_file_name, ca_file_name):
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
output, err = process.communicate(formatted)
|
||||
retcode = process.poll()
|
||||
output, err, retcode = _process_communicate_handle_oserror(
|
||||
process, formatted, (signing_cert_file_name, ca_file_name))
|
||||
|
||||
# Do not log errors, as some happen in the positive thread
|
||||
# instead, catch them in the calling code and log them there.
|
||||
@@ -184,8 +225,10 @@ def cms_sign_text(text, signing_cert_file_name, signing_key_file_name):
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
output, err = process.communicate(text)
|
||||
retcode = process.poll()
|
||||
|
||||
output, err, retcode = _process_communicate_handle_oserror(
|
||||
process, text, (signing_cert_file_name, signing_key_file_name))
|
||||
|
||||
if retcode or "Error" in err:
|
||||
LOG.error('Signing error: %s' % err)
|
||||
raise subprocess.CalledProcessError(retcode, "openssl")
|
||||
|
@@ -28,7 +28,7 @@ TESTDIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOTDIR = os.path.normpath(os.path.join(TESTDIR, '..', '..'))
|
||||
CERTDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'certs')
|
||||
CMSDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'cms')
|
||||
|
||||
KEYDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'private')
|
||||
|
||||
# @TODO(mordred) This should become a testresources resource attached to the
|
||||
# class
|
||||
@@ -51,9 +51,17 @@ with open(os.path.join(CMSDIR, 'revocation_list.json')) as f:
|
||||
REVOCATION_LIST = jsonutils.loads(f.read())
|
||||
with open(os.path.join(CMSDIR, 'revocation_list.pem')) as f:
|
||||
SIGNED_REVOCATION_LIST = jsonutils.dumps({'signed': f.read()})
|
||||
with open(os.path.join(CERTDIR, 'signing_cert.pem')) as f:
|
||||
|
||||
SIGNING_CERT_FILE = os.path.join(CERTDIR, 'signing_cert.pem')
|
||||
with open(SIGNING_CERT_FILE) as f:
|
||||
SIGNING_CERT = f.read()
|
||||
with open(os.path.join(CERTDIR, 'cacert.pem')) as f:
|
||||
|
||||
SIGNING_KEY_FILE = os.path.join(KEYDIR, 'signing_key.pem')
|
||||
with open(SIGNING_KEY_FILE) as f:
|
||||
SIGNING_KEY = f.read()
|
||||
|
||||
SIGNING_CA_FILE = os.path.join(CERTDIR, 'cacert.pem')
|
||||
with open(SIGNING_CA_FILE) as f:
|
||||
SIGNING_CA = f.read()
|
||||
|
||||
UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d"
|
||||
|
@@ -12,15 +12,100 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import mock
|
||||
|
||||
from keystoneclient.common import cms
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient.tests import client_fixtures
|
||||
from keystoneclient.tests import utils
|
||||
|
||||
|
||||
class CMSTest(utils.TestCase):
|
||||
|
||||
"""Unit tests for the keystoneclient.common.cms module."""
|
||||
|
||||
def test_cms_verify(self):
|
||||
self.assertRaises(exceptions.CertificateConfigError,
|
||||
cms.cms_verify,
|
||||
'data',
|
||||
'no_exist_cert_file',
|
||||
'no_exist_ca_file')
|
||||
|
||||
def test_token_to_cms_to_token(self):
|
||||
with open(os.path.join(client_fixtures.CMSDIR,
|
||||
'auth_token_scoped.pem')) as f:
|
||||
AUTH_TOKEN_SCOPED_CMS = f.read()
|
||||
|
||||
self.assertEqual(cms.token_to_cms(client_fixtures.SIGNED_TOKEN_SCOPED),
|
||||
AUTH_TOKEN_SCOPED_CMS)
|
||||
|
||||
tok = cms.cms_to_token(cms.token_to_cms(
|
||||
client_fixtures.SIGNED_TOKEN_SCOPED))
|
||||
self.assertEqual(tok, client_fixtures.SIGNED_TOKEN_SCOPED)
|
||||
|
||||
def test_ans1_token(self):
|
||||
self.assertTrue(cms.is_ans1_token(client_fixtures.SIGNED_TOKEN_SCOPED))
|
||||
self.assertFalse(cms.is_ans1_token('FOOBAR'))
|
||||
|
||||
def test_cms_sign_token_no_files(self):
|
||||
self.assertRaises(subprocess.CalledProcessError,
|
||||
cms.cms_sign_token,
|
||||
client_fixtures.SIGNED_TOKEN_SCOPED,
|
||||
'/no/such/file', '/no/such/key')
|
||||
|
||||
def test_cms_sign_token_success(self):
|
||||
self.assertTrue(
|
||||
cms.cms_sign_token(client_fixtures.SIGNED_TOKEN_SCOPED,
|
||||
client_fixtures.SIGNING_CERT_FILE,
|
||||
client_fixtures.SIGNING_KEY_FILE))
|
||||
|
||||
def test_cms_verify_token_no_files(self):
|
||||
self.assertRaises(exceptions.CertificateConfigError,
|
||||
cms.cms_verify,
|
||||
client_fixtures.SIGNED_TOKEN_SCOPED,
|
||||
'/no/such/file', '/no/such/key')
|
||||
|
||||
def test_cms_verify_token_no_oserror(self):
|
||||
import errno
|
||||
|
||||
def raise_OSError(*args):
|
||||
e = OSError()
|
||||
e.errno = errno.EPIPE
|
||||
raise e
|
||||
|
||||
with mock.patch('subprocess.Popen.communicate', new=raise_OSError):
|
||||
try:
|
||||
cms.cms_verify("x", '/no/such/file', '/no/such/key')
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.assertIn('/no/such/file', e.output)
|
||||
self.assertIn('Hit OSError ', e.output)
|
||||
else:
|
||||
self.fail('Expected subprocess.CalledProcessError')
|
||||
|
||||
def test_cms_verify_token_scoped(self):
|
||||
cms_content = cms.token_to_cms(client_fixtures.SIGNED_TOKEN_SCOPED)
|
||||
self.assertTrue(cms.cms_verify(cms_content,
|
||||
client_fixtures.SIGNING_CERT_FILE,
|
||||
client_fixtures.SIGNING_CA_FILE))
|
||||
|
||||
def test_cms_verify_token_scoped_expired(self):
|
||||
cms_content = cms.token_to_cms(
|
||||
client_fixtures.SIGNED_TOKEN_SCOPED_EXPIRED)
|
||||
self.assertTrue(cms.cms_verify(cms_content,
|
||||
client_fixtures.SIGNING_CERT_FILE,
|
||||
client_fixtures.SIGNING_CA_FILE))
|
||||
|
||||
def test_cms_verify_token_unscoped(self):
|
||||
cms_content = cms.token_to_cms(client_fixtures.SIGNED_TOKEN_UNSCOPED)
|
||||
self.assertTrue(cms.cms_verify(cms_content,
|
||||
client_fixtures.SIGNING_CERT_FILE,
|
||||
client_fixtures.SIGNING_CA_FILE))
|
||||
|
||||
def test_cms_verify_token_v3_scoped(self):
|
||||
cms_content = cms.token_to_cms(client_fixtures.SIGNED_v3_TOKEN_SCOPED)
|
||||
self.assertTrue(cms.cms_verify(cms_content,
|
||||
client_fixtures.SIGNING_CERT_FILE,
|
||||
client_fixtures.SIGNING_CA_FILE))
|
||||
|
Reference in New Issue
Block a user