From 4799053ca958d08a06b34f858fa0fc8396cc253e Mon Sep 17 00:00:00 2001 From: Sander Marechal Date: Wed, 3 Oct 2012 10:50:02 +0200 Subject: [PATCH 1/6] Allow dot in URL scheme This makes it possible to support SSH2 urls, like ssh2.scp:// See: http://www.php.net/manual/en/wrappers.ssh2.php --- src/Composer/Repository/ComposerRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 51c98dec0..83084d0ae 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -37,7 +37,7 @@ class ComposerRepository extends ArrayRepository implements NotifiableRepository public function __construct(array $repoConfig, IOInterface $io, Config $config) { - if (!preg_match('{^\w+://}', $repoConfig['url'])) { + if (!preg_match('{^[\w.]+://}', $repoConfig['url'])) { // assume http as the default protocol $repoConfig['url'] = 'http://'.$repoConfig['url']; } From 6cf860669f1556cca2d84861e1fbfae44682ca04 Mon Sep 17 00:00:00 2001 From: Sander Marechal Date: Wed, 3 Oct 2012 11:56:31 +0200 Subject: [PATCH 2/6] Add repository stream context options Add support for passing stream context options to the StreamContextFactory. This allows support for SSH keyfiles, SSL certificates and much more. Example: { "repositories": [ { "type": "composer", "url": "ssh2.sftp://host:22/path/to/packages.json", "options": { "ssh2": { "username": "composer", "pubkey_file": "/path/to/composer.key.pub", "privkey_file": "/path/to/composer.key" } } } ] } --- src/Composer/Repository/ComposerRepository.php | 8 +++++++- src/Composer/Util/RemoteFilesystem.php | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 83084d0ae..5904e6a94 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -27,6 +27,7 @@ use Composer\Util\RemoteFilesystem; class ComposerRepository extends ArrayRepository implements NotifiableRepositoryInterface, StreamableRepositoryInterface { protected $config; + protected $options; protected $url; protected $io; protected $cache; @@ -46,7 +47,12 @@ class ComposerRepository extends ArrayRepository implements NotifiableRepository throw new \UnexpectedValueException('Invalid url given for Composer repository: '.$repoConfig['url']); } + if (!isset($repoConfig['options'])) { + $repoConfig['options'] = array(); + } + $this->config = $config; + $this->options = $repoConfig['options']; $this->url = $repoConfig['url']; $this->io = $io; $this->cache = new Cache($io, $config->get('home').'/cache/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url)); @@ -199,7 +205,7 @@ class ComposerRepository extends ArrayRepository implements NotifiableRepository $jsonUrl = $this->url . '/packages.json'; } - $json = new JsonFile($jsonUrl, new RemoteFilesystem($this->io)); + $json = new JsonFile($jsonUrl, new RemoteFilesystem($this->io, $this->options)); $data = $json->read(); if (!empty($data['notify'])) { diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index e82313033..f2dfaf842 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -30,15 +30,17 @@ class RemoteFilesystem private $result; private $progress; private $lastProgress; + private $options; /** * Constructor. * * @param IOInterface $io The IO instance */ - public function __construct(IOInterface $io) + public function __construct(IOInterface $io, $options = array()) { $this->io = $io; + $this->options = $options; } /** @@ -241,6 +243,8 @@ class RemoteFilesystem $options['http']['header'] .= "Authorization: Basic $authStr\r\n"; } + $options = array_merge_recursive($options, $this->options); + return $options; } } From 748c4764ba0af5862e2b53879ec72fd525d00136 Mon Sep 17 00:00:00 2001 From: Sander Marechal Date: Wed, 3 Oct 2012 15:09:47 +0200 Subject: [PATCH 3/6] Unittest for stream options --- .../Test/Util/RemoteFilesystemTest.php | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index 4824e1af9..897053c92 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -25,7 +25,7 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue(false)) ; - $res = $this->callGetOptionsForUrl($io, array('http://example.org')); + $res = $this->callGetOptionsForUrl($io, array(), array('http://example.org')); $this->assertTrue(isset($res['http']['header']) && false !== strpos($res['http']['header'], 'User-Agent'), 'getOptions must return an array with a header containing a User-Agent'); } @@ -43,10 +43,27 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue(array('username' => 'login', 'password' => 'password'))) ; - $options = $this->callGetOptionsForUrl($io, array('http://example.org')); + $options = $this->callGetOptionsForUrl($io, array(), array('http://example.org')); $this->assertContains('Authorization: Basic', $options['http']['header']); } + public function testGetOptionsForUrlWithStreamOptions() + { + $io = $this->getMock('Composer\IO\IOInterface'); + $io + ->expects($this->once()) + ->method('hasAuthorization') + ->will($this->returnValue(true)) + ; + + $streamOptions = array('ssl' => array( + 'allow_self_signed' => true, + )); + + $res = $this->callGetOptionsForUrl($io, $streamOptions, array('https://example.org')); + $this->assertTrue(isset($res['ssl']) && isset($res['ssl']['allow_self_signed']) && true === $res['ssl']['allow_self_signed'], 'getOptions must return an array with a allow_self_signed set to true'); + } + public function testCallbackGetFileSize() { $fs = new RemoteFilesystem($this->getMock('Composer\IO\IOInterface')); @@ -102,9 +119,9 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase unlink($file); } - protected function callGetOptionsForUrl($io, array $args = array()) + protected function callGetOptionsForUrl($io, array $options = array(), array $args = array()) { - $fs = new RemoteFilesystem($io); + $fs = new RemoteFilesystem($io, $options); $ref = new \ReflectionMethod($fs, 'getOptionsForUrl'); $ref->setAccessible(true); From 11b5b5944d98add7b51f24001c6b94b3097e300f Mon Sep 17 00:00:00 2001 From: Sander Marechal Date: Wed, 3 Oct 2012 18:51:42 +0200 Subject: [PATCH 4/6] Improved argument order --- tests/Composer/Test/Util/RemoteFilesystemTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index 897053c92..4dd3aa0e7 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -25,7 +25,7 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue(false)) ; - $res = $this->callGetOptionsForUrl($io, array(), array('http://example.org')); + $res = $this->callGetOptionsForUrl($io, array('http://example.org')); $this->assertTrue(isset($res['http']['header']) && false !== strpos($res['http']['header'], 'User-Agent'), 'getOptions must return an array with a header containing a User-Agent'); } @@ -43,7 +43,7 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue(array('username' => 'login', 'password' => 'password'))) ; - $options = $this->callGetOptionsForUrl($io, array(), array('http://example.org')); + $options = $this->callGetOptionsForUrl($io, array('http://example.org')); $this->assertContains('Authorization: Basic', $options['http']['header']); } @@ -60,7 +60,7 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase 'allow_self_signed' => true, )); - $res = $this->callGetOptionsForUrl($io, $streamOptions, array('https://example.org')); + $res = $this->callGetOptionsForUrl($io, array('https://example.org'), $streamOptions); $this->assertTrue(isset($res['ssl']) && isset($res['ssl']['allow_self_signed']) && true === $res['ssl']['allow_self_signed'], 'getOptions must return an array with a allow_self_signed set to true'); } @@ -119,7 +119,7 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase unlink($file); } - protected function callGetOptionsForUrl($io, array $options = array(), array $args = array()) + protected function callGetOptionsForUrl($io, array $args = array(), array $options = array()) { $fs = new RemoteFilesystem($io, $options); $ref = new \ReflectionMethod($fs, 'getOptionsForUrl'); From 0d9e08f9afc3288ea37944b23d106a901c399d4d Mon Sep 17 00:00:00 2001 From: Sander Marechal Date: Wed, 3 Oct 2012 18:54:27 +0200 Subject: [PATCH 5/6] Use array_replace_recursive() instead of array_merge_recursive() --- src/Composer/Util/RemoteFilesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index f2dfaf842..3ccb127ac 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -243,7 +243,7 @@ class RemoteFilesystem $options['http']['header'] .= "Authorization: Basic $authStr\r\n"; } - $options = array_merge_recursive($options, $this->options); + $options = array_replace_recursive($options, $this->options); return $options; } From 3e63ebf7fe3494961423f8d37123c78a1e23cba8 Mon Sep 17 00:00:00 2001 From: Sander Marechal Date: Wed, 3 Oct 2012 20:32:00 +0200 Subject: [PATCH 6/6] Add stream context options documetation --- doc/04-schema.md | 15 ++++++- doc/05-repositories.md | 7 ++++ .../handling-private-packages-with-satis.md | 40 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/doc/04-schema.md b/doc/04-schema.md index 5ba680bd8..8f619ee3a 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -504,8 +504,10 @@ ignored. The following repository types are supported: * **composer:** A composer repository is simply a `packages.json` file served - via HTTP, that contains a list of `composer.json` objects with additional - `dist` and/or `source` information. + via the network (HTTP, FTP, SSH), that contains a list of `composer.json` + objects with additional `dist` and/or `source` information. The `packages.json` + file is loaded using a PHP stream. You can set extra options on that stream + using the `options` parameter. * **vcs:** The version control system repository can fetch packages from git, svn and hg repositories. * **pear:** With this you can import any pear repository into your composer @@ -524,6 +526,15 @@ Example: "type": "composer", "url": "http://packages.example.com" }, + { + "type": "composer", + "url": "https://packages.example.com", + "options": { + "ssl": { + "verify_peer": "true" + } + } + }, { "type": "vcs", "url": "https://github.com/Seldaek/monolog" diff --git a/doc/05-repositories.md b/doc/05-repositories.md index c0f70c1e5..a675ab47c 100644 --- a/doc/05-repositories.md +++ b/doc/05-repositories.md @@ -148,6 +148,13 @@ hash changed. This field is optional. You probably don't need it for your own custom repository. +#### stream options + +The `packages.json` file is loaded using a PHP stream. You can set extra options +on that stream using the `options` parameter. You can set any valid PHP stream +context option. See [Context options and parameters](http://nl3.php.net/manual/en/context.php) +for more information. + ### VCS VCS stands for version control system. This includes versioning systems like diff --git a/doc/articles/handling-private-packages-with-satis.md b/doc/articles/handling-private-packages-with-satis.md index 722a497ad..e7fa23509 100644 --- a/doc/articles/handling-private-packages-with-satis.md +++ b/doc/articles/handling-private-packages-with-satis.md @@ -85,3 +85,43 @@ itself. "company/package3": "dev-master" } } + +### Security + +To secure your private repository you can host it over SSH or SSL using a client +certificate. In your project you can use the `options` parameter to specify the +connection options for the server. + +Example using a custom repository using SSH (requires the SSH2 PECL extension): + + { + "repositories": [ + { + "type": "composer", + "url": "ssh2.sftp://example.org", + "options": { + "ssh2": { + "username": "composer", + "pubkey_file": "/home/composer/.ssh/id_rsa.pub", + "privkey_file": "/home/composer/.ssh/id_rsa" + } + } + } + ] + } + +Example using HTTP over SSL using a client certificate: + + { + "repositories": [ + { + "type": "composer", + "url": "https://example.org", + "options": { + "ssl": { + "cert_file": "/home/composer/.ssl/composer.pem", + } + } + } + ] + }