├── .editorconfig ├── .github ├── FUNDING.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .php-cs-fixer.php ├── CHANGELOG.md ├── README.md ├── composer.json ├── docs ├── certificate.md └── index.md ├── example ├── .gitignore ├── app │ ├── Commands │ │ └── SendCommand.php │ ├── Models │ │ ├── DataObject.php │ │ ├── Message.php │ │ └── Partner.php │ ├── Repositories │ │ ├── MessageRepository.php │ │ └── PartnerRepository.php │ ├── bootstrap.php │ ├── dependencies.php │ ├── helpers.php │ ├── middleware.php │ └── routes.php ├── bin │ └── console ├── composer.json ├── config │ ├── commands.php │ ├── partners.php │ └── settings.php ├── public │ ├── .htaccess │ └── index.php ├── resources │ ├── key3.pfx │ └── phpas2.p12 └── storage │ ├── .gitignore │ ├── logs │ └── .gitignore │ └── messages │ └── .gitignore ├── phpunit.xml ├── src ├── ASN1Helper.php ├── CryptoHelper.php ├── Management.php ├── MessageInterface.php ├── MessageRepositoryInterface.php ├── MimePart.php ├── PartnerInterface.php ├── PartnerRepositoryInterface.php ├── Server.php └── Utils.php └── tests ├── Mock ├── DataObject.php ├── Message.php ├── MessageRepository.php ├── Partner.php └── PartnerRepository.php ├── TestCase.php ├── Unit ├── Asn1HelperTest.php ├── CryptoHelperTest.php ├── ManagementTest.php ├── MimePartTest.php └── ServerTest.php ├── bootstrap.php └── fixtures ├── mic-calculation ├── phpas2.raw ├── phpas2_new.raw ├── pub.pem ├── s1.txt ├── s2.txt ├── server.crt ├── server.p7b ├── server.pem ├── server.pub ├── si_signed.mdn ├── si_signed_cmp.msg ├── signed-msg.txt ├── signed1.txt ├── signed2.txt ├── test.edi ├── test.mdn └── test.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: tiamo 4 | # custom: ["https://..."] 5 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build: 14 | name: Build - PHP ${{ matrix.php }} ${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [ ubuntu-latest ] 20 | php: [ 7.3, 7.4, 8.0, 8.1 ] 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php }} 29 | 30 | - id: composer-cache 31 | run: | 32 | echo "::set-output name=dir::$(composer config cache-files-dir)" 33 | 34 | - name: Setup Composer Cache 35 | uses: actions/cache@v3 36 | with: 37 | path: ${{ steps.composer-cache.outputs.dir }} 38 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-composer- 41 | 42 | - name: Install Composer Dependencies 43 | uses: php-actions/composer@v6 44 | with: 45 | # args: --prefer-dist --ignore-platform-reqs 46 | php_version: ${{ matrix.php }} 47 | 48 | - name: Run tests 49 | run: composer test 50 | 51 | # - name: Run tests 52 | # uses: php-actions/phpunit@v3 53 | # with: 54 | # version: 9 55 | # configuration: ./phpunit.xml 56 | # php_version: ${{ matrix.php }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | Thumbs.db 6 | /phpunit.xml 7 | /.idea 8 | /.vscode 9 | .phpunit.result.cache 10 | .php_cs.cache 11 | .php-cs-fixer.cache 12 | tmp 13 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | ignoreDotFiles(false) 5 | ->ignoreVCSIgnored(true) 6 | ->in(__DIR__.DIRECTORY_SEPARATOR.'tests') 7 | ->in(__DIR__.DIRECTORY_SEPARATOR.'src'); 8 | 9 | $config = new PhpCsFixer\Config(); 10 | $config 11 | ->setRiskyAllowed(true) 12 | ->setRules([ 13 | '@PhpCsFixer' => true, 14 | '@PhpCsFixer:risky' => true, 15 | 'general_phpdoc_annotation_remove' => ['annotations' => ['expectedDeprecation']], 16 | '@Symfony' => true, 17 | 'phpdoc_no_empty_return' => false, 18 | 'array_syntax' => ['syntax' => 'short'], 19 | 'yoda_style' => false, 20 | 'binary_operator_spaces' => [ 21 | 'operators' => [ 22 | '=>' => 'align', 23 | '=' => 'align', 24 | ], 25 | ], 26 | 'concat_space' => ['spacing' => 'one'], 27 | 'not_operator_with_space' => false, 28 | ]) 29 | ->setFinder($finder); 30 | 31 | return $config; 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 2.0.1 4 | 5 | * Add return types to MimePart, so it works on PHP 8 with the newer version of 6 | psr/http-message ([#23](https://github.com/tiamo/phpas2/pull/42)) 7 | * Improved tests 8 | * Code refactored 9 | 10 | ## 2.0.0 11 | 12 | * Minimum php version is 7.1 13 | * Added support php 8.0 14 | * Fixed bugs with mdn sign 15 | * Added new message parser 16 | * Improved tests 17 | * Added v1 branch for 5.6 support 18 | * Added github actions 19 | 20 | ## 1.4.8 21 | 22 | * Fixed 3 bugs ([#28](https://github.com/tiamo/phpas2/pull/28)) 23 | * phpspeclib v3 24 | * Improved tests 25 | 26 | ## 1.4.7 27 | 28 | * Updated base64 decode on processMessage to fix problem on bigger 29 | mess… ([#23](https://github.com/tiamo/phpas2/pull/23)) 30 | 31 | ## 1.4.6 32 | 33 | * Improved usage of sample ([#19](https://github.com/tiamo/phpas2/pull/19)) 34 | * Fixed bad MIC compare ([#18](https://github.com/tiamo/phpas2/pull/18)) 35 | * Readme improved 36 | 37 | ## 1.4.5 38 | 39 | * Fixed bug when MDN sent to own host ([#16](https://github.com/tiamo/phpas2/pull/16)) 40 | * Improved tests 41 | 42 | ## 1.4.4 43 | 44 | * Fixed bug on Win OS ([#15](https://github.com/tiamo/phpas2/issues/15)) 45 | 46 | ## 1.4.3 47 | 48 | * Support guzzle: ^7.0 ([#14](https://github.com/tiamo/phpas2/issues/14)) 49 | 50 | ## 1.4.0 51 | 52 | * Bugfixes 53 | * Minimum php >= 5.6 54 | * Improved tests 55 | * Removed StorageInterface 56 | * Added PartnerRepositoryInterface 57 | * Added MessageRepositoryInterface 58 | 59 | ## 1.3.8 60 | 61 | * Fixed binary encoding bug 62 | * Global code improvements 63 | 64 | ## 1.3.7 65 | 66 | * Added header normalization 67 | * Some code refactory 68 | 69 | ## 1.3.6 70 | 71 | * psr/log@^1.1 72 | * guzzlehttp/guzzle@^5.5 73 | * Fixed exceptions 74 | * Global code refactory 75 | * Server: fixed MDN generation and error response 76 | * Fixed MIC calculation 77 | * Example improved 78 | 79 | ## 1.3.5 80 | 81 | * Fix: Compression 82 | * Fix: Mic calculation 83 | * Code refactory 84 | * Tests added 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPAS2 is a php-based implementation of the EDIINT AS2 standard 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/tiamo/phpas2.svg?style=flat-square)](https://packagist.org/packages/tiamo/phpas2) 4 | [![Build Status](https://github.com/tiamo/phpas2/actions/workflows/ci.yml/badge.svg)](https://github.com/tiamo/phpas2) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/tiamo/phpas2.svg?style=flat-square)](https://packagist.org/packages/tiamo/phpas2) 6 | [![License](https://poser.pugx.org/tiamo/phpas2/license)](https://packagist.org/packages/tiamo/phpas2) 7 | 8 | The PHPAS2 application enables you to transmit and receive AS2 messages with EDI-X12, EDIFACT, XML, or binary payloads 9 | between trading partners. 10 | 11 | ## Requirements 12 | 13 | * php >= 7.1 14 | * ext-openssl 15 | * ext-zlib 16 | 17 | ## Installation 18 | 19 | ``` 20 | composer require tiamo/phpas2 21 | ``` 22 | 23 | ## Usage 24 | 25 | * [Documentation](./docs/index.md) 26 | * [Example](./example) 27 | 28 | Basic example 29 | 30 | ```bash 31 | cd example 32 | 33 | composer install 34 | 35 | chmod +x ./bin/console 36 | 37 | # start a server to receive messages in 8000 port 38 | php -S 127.0.0.1:8000 ./public/index.php 39 | 40 | # send a test message 41 | php bin/console send-message --from mycompanyAS2 --to phpas2 42 | 43 | # send a file 44 | php bin/console send-message --from mycompanyAS2 --to phpas2 --file /path/to/the/file 45 | ``` 46 | 47 | ## Changelog 48 | 49 | Please have a look in [CHANGELOG](CHANGELOG.md) 50 | 51 | ## License 52 | 53 | Licensed under the [MIT license](http://opensource.org/licenses/MIT). 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiamo/phpas2", 3 | "description": "PHPAS2 is a php-based implementation of the EDIINT AS2 standard", 4 | "type": "library", 5 | "version": "2.0.1", 6 | "authors": [ 7 | { 8 | "name": "Vladyslav K", 9 | "email": "vk.tiamo@gmail.com" 10 | } 11 | ], 12 | "keywords": [ 13 | "edi", 14 | "ediint", 15 | "as2", 16 | "x12", 17 | "server" 18 | ], 19 | "license": "MIT", 20 | "require": { 21 | "php": "^7.1 || ^7.2 || ^7.3 || ^7.4 || ^8.0 || ^8.1", 22 | "ext-openssl": "*", 23 | "ext-zlib": "*", 24 | "ext-ctype": "*", 25 | "guzzlehttp/guzzle": "^6.5 || ^7.0", 26 | "phpseclib/phpseclib": "^3.0.8", 27 | "psr/log": "^1.1" 28 | }, 29 | "require-dev": { 30 | "friendsofphp/php-cs-fixer": "^3.1", 31 | "phpunit/phpunit": "^9.6.11", 32 | "symfony/var-dumper": "^4.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "AS2\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "AS2\\Tests\\": "tests/" 42 | } 43 | }, 44 | "minimum-stability": "dev", 45 | "prefer-stable": true, 46 | "config": { 47 | "sort-packages": true, 48 | "platform-check": false, 49 | "preferred-install": "dist" 50 | }, 51 | "scripts": { 52 | "lint": "./vendor/bin/php-cs-fixer fix -v", 53 | "test": "./vendor/bin/phpunit --no-coverage --debug", 54 | "test-cover": "./vendor/bin/phpunit --coverage-text" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docs/certificate.md: -------------------------------------------------------------------------------- 1 | Generate a couple private and public key: 2 | 3 | ```bash 4 | openssl req -newkey rsa:2048 -sha256 -nodes -keyout server.pem -x509 -days 1095 -out server.pub -subj "/C=US/ST=MyDept/L=m=MyCity/O=myCompany/OU=IT/CN=mydomain.com" 5 | ``` 6 | 7 | Extract a pkcs7 cert file: 8 | 9 | ```bash 10 | openssl crl2pkcs7 -nocrl -certfile server.pub -out server.p7b 11 | ``` 12 | 13 | Extract a smine cert file from pkcs7: 14 | 15 | ```bash 16 | openssl pkcs7 -in server.p7b -out server.crt -print_certs 17 | ``` 18 | 19 | Merge them into a p12 file: 20 | 21 | ```bash 22 | openssl pkcs12 -inkey server.p7b -in server.crt -inkey server.pem -in server.pub -export -out server.p12 -nodes -passout pass: 23 | ``` 24 | 25 | Check your p12 file: 26 | 27 | ```bash 28 | openssl pkcs12 -in server.p12 -noout -info 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # DOCUMENTATION 2 | 3 | Please have a look at an example application based on Slim3 framework. 4 | 5 | You can also create your own classes. 6 | 7 | - Implement MessageRepository class based on \AS2\MessageRepositoryInterface 8 | - Implement Message class based on \AS2\MessageInterface 9 | - Implement PartnerRepository class based on \AS2\PartnerRepositoryInterface 10 | - Implement Partner class based on \AS2\PartnerInterface 11 | 12 | ### Example Receive AS2 Message 13 | ```php 14 | $manager = new \AS2\Management(); 15 | 16 | /** @var /AS2/MessageRepositoryInterface $messageRepository */ 17 | $messageRepository = new App\Repositories\MessageRepository(); 18 | 19 | /** @var /AS2/PartnerRepositoryInterface $partnerRepository */ 20 | $partnerRepository = new App\Repositories\PartnerRepository(); 21 | 22 | $server = new \AS2\Server($manager, $partnerRepository, $messageRepository); 23 | 24 | /** @var \GuzzleHttp\Psr7\Response $response */ 25 | $response = $server->excecute(); 26 | ``` 27 | 28 | ### Example Send AS2 Message 29 | ```php 30 | 31 | $manager = new \AS2\Management(); 32 | 33 | //loading conf files 34 | $partners = require __DIR__ . '/config/partners.php'; 35 | 36 | /** @var /AS2/MessageRepositoryInterface $messageRepository */ 37 | $messageRepository = new App\Repositories\MessageRepository(['path' => $storagePath . DIRECTORY_SEPARATOR . 'sent']); 38 | 39 | /** @var /AS2/PartnerRepositoryInterface $partnerRepository */ 40 | $partnerRepository = new App\Repositories\PartnerRepository($partners); 41 | 42 | // Init partners 43 | $sender = $partnerRepository->findPartnerById('A'); 44 | $receiver = $partnerRepository->findPartnerById('B'); 45 | 46 | // Generate new message ID 47 | $messageId = \AS2\Utils::generateMessageID($sender); 48 | $rawMessage = ' 49 | Content-type: Application/EDI-X12 50 | Content-disposition: attachment; filename=payload 51 | Content-id: 52 | 53 | ISA*00~'; 54 | 55 | // Init new Message 56 | $message = $messageRepository->createMessage(); 57 | $message->setMessageId($messageId); 58 | $message->setSender($sender); 59 | $message->setReceiver($receiver); 60 | 61 | $payload = $manager->buildMessage($message, $rawMessage); 62 | if ($response = $manager->sendMessage($message, $payload)){ 63 | echo "OK \n"; 64 | } 65 | 66 | $messageRepository->saveMessage($message); 67 | 68 | ``` 69 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | Thumbs.db 6 | /phpunit.xml 7 | /.idea 8 | /.vscode 9 | .phpunit.result.cache 10 | .php_cs.cache 11 | tmp 12 | -------------------------------------------------------------------------------- /example/app/Commands/SendCommand.php: -------------------------------------------------------------------------------- 1 | container = $container; 26 | parent::__construct('send-message'); 27 | } 28 | 29 | protected function configure() 30 | { 31 | $this 32 | ->setDescription('Send message to the partner') 33 | ->setHelp('This command allows you to send a message to the partner...') 34 | ->addOption('file', null, InputOption::VALUE_OPTIONAL, 'File to send') 35 | ->addOption('from', null, InputOption::VALUE_REQUIRED, 'Sender partner as2id') 36 | ->addOption('to', null, InputOption::VALUE_REQUIRED, 'Receiver partner as2id'); 37 | } 38 | 39 | protected function execute(InputInterface $input, OutputInterface $output) 40 | { 41 | $file = $input->getOption('file'); 42 | 43 | if (! empty($file)) { 44 | if (! file_exists($file)) { 45 | throw new \RuntimeException( 46 | sprintf('File `%s` not found, please enter the correct file path.', $file) 47 | ); 48 | } 49 | } else { 50 | // Default test message 51 | 52 | $rawMessage = << 56 | 57 | ISA*00~ 58 | MSG; 59 | } 60 | 61 | /** @var PartnerRepository $partnerRepository */ 62 | $partnerRepository = $this->container->get('PartnerRepository'); 63 | 64 | $sender = $partnerRepository->findPartnerById($input->getOption('from')); 65 | $receiver = $partnerRepository->findPartnerById($input->getOption('to')); 66 | 67 | // Initialize New Message 68 | $messageId = Utils::generateMessageID($sender); 69 | 70 | // $output->writeln('Initialize new message with id: ' . $messageId); 71 | 72 | /** @var MessageRepository $messageRepository */ 73 | $messageRepository = $this->container->get('MessageRepository'); 74 | $message = $messageRepository->createMessage(); 75 | $message->setMessageId($messageId); 76 | $message->setSender($sender); 77 | $message->setReceiver($receiver); 78 | 79 | /** @var Management $manager */ 80 | $manager = $this->container->get('manager'); 81 | 82 | // Generate Message Payload 83 | if (isset($rawMessage)) { 84 | $payload = $manager->buildMessage($message, $rawMessage); 85 | } else { 86 | $payload = $manager->buildMessageFromFile($message, $file); 87 | } 88 | 89 | // $output->writeln('The message was built successfully...'); 90 | 91 | // Try to send a message 92 | $manager->sendMessage($message, $payload); 93 | 94 | // $output->writeln('Status: ' . $message->getStatus()); 95 | // $output->writeln('Status Message: ' . $message->getStatusMsg()); 96 | 97 | $messageRepository->saveMessage($message); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /example/app/Models/DataObject.php: -------------------------------------------------------------------------------- 1 | _data = $data; 31 | } 32 | 33 | /** 34 | * Set/Get attribute wrapper. 35 | * 36 | * @param string $method 37 | * @param array $args 38 | * 39 | * @return mixed 40 | * 41 | * @throws \Exception 42 | */ 43 | public function __call($method, $args) 44 | { 45 | switch (substr($method, 0, 3)) { 46 | case 'get': 47 | $key = $this->_underscore(substr($method, 3)); 48 | $index = $args[0] ?: null; 49 | 50 | return $this->getData($key, $index); 51 | 52 | case 'set': 53 | $key = $this->_underscore(substr($method, 3)); 54 | $value = $args[0] ?: null; 55 | 56 | return $this->setData($key, $value); 57 | 58 | case 'uns': 59 | $key = $this->_underscore(substr($method, 3)); 60 | 61 | return $this->unsetData($key); 62 | 63 | case 'has': 64 | $key = $this->_underscore(substr($method, 3)); 65 | 66 | return isset($this->_data[$key]); 67 | } 68 | 69 | throw new \Exception(sprintf('Invalid method %s::%s', static::class, $method)); 70 | } 71 | 72 | /** 73 | * Add data to the object. 74 | * 75 | * Retains previous data in the object. 76 | * 77 | * @return $this 78 | */ 79 | public function addData(array $arr) 80 | { 81 | foreach ($arr as $index => $value) { 82 | $this->setData($index, $value); 83 | } 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Unset data from the object. 90 | * 91 | * @param null|array|string $key 92 | * 93 | * @return $this 94 | */ 95 | public function unsetData($key = null) 96 | { 97 | if (null === $key) { 98 | $this->setData([]); 99 | } elseif (\is_string($key)) { 100 | if (isset($this->_data[$key]) || \array_key_exists($key, $this->_data)) { 101 | unset($this->_data[$key]); 102 | } 103 | } elseif ($key === (array) $key) { 104 | foreach ($key as $element) { 105 | $this->unsetData($element); 106 | } 107 | } 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Object data getter. 114 | * 115 | * If $key is not defined will return all the data as an array. 116 | * Otherwise it will return value of the element specified by $key. 117 | * It is possible to use keys like a/b/c for access nested array data 118 | * 119 | * If $index is specified it will assume that attribute data is an array 120 | * and retrieve corresponding member. If data is the string - it will be explode 121 | * by new line character and converted to array. 122 | * 123 | * @param string $key 124 | * @param int|string $index 125 | * 126 | * @return mixed 127 | */ 128 | public function getData($key = '', $index = null) 129 | { 130 | if ('' === $key) { 131 | return $this->_data; 132 | } 133 | // process a/b/c key as ['a']['b']['c'] 134 | if (strpos($key, '/')) { 135 | $data = $this->getDataByPath($key); 136 | } else { 137 | $data = $this->_getData($key); 138 | } 139 | if (null !== $index) { 140 | if ($data === (array) $data) { 141 | $data = isset($data[$index]) ? $data[$index] : null; 142 | } elseif (is_string($data)) { 143 | $data = explode(PHP_EOL, $data); 144 | $data = isset($data[$index]) ? $data[$index] : null; 145 | } elseif ($data instanceof static) { 146 | $data = $data->getData($index); 147 | } else { 148 | $data = null; 149 | } 150 | } 151 | 152 | return $data; 153 | } 154 | 155 | /** 156 | * Overwrite data in the object. 157 | * 158 | * The $key parameter can be string or array. 159 | * If $key is string, the attribute value will be overwritten by $value 160 | * 161 | * If $key is an array, it will overwrite all the data in the object. 162 | * 163 | * @param array|string $key 164 | * @param mixed $value 165 | * 166 | * @return $this 167 | */ 168 | public function setData($key, $value = null) 169 | { 170 | if ($key === (array) $key) { 171 | $this->_data = $key; 172 | } else { 173 | $this->_data[$key] = $value; 174 | } 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * Get object data by path. 181 | * 182 | * Method consider the path as chain of keys: a/b/c => ['a']['b']['c'] 183 | * 184 | * @param string $path 185 | * 186 | * @return mixed 187 | */ 188 | public function getDataByPath($path) 189 | { 190 | $keys = explode('/', $path); 191 | $data = $this->_data; 192 | foreach ($keys as $key) { 193 | if ((array) $data === $data && isset($data[$key])) { 194 | $data = $data[$key]; 195 | } elseif ($data instanceof static) { 196 | $data = $data->getDataByKey($key); 197 | } else { 198 | return null; 199 | } 200 | } 201 | 202 | return $data; 203 | } 204 | 205 | /** 206 | * Get object data by particular key. 207 | * 208 | * @param string $key 209 | * 210 | * @return mixed 211 | */ 212 | public function getDataByKey($key) 213 | { 214 | return $this->_getData($key); 215 | } 216 | 217 | /** 218 | * Get object data by key with calling getter method. 219 | * 220 | * @param string $key 221 | * @param mixed $args 222 | * 223 | * @return mixed 224 | */ 225 | public function getDataUsingMethod($key, $args = null) 226 | { 227 | $method = 'get'.str_replace(' ', '', ucwords(str_replace('_', ' ', $key))); 228 | 229 | return $this->{$method}($args); 230 | } 231 | 232 | /** 233 | * If $key is empty, checks whether there's any data in the object 234 | * Otherwise checks if the specified attribute is set. 235 | * 236 | * @param string $key 237 | * 238 | * @return bool 239 | */ 240 | public function hasData($key = '') 241 | { 242 | if (empty($key) || ! \is_string($key)) { 243 | return ! empty($this->_data); 244 | } 245 | 246 | return \array_key_exists($key, $this->_data); 247 | } 248 | 249 | /** 250 | * Implementation of \ArrayAccess::offsetSet(). 251 | * 252 | * @param string $offset 253 | * @param mixed $value 254 | * 255 | * @see http://www.php.net/manual/en/arrayaccess.offsetset.php 256 | */ 257 | public function offsetSet($offset, $value): void 258 | { 259 | $this->_data[$offset] = $value; 260 | } 261 | 262 | /** 263 | * Implementation of \ArrayAccess::offsetExists(). 264 | * 265 | * @param string $offset 266 | * 267 | * @return bool 268 | * 269 | * @see http://www.php.net/manual/en/arrayaccess.offsetexists.php 270 | */ 271 | public function offsetExists($offset) 272 | { 273 | return isset($this->_data[$offset]) || \array_key_exists($offset, $this->_data); 274 | } 275 | 276 | /** 277 | * Implementation of \ArrayAccess::offsetUnset(). 278 | * 279 | * @param string $offset 280 | * 281 | * @see http://www.php.net/manual/en/arrayaccess.offsetunset.php 282 | */ 283 | public function offsetUnset($offset): void 284 | { 285 | unset($this->_data[$offset]); 286 | } 287 | 288 | /** 289 | * Implementation of \ArrayAccess::offsetGet(). 290 | * 291 | * @param string $offset 292 | * 293 | * @return mixed 294 | * 295 | * @see http://www.php.net/manual/en/arrayaccess.offsetget.php 296 | */ 297 | public function offsetGet($offset) 298 | { 299 | if (isset($this->_data[$offset])) { 300 | return $this->_data[$offset]; 301 | } 302 | 303 | return null; 304 | } 305 | 306 | /** 307 | * Get value from _data array without parse key. 308 | * 309 | * @param string $key 310 | * 311 | * @return mixed 312 | */ 313 | protected function _getData($key) 314 | { 315 | if (isset($this->_data[$key])) { 316 | return $this->_data[$key]; 317 | } 318 | 319 | return null; 320 | } 321 | 322 | /** 323 | * Converts field names for setters and getters. 324 | * 325 | * $this->setMyField($value) === $this->setData('my_field', $value) 326 | * Uses cache to eliminate unnecessary preg_replace 327 | * 328 | * @param string $name 329 | * 330 | * @return string 331 | */ 332 | protected function _underscore($name) 333 | { 334 | if (isset(self::$_underscoreCache[$name])) { 335 | return self::$_underscoreCache[$name]; 336 | } 337 | $result = strtolower(trim(preg_replace('/([A-Z]|[0-9]+)/', '_$1', $name), '_')); 338 | self::$_underscoreCache[$name] = $result; 339 | 340 | return $result; 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /example/app/Models/Message.php: -------------------------------------------------------------------------------- 1 | getData('id'); 19 | } 20 | 21 | /** 22 | * @param string $id 23 | * 24 | * @return $this 25 | */ 26 | public function setMessageId($id) 27 | { 28 | return $this->setData('id', $id); 29 | } 30 | 31 | /** 32 | * @param string $dir 33 | * 34 | * @return $this 35 | */ 36 | public function setDirection($dir) 37 | { 38 | return $this->setData('direction', $dir); 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getDirection() 45 | { 46 | return $this->getData('direction'); 47 | } 48 | 49 | /** 50 | * @param string $id 51 | * 52 | * @return $this 53 | */ 54 | public function setSenderId($id) 55 | { 56 | return $this->setData('sender_id', $id); 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getSenderId() 63 | { 64 | return $this->getData('sender_id'); 65 | } 66 | 67 | /** 68 | * @return PartnerInterface 69 | */ 70 | public function getSender() 71 | { 72 | return $this->getData('sender'); 73 | } 74 | 75 | /** 76 | * @return $this 77 | */ 78 | public function setSender(PartnerInterface $partner) 79 | { 80 | $this->setSenderId($partner->getAs2Id()); 81 | 82 | return $this->setData('sender', $partner); 83 | } 84 | 85 | /** 86 | * @param string $id 87 | * 88 | * @return $this 89 | */ 90 | public function setReceiverId($id) 91 | { 92 | return $this->setData('receiver_id', $id); 93 | } 94 | 95 | /** 96 | * @return string 97 | */ 98 | public function getReceiverId() 99 | { 100 | return $this->getData('receiver_id'); 101 | } 102 | 103 | /** 104 | * @return PartnerInterface 105 | */ 106 | public function getReceiver() 107 | { 108 | return $this->getData('receiver'); 109 | } 110 | 111 | /** 112 | * @return $this 113 | */ 114 | public function setReceiver(PartnerInterface $partner) 115 | { 116 | $this->setReceiverId($partner->getAs2Id()); 117 | 118 | return $this->setData('receiver', $partner); 119 | } 120 | 121 | /** 122 | * @return string 123 | */ 124 | public function getHeaders() 125 | { 126 | return $this->getData('headers'); 127 | } 128 | 129 | /** 130 | * @param string $headers 131 | * 132 | * @return $this 133 | */ 134 | public function setHeaders($headers) 135 | { 136 | return $this->setData('headers', $headers); 137 | } 138 | 139 | /** 140 | * @return string 141 | */ 142 | public function getPayload() 143 | { 144 | return $this->getData('payload'); 145 | } 146 | 147 | /** 148 | * @param string $payload 149 | * 150 | * @return $this 151 | */ 152 | public function setPayload($payload) 153 | { 154 | return $this->setData('payload', $payload); 155 | } 156 | 157 | /** 158 | * @return string 159 | */ 160 | public function getStatus() 161 | { 162 | return $this->getData('status'); 163 | } 164 | 165 | /** 166 | * @param string $status 167 | * 168 | * @return $this 169 | */ 170 | public function setStatus($status) 171 | { 172 | return $this->setData('status', $status); 173 | } 174 | 175 | /** 176 | * @return string 177 | */ 178 | public function getStatusMsg() 179 | { 180 | return $this->getData('status_msg'); 181 | } 182 | 183 | /** 184 | * @param string $msg 185 | * 186 | * @return $this 187 | */ 188 | public function setStatusMsg($msg) 189 | { 190 | return $this->setData('status_msg', $msg); 191 | } 192 | 193 | /** 194 | * @return string 195 | */ 196 | public function getMdnMode() 197 | { 198 | return $this->getData('mdn_mode'); 199 | } 200 | 201 | /** 202 | * @param string $status 203 | * 204 | * @return $this 205 | */ 206 | public function setMdnMode($status) 207 | { 208 | return $this->setData('mdn_mode', $status); 209 | } 210 | 211 | /** 212 | * @return string 213 | */ 214 | public function getMdnStatus() 215 | { 216 | return $this->getData('mdn_status'); 217 | } 218 | 219 | /** 220 | * @param string $status 221 | * 222 | * @return $this 223 | */ 224 | public function setMdnStatus($status) 225 | { 226 | return $this->setData('mdn_status', $status); 227 | } 228 | 229 | /** 230 | * @return string 231 | */ 232 | public function getMdnPayload() 233 | { 234 | return $this->getData('mdn'); 235 | } 236 | 237 | /** 238 | * @param mixed $mdn 239 | * 240 | * @return $this 241 | */ 242 | public function setMdnPayload($mdn) 243 | { 244 | return $this->setData('mdn', $mdn); 245 | } 246 | 247 | /** 248 | * @return string 249 | */ 250 | public function getMic() 251 | { 252 | return $this->getData('mic'); 253 | } 254 | 255 | /** 256 | * @param string $mic 257 | * 258 | * @return $this 259 | */ 260 | public function setMic($mic) 261 | { 262 | return $this->setData('mic', $mic); 263 | } 264 | 265 | /** 266 | * @return bool 267 | */ 268 | public function getSigned() 269 | { 270 | return $this->getData('signed'); 271 | } 272 | 273 | /** 274 | * @param bool $val 275 | * 276 | * @return $this 277 | */ 278 | public function setSigned($val = true) 279 | { 280 | return $this->setData('signed', $val); 281 | } 282 | 283 | /** 284 | * @return bool 285 | */ 286 | public function getEncrypted() 287 | { 288 | return $this->getData('encrypted'); 289 | } 290 | 291 | /** 292 | * @param bool $val 293 | * 294 | * @return $this 295 | */ 296 | public function setEncrypted($val = true) 297 | { 298 | return $this->setData('encrypted', $val); 299 | } 300 | 301 | /** 302 | * @return bool 303 | */ 304 | public function getCompressed() 305 | { 306 | return $this->getData('compressed'); 307 | } 308 | 309 | /** 310 | * @param bool $val 311 | * 312 | * @return $this 313 | */ 314 | public function setCompressed($val = true) 315 | { 316 | return $this->setData('compressed', $val); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /example/app/Models/Partner.php: -------------------------------------------------------------------------------- 1 | getData('id'); 18 | } 19 | 20 | /** 21 | * @return string 22 | */ 23 | public function getEmail() 24 | { 25 | return $this->getData('email'); 26 | } 27 | 28 | /** 29 | * @return string 30 | */ 31 | public function getTargetUrl() 32 | { 33 | return $this->getData('target_url'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getContentType() 40 | { 41 | return $this->getData('content_type'); 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getContentTransferEncoding() 48 | { 49 | return $this->getData('content_transfer_encoding'); 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getSubject() 56 | { 57 | return $this->getData('subject'); 58 | } 59 | 60 | /** 61 | * @return null|string 62 | */ 63 | public function getAuthMethod() 64 | { 65 | return $this->getData('auth'); 66 | } 67 | 68 | /** 69 | * @return string 70 | */ 71 | public function getAuthUser() 72 | { 73 | return $this->getData('auth_user'); 74 | } 75 | 76 | /** 77 | * @return string 78 | */ 79 | public function getAuthPassword() 80 | { 81 | return $this->getData('auth_password'); 82 | } 83 | 84 | /** 85 | * @return null|string 86 | */ 87 | public function getSignatureAlgorithm() 88 | { 89 | return $this->getData('signature_algorithm'); 90 | } 91 | 92 | /** 93 | * @return null|string 94 | */ 95 | public function getEncryptionAlgorithm() 96 | { 97 | return $this->getData('encryption_algorithm'); 98 | } 99 | 100 | /** 101 | * @return string 102 | */ 103 | public function getCertificate() 104 | { 105 | return $this->getData('certificate'); 106 | } 107 | 108 | /** 109 | * @return string 110 | */ 111 | public function getPrivateKey() 112 | { 113 | return $this->getData('private_key'); 114 | } 115 | 116 | /** 117 | * @return string 118 | */ 119 | public function getPrivateKeyPassPhrase() 120 | { 121 | // TODO: Implement getPrivateKeyPassPhrase() method. 122 | return $this->getData('private_key_pass_phrase'); 123 | } 124 | 125 | /** 126 | * @return string [null, zlib, deflate] 127 | */ 128 | public function getCompressionType() 129 | { 130 | return $this->getData('compression'); 131 | } 132 | 133 | /** 134 | * @return string [null, sync, async] 135 | */ 136 | public function getMdnMode() 137 | { 138 | return $this->getData('mdn_mode'); 139 | } 140 | 141 | /** 142 | * @return string (Example: signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, SHA256) 143 | */ 144 | public function getMdnOptions() 145 | { 146 | return $this->getData('mdn_options'); 147 | } 148 | 149 | /** 150 | * @return string (Example: Your requested MDN response from $receiver.as2_id$) 151 | */ 152 | public function getMdnSubject() 153 | { 154 | return $this->getData('mdn_subject'); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /example/app/Repositories/MessageRepository.php: -------------------------------------------------------------------------------- 1 | path = $options['path']; 21 | } 22 | 23 | /** 24 | * @param string $id 25 | * 26 | * @return Message 27 | */ 28 | public function findMessageById($id) 29 | { 30 | $path = sprintf('%s/%s.json', $this->path, $id); 31 | if (! file_exists($path)) { 32 | return null; 33 | } 34 | 35 | $data = file_get_contents($path); 36 | if (empty($data)) { 37 | return null; 38 | } 39 | 40 | return new Message(json_decode($data, true)); 41 | } 42 | 43 | public function createMessage($data = []) 44 | { 45 | return new Message($data); 46 | } 47 | 48 | /** 49 | * @param Message|MessageInterface $message 50 | * 51 | * @return bool 52 | */ 53 | public function saveMessage(MessageInterface $message) 54 | { 55 | $data = $message->getData(); 56 | unset($data['receiver'], $data['receiver']); 57 | 58 | $path = sprintf('%s/%s', $this->path, $message->getMessageId()); 59 | 60 | // if ($headers = $message->getHeaders()) { 61 | // file_put_contents($path.'.headers', $headers); 62 | // } 63 | // 64 | // if ($payload = $message->getPayload()) { 65 | // file_put_contents($path.'.payload', $payload); 66 | // } 67 | // 68 | // if ($mdn = $message->getMdnPayload()) { 69 | // file_put_contents($path.'.mdn', $mdn); 70 | // } 71 | 72 | return (bool) file_put_contents($path.'.json', json_encode($data)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /example/app/Repositories/PartnerRepository.php: -------------------------------------------------------------------------------- 1 | partners = $partners; 19 | } 20 | 21 | /** 22 | * @param string $id 23 | * 24 | * @return Partner 25 | */ 26 | public function findPartnerById($id) 27 | { 28 | foreach ($this->partners as $partner) { 29 | if ($id === $partner['id']) { 30 | return new Partner($partner); 31 | } 32 | } 33 | 34 | throw new \RuntimeException(sprintf('Unknown partner `%s`.', $id)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/app/bootstrap.php: -------------------------------------------------------------------------------- 1 | require __DIR__.'/../config/settings.php', 9 | ]); 10 | 11 | $container = $app->getContainer(); 12 | 13 | // Set up dependencies 14 | $dependencies = require __DIR__.'/dependencies.php'; 15 | $dependencies($container); 16 | 17 | // Register middleware 18 | $middleware = require __DIR__.'/middleware.php'; 19 | $middleware($app); 20 | 21 | // Register routes 22 | $routes = require __DIR__.'/routes.php'; 23 | $routes($app); 24 | -------------------------------------------------------------------------------- /example/app/dependencies.php: -------------------------------------------------------------------------------- 1 | $c['settings']['storage']['path'].'/messages', 13 | ]); 14 | }; 15 | 16 | $container['PartnerRepository'] = function ($c) { 17 | return new PartnerRepository( 18 | require __DIR__.'/../config/partners.php' 19 | ); 20 | }; 21 | 22 | $container['logger'] = function ($c) { 23 | $logger = new Logger('app'); 24 | if (! empty($c['settings']['logHandlers'])) { 25 | foreach ($c['settings']['logHandlers'] as $handler) { 26 | $logger->pushHandler($handler); 27 | } 28 | } 29 | 30 | return $logger; 31 | }; 32 | 33 | $container['manager'] = function ($c) { 34 | $manager = new Management($c['settings']['management']); 35 | $manager->setLogger($c['logger']); 36 | 37 | return $manager; 38 | }; 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /example/app/helpers.php: -------------------------------------------------------------------------------- 1 | = 5.3. 18 | */ 19 | if (! function_exists('getallheaders')) { 20 | /** 21 | * Get all HTTP header key/values as an associative array for the current request. 22 | * 23 | * @return array [string] The HTTP header key/value pairs. 24 | */ 25 | function getallheaders() 26 | { 27 | $headers = []; 28 | $copy_server = [ 29 | 'CONTENT_TYPE' => 'Content-Type', 30 | 'CONTENT_LENGTH' => 'Content-Length', 31 | 'CONTENT_MD5' => 'Content-Md5', 32 | ]; 33 | foreach ($_SERVER as $key => $value) { 34 | if (strpos($key, 'HTTP_') === 0) { 35 | $key = substr($key, 5); 36 | if (! isset($copy_server[$key], $_SERVER[$key])) { 37 | $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key)))); 38 | $headers[$key] = $value; 39 | } 40 | } elseif (isset($copy_server[$key])) { 41 | $headers[$copy_server[$key]] = $value; 42 | } 43 | } 44 | if (! isset($headers['Authorization'])) { 45 | if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { 46 | $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; 47 | } elseif (isset($_SERVER['PHP_AUTH_USER'])) { 48 | $basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; 49 | $headers['Authorization'] = 'Basic '.base64_encode($_SERVER['PHP_AUTH_USER'].':'.$basic_pass); 50 | } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) { 51 | $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST']; 52 | } 53 | } 54 | 55 | return $headers; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/app/middleware.php: -------------------------------------------------------------------------------- 1 | add(...); 7 | }; 8 | -------------------------------------------------------------------------------- /example/app/routes.php: -------------------------------------------------------------------------------- 1 | any('/', function (Request $request, Response $response, array $args) { 13 | $server = new Server( 14 | $this->get('manager'), 15 | $this->get('PartnerRepository'), 16 | $this->get('MessageRepository') 17 | ); 18 | 19 | // $message = file_get_contents(__DIR__ . '/tmp/phpas2_aXFQKQ'); 20 | // $payload = \AS2\Utils::parseMessage($message); 21 | // $serverRequest = new ServerRequest( 22 | // 'POST', 23 | // 'http:://localhost', 24 | // $payload['headers'], 25 | // $payload['body'], 26 | // '1.1', 27 | // [ 28 | // 'REMOTE_ADDR' => '127.0.0.1' 29 | // ] 30 | // ); 31 | // return $server->execute($serverRequest); 32 | 33 | return $server->execute(); 34 | 35 | // foreach($result->getHeaders() as $name => $values) { 36 | // foreach ($values as $value) { 37 | // @header(sprintf('%s: %s', $name, $value), false); 38 | // } 39 | // } 40 | // 41 | // return $response->withBody($response->getBody()); 42 | }); 43 | 44 | }; 45 | -------------------------------------------------------------------------------- /example/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new $command($app->getContainer())); 14 | } 15 | 16 | $console->run(); 17 | -------------------------------------------------------------------------------- /example/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiamo/phpas2-example", 3 | "description": "phpas2 example project", 4 | "type": "project", 5 | "keywords": [], 6 | "license": "MIT", 7 | "require": { 8 | "php": ">=7.1", 9 | "ext-json": "*", 10 | "tiamo/phpas2": "^2.0", 11 | "slim/slim": "^3", 12 | "monolog/monolog": "^1.1", 13 | "symfony/console": "^2" 14 | }, 15 | "require-dev": { 16 | "symfony/var-dumper": "^4.0" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "App\\": "app/", 21 | "AS2\\": "../src" 22 | }, 23 | "files": [ 24 | "app/helpers.php" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/config/commands.php: -------------------------------------------------------------------------------- 1 | file_get_contents($resources.'/phpas2.crt'), 12 | // 'pkey' => file_get_contents($resources.'/phpas2.pem'), 13 | // ]; 14 | 15 | // mendelson key3 16 | openssl_pkcs12_read(file_get_contents($resources.'/key3.pfx'), $key3, 'test'); 17 | 18 | $local = [ 19 | 'cert' => file_get_contents($resources.'/phpas2.crt'), 20 | 'pkey' => file_get_contents($resources.'/phpas2.pem'), 21 | ]; 22 | 23 | return [ 24 | [ 25 | 'id' => '3770002306000', 26 | 'email' => 'support@oscss-shop.fr', 27 | 'target_url' => 'http://as2.pulpedevie.com/', 28 | 'certificate' => '-----BEGIN CERTIFICATE----- 29 | MIID0TCCArmgAwIBAgIUbuBEbAxOhiVb7TLCw8gwOfv3Q3AwDQYJKoZIhvcNAQEF 30 | BQAweDELMAkGA1UEBhMCRlIxGTAXBgNVBAgMEEJvdWNoZXMtZHUtUmhvbmUxEjAQ 31 | BgNVBAcMCU1BUlNFSUxMRTEVMBMGA1UECgwMQklPIFBST1ZFTkNFMQswCQYDVQQL 32 | DAJJVDEWMBQGA1UEAwwNcHVscGVkZXZpZS5mcjAeFw0yMDA2MTcxMzM1NDJaFw0y 33 | MzA2MTcxMzM1NDJaMHgxCzAJBgNVBAYTAkZSMRkwFwYDVQQIDBBCb3VjaGVzLWR1 34 | LVJob25lMRIwEAYDVQQHDAlNQVJTRUlMTEUxFTATBgNVBAoMDEJJTyBQUk9WRU5D 35 | RTELMAkGA1UECwwCSVQxFjAUBgNVBAMMDXB1bHBlZGV2aWUuZnIwggEiMA0GCSqG 36 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKFUha+jIa+AaP8h587hg4+CWEDs+SuzCP 37 | UUJ6TWZUs3RWm11VsTuWbptl2wFjaWfboLnm7hCRS40ymfNGxoawVd9QHHsIMe17 38 | 9BBr2uZ+OLLzjneqM9sFJdrBPzMLR7k+Nd+HounM5KmnVbSmZKMGwkZYRGVWF35E 39 | zluzbt049ZxXgF+9AfpQCrXRfd+PG+f9lOq/vTWHju6WiZM33k9XeA2t4DcoYX3u 40 | IYumG7l6d0MrP1025JR32gshqpiqBbLSfPzM6IyUN6LYv0HSOKPcyPSWCxZaP5jT 41 | g3pKP4t+eezw68r1nhqx2GNKS79AB0Syj6E/XU05X2xTlU+peqOfAgMBAAGjUzBR 42 | MB0GA1UdDgQWBBS+yt/ro23XracWpRgPve1ufWVibTAfBgNVHSMEGDAWgBS+yt/r 43 | o23XracWpRgPve1ufWVibTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBBQUA 44 | A4IBAQDGS6td70R5L/OKhNLJWRbRtO3QMYmBpRCyxQ3YAt5iGmkIuM9DvXGwk11U 45 | AGSckKJRw3IH6t8emWfE09d9COdF3umKVh3+eVUsjdImqx/5/mWZ21L4Doe2eXLB 46 | ORJwa2GBDByw68skQk/OUhGe9DmIPJRO9BmJJL4lcobSunkcB1gURIyoWr9l/onl 47 | 0f8gMQmKksaHKpkDXN2XDZtv1rgzO36xNSKeYwMBMcSjEbMPGg8Rvq0q6N2AwBtA 48 | BTANITTXt0gwKWTZrDumf1X/OIZpOPiLPmoDOUJbAioJ+mCQ0GH48Ckm13bGqVpX 49 | Dyq2lN+c7kS0bY1K3XjmlZ7gHix1 50 | -----END CERTIFICATE----- 51 | ', 52 | 'private_key' => '-----BEGIN PRIVATE KEY----- 53 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDKFUha+jIa+AaP 54 | 8h587hg4+CWEDs+SuzCPUUJ6TWZUs3RWm11VsTuWbptl2wFjaWfboLnm7hCRS40y 55 | mfNGxoawVd9QHHsIMe179BBr2uZ+OLLzjneqM9sFJdrBPzMLR7k+Nd+HounM5Kmn 56 | VbSmZKMGwkZYRGVWF35Ezluzbt049ZxXgF+9AfpQCrXRfd+PG+f9lOq/vTWHju6W 57 | iZM33k9XeA2t4DcoYX3uIYumG7l6d0MrP1025JR32gshqpiqBbLSfPzM6IyUN6LY 58 | v0HSOKPcyPSWCxZaP5jTg3pKP4t+eezw68r1nhqx2GNKS79AB0Syj6E/XU05X2xT 59 | lU+peqOfAgMBAAECggEAZghs6hKdreRBW/jB0A5fiJQyTQU1ZT7Ce/ppeFsQKgAZ 60 | 44i6jYPZNFFQgRMdFlaoK8pxUtos30+oUT5OCRQ/+VTCVi6rKC4dXJKUoAB8lIqI 61 | QFVUsklQcr70PtJsMWvbaj/FRzTIm71ws56ggcsaTVVWM0cFa3ydMpyGzIhThmgF 62 | 8osW/KBADnzwpIvLjjs8SPlpep6YPEWT5rzhWRIGbOzawfl2pBnBNUJ/KRjogCwp 63 | Hfv8ki0h7dr7dmAiTjPAtcbbLRFToRW7OnESeNr3NuV31CliwGALqezlmj7Fj4ke 64 | sjSGEo9DjntJ+Nm0pmqOxZTSkbu+inW5IUWq4seT8QKBgQD6bhrCy12U/cFL6yxV 65 | MnFGLVWOYzkqk1i5PwhFipBio4r0XC/TevQdE/j2Gd0kPxdsjFZDoSkta/+dSX0A 66 | l6zsLNKBj8bwTGLsAPuG1Pty7MomNObknLUVG4Hwp6l3Bz5AidFFEWIhHshYAHPU 67 | nLMRjYJSfDZLCTeboFwviq6xewKBgQDOk+adXSd2Pj3YvIXOrMOOFXXaX3SAVy2e 68 | mhRDzjxV9T453iaZE/ak7OISF7+BtlE8xaVyqVK2MsK6081orOfY6KrMiMZQjYK0 69 | hUKtzovHGoJ9qPEkV7kOjswVDZQto0t4M8U37jBCz1a4lZ3xeeIL72kPpNKtyBUY 70 | nFUdZMMDLQKBgQCr/Qwx9dsKZQ/opNWomWEEEkRs6qYrIFDRwIFcySIKLElVMy7B 71 | bfLTOZFE61Rd/VqH+QWRotAV2tMNYZgQ3RoshUf5JRY6mCtj6/TSj9k0/3yBqtlb 72 | 7mfK3D5sWalgDsBpMH1hkuOy3WI4Ve82+HtetbHoFlhvRiBDqGlHWVZKmwKBgFD5 73 | RodekW5W/XUsiKK3s7vJC7Y6fnckNPybVuAxQhNLm0Whn62XVrHVLNR8vJOCvJs+ 74 | uhiU6JgEk7IZ/cVPKV4r7W9ZGatPnPFX3wg0EzRLXuUUyNk/DYn4TWTfOrsc7CNE 75 | 38SJuB8oGM0n0I5sAUA+awc3y2FVMXfBJ9fqvEpNAoGBAIy5OYvIm4dlIs3gQNha 76 | fTkn26ULoAghamChlSXbqe2ECzmp1yTqg+UaxzWLnq5gsMpZC353Y/KRqo48ymyn 77 | +PqHiywaIhpxPDJwPKn/y7rLUJWsaU9aK39Jd4TzqY3e2Z6desqtVUF+ogr/Zgy8 78 | CZ+8cgwzJdaIiOs2xZ00O7qc 79 | -----END PRIVATE KEY----- 80 | ', 81 | // 'private_key_pass_phrase' => 'password', 82 | // 'content_type' => 'application/edi-x12', 83 | 'content_type' => 'Text/Plain', 84 | 'compression' => false, 85 | 'signature_algorithm' => 'sha256', 86 | 'signature_algorithm_required' => false, 87 | 'encryption_algorithm' => '3des', 88 | 'content_transfer_encoding' => 'base64', 89 | 'mdn_mode' => PartnerInterface::MDN_MODE_SYNC, 90 | 'mdn_options' => 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha256', 91 | ], 92 | 93 | // add your partners here ... 94 | 95 | [ 96 | // @see http://mendelson-e-c.com/as2/#testserversetup 97 | 98 | 'id' => 'mendelsontestAS2', 99 | 'target_url' => 'http://testas2.mendelson-e-c.com:8080/as2/HttpReceiver', 100 | 101 | // key4 102 | 'certificate' => '-----BEGIN CERTIFICATE----- 103 | MIIEJTCCAw2gAwIBAgIEWipbyDANBgkqhkiG9w0BAQsFADCBujEjMCEGCSqGSIb3DQEJARYUc2Vy 104 | dmljZUBtZW5kZWxzb24uZGUxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcM 105 | BkJlcmxpbjEiMCAGA1UECgwZbWVuZGVsc29uLWUtY29tbWVyY2UgR21iSDEhMB8GA1UECwwYRG8g 106 | bm90IHVzZSBpbiBwcm9kdWN0aW9uMR0wGwYDVQQDDBRtZW5kZWxzb24gdGVzdCBrZXkgNDAeFw0x 107 | NzEyMDgwOTMwNDhaFw0yNzEyMDYwOTMwNDhaMIG6MSMwIQYJKoZIhvcNAQkBFhRzZXJ2aWNlQG1l 108 | bmRlbHNvbi5kZTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGlu 109 | MSIwIAYDVQQKDBltZW5kZWxzb24tZS1jb21tZXJjZSBHbWJIMSEwHwYDVQQLDBhEbyBub3QgdXNl 110 | IGluIHByb2R1Y3Rpb24xHTAbBgNVBAMMFG1lbmRlbHNvbiB0ZXN0IGtleSA0MIIBIjANBgkqhkiG 111 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyeDD3FzJD3GdWoMj4pcpX7XLc5ZWJyVmt7ci+hCIyVmc4Kz5 112 | JIhAqQmes/EYNBf1CHBQL6yLbVPzfmDhadoXcRtVtosyG6+XvTzP8zaUQ5NcEZPkOA8S14VcvPkI 113 | X4I7NuU5TKkgRQ6G91tnFg3F5Ywm79qBuggxa3VPSofQpq3bJXYkaNI8vMARFyX/bDjNYFzOYCyD 114 | jG6Jwbwg1M69DLK6IGntku6PXGOf3X2BPMNgiZfV29sGIBKoWyx4q3p0qLXKYTPAtYP9+Uzkz+mq 115 | 2dcH56L6rFuAMbXYGEwarbby0JsVULc3q8+anlfxrfzDJH1KYzrdYmW6bRi/dh8AWQIDAQABozEw 116 | LzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA0GCSqGSIb3 117 | DQEBCwUAA4IBAQCh7+6IQjfGwsisA7xMNcPsRQC1av9T1eF2WjgmNjY0htKpK+Q2VgsAm3EgraoK 118 | EaUL5LaAJpQvH8iLVLdct3Qn483HVHeCiB/DE/eBrbxLVrUZqysZerWONX97BPbIBCKJAEm3Pqyi 119 | ej7IBY7WKy9OvCErUoH0zXsdfkuJlJXf1jS+qtEbWRGnbxwfXgH0S1uw7QU0q8EECvEb+MNrCEtD 120 | 4Wdjq35OFKLLPcChlEgoXabGefFSAeALnIZ2CJDn8Yz+7ZvdXkBjl17z9GYnR54bBz8CUxYqJBgu 121 | 0iE784sGpulvrJeeyrNS7EgP3odta2vn5ySjQQI8M8ubL+/cs1T7 122 | -----END CERTIFICATE----- 123 | ', 124 | 'content_type' => 'application/EDI-Consent', 125 | 'compression' => true, 126 | 'signature_algorithm' => 'sha256', 127 | 'encryption_algorithm' => '3des', 128 | // 'content_transfer_encoding' => 'binary', 129 | 'mdn_mode' => PartnerInterface::MDN_MODE_SYNC, 130 | 'mdn_options' => 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha256', 131 | ], 132 | 133 | [ 134 | // @see http://mendelson-e-c.com/as2_software 135 | 136 | 'id' => 'mycompanyAS2', 137 | // 'target_url' => 'http://127.0.0.1:8000', 138 | 'target_url' => 'http://127.0.0.1:8080/as2/HttpReceiver', 139 | 'private_key' => $key3['pkey'] ?: null, 140 | 'certificate' => $key3['cert'] ?: null, 141 | 'content_type' => 'application/EDI-Consent', 142 | 'compression' => true, 143 | 'signature_algorithm' => 'sha256', 144 | 'encryption_algorithm' => '3des', 145 | // 'content_transfer_encoding' => 'binary', 146 | 'mdn_mode' => PartnerInterface::MDN_MODE_SYNC, 147 | 'mdn_options' => 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha256', 148 | ], 149 | 150 | [ 151 | 'id' => 'webedi', 152 | 'target_url' => 'http://as2.webedi.co.uk:8181/as2connector/pub/ReceiveFile.rsb', 153 | 'certificate' => '-----BEGIN CERTIFICATE----- 154 | MIICwTCCAamgAwIBAgIBAjANBgkqhkiG9w0BAQUFADAkMRAwDgYDVQQDEwdXZWIgRURJMRAw 155 | DgYDVQQKEwdXZWIgRURJMB4XDTEzMTAxMDExMzkxN1oXDTIzMTAwODExMzkxN1owJDEQMA4G 156 | A1UEAxMHV2ViIEVESTEQMA4GA1UEChMHV2ViIEVESTCCASIwDQYJKoZIhvcNAQEBBQADggEP 157 | ADCCAQoCggEBANejtFteDrfVcsosbgerSLISkGZaomgRbqElAMzIoBt9auvPyiSzI893Ii1L 158 | GIgPzer/YKnyHJ278fwqJ9xID5BP0ukAOXdrrtbMwWC1cgPsHAljfCgOWMl10Ry6wadp9myG 159 | FV9z/WWI0eyXfSabQtTsiwZ9IX9EOBvJ1nrylB0WfIxz5aQc0WURyjtsEKFXMPdlF9xJezIj 160 | tzlom82vD2VSSqukmsjBe5IQSeKm99U96TPHeQHs+JETdcWrGCd5ff3fXZ5QuPj9hNQdIjxA 161 | JTJxMTKsqI4XSgqaXgq+5jaF5wv1FeA+ksQFGJyuoRslHsTnf94zyhjqY/iq1b8JaAkCAwEA 162 | ATANBgkqhkiG9w0BAQUFAAOCAQEAIUDhP/IvGLzu0NJqkCtA4YSdmAtWCQFlEV8NrbJQjWRQ 163 | Fbb88MMCxlXh/fs2PCISnpT9GyAbzCXFiA2v2aLoGqj1mSsaP9iIRiUNA9aJNVTWGEzkrGfk 164 | P3+zA/1bquqyvPwzY6KZAIp18swV/cmB8HKzQT7Q252agNSVPp/YFwYT84FWVVlFtMmgLJbU 165 | ROE7OEf7NAxOUOfirjU8JHgTJJNfJUbl2ma8nUqd+UKYG5NxsW6YnC+pBcWp66+h6do5vGLC 166 | nx0D9QmPKky9nScaBit2VgSoOdRLrGo48ZaYNWs/hgPKPFM+hyXNBD+1A/h8b+vm8pQm2VKv 167 | RnPyOoGNsA== 168 | -----END CERTIFICATE----- 169 | ', 170 | 'content_type' => 'application/EDI-Consent', 171 | 'compression' => false, 172 | 'signature_algorithm' => 'sha1', 173 | 'encryption_algorithm' => '3des', 174 | 'mdn_mode' => PartnerInterface::MDN_MODE_SYNC, 175 | 'mdn_options' => 'signed-receipt-protocol=required, pkcs7-signature; signed-receipt-micalg=optional, sha1', 176 | ], 177 | 178 | [ 179 | 'id' => 'EDI_AS2_OTTOGROUP', 180 | 'target_url' => 'http://80.85.204.104:6060/as2connector/pub/ReceiveFile.rsb', 181 | 'certificate' => '-----BEGIN CERTIFICATE----- 182 | MIIDYTCCAkmgAwIBAgIDAnOgMA0GCSqGSIb3DQEBCwUAMHMxDTALBgNVBAMTBE9UVE8xDTAL 183 | BgNVBAoTBE9UVE8xDTALBgNVBAsTBE9UVE8xEDAOBgNVBAcTB0hhbWJ1cmcxEDAOBgNVBAYT 184 | B0dlcm1hbnkxIDAeBgkqhkiG9w0BCQEWEWVkaUBvdHRvZ3JvdXAuY29tMB4XDTIwMTExMjE0 185 | MzI1M1oXDTIzMTExMjE0MzI1M1owczENMAsGA1UEAxMET1RUTzENMAsGA1UEChMET1RUTzEN 186 | MAsGA1UECxMET1RUTzEQMA4GA1UEBxMHSGFtYnVyZzEQMA4GA1UEBhMHR2VybWFueTEgMB4G 187 | CSqGSIb3DQEJARYRZWRpQG90dG9ncm91cC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 188 | ggEKAoIBAQDcTF5Qu6OZ9mE4OfdI8ok9WuRxNcCe5kXJd2pdeX7ufZDj8mTDQ1KKgoa9vpmY 189 | SGMpX9vFFS0cJIFVguhBdo/CH3jPVmbQABItkEHYLJxYKr06QTo4rKWnRpf9H6zgiO8wdZ/B 190 | F6iT79Hh9ogHI4N6woA7NjjF+q3612k1EPIekvZUJGxHiOhJjgbsPB0f+RlKPnW+lv7SwS4x 191 | tq66XzrCaXJXjBQBeaEP31x/dVECGynjcynJhTAJYv+9XY6q0TFrqDsDhOmf9jTE5k1h/tML 192 | y4A13cit2SHo0YN67mZwl+3cWP0h7cu1wF869HzyttPgWCG9/HHi5wLnU61aNxkhAgMBAAEw 193 | DQYJKoZIhvcNAQELBQADggEBAEoA9sJPxINJilnowUWw0UmRYVSHjXQIuLdt+HmThSH+5K7x 194 | TpwmD2OGmDAHPZ1wX/XNBuDye+ZoBWrybUhiPSTxN2K80N5Q4pJNcfS2LCw+m5qely0rfmZI 195 | 2AMOLJqB4M+nB5mxGWjMPAtBMtChcfpf5veOSLLw2aTb+/4Ek4c/6g3m8S2Uo+hdrt1EcLGC 196 | 06G7dcmG+ykziqGnQJRaC6pZy+Wxtytpp62kSOgPi72bA2BWOagTO5VxpoxbDtTdSNV3Qnfx 197 | u+xcDrxvgWE8E3M0RyA4uTx3idhMLK0WZC4fbjQPDpIsB3YgIzWwtdQN566byEqmZ3uP2iuR 198 | aHvMIGg= 199 | -----END CERTIFICATE----- 200 | ', 201 | 'content_type' => 'application/EDIFACT', 202 | 'compression' => false, 203 | 'signature_algorithm' => 'sha1', 204 | 'encryption_algorithm' => '3des', 205 | 'mdn_mode' => PartnerInterface::MDN_MODE_SYNC, 206 | 'mdn_options' => 'signed-receipt-protocol=required, pkcs7-signature; signed-receipt-micalg=optional, sha1', 207 | ], 208 | // local station 209 | 210 | [ 211 | 'id' => 'as2polini', 212 | 'email' => 'phpas2@example.com', 213 | 'target_url' => 'http://127.0.0.1:8000', 214 | 'certificate' => '-----BEGIN CERTIFICATE----- 215 | MIIC8TCCAdmgAwIBAgIJAI+A9000plXZMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV 216 | BAMMEmFzMi5ldGVjby1ncm91cC5kZTAeFw0xOTAxMTQwOTA1NThaFw0yOTAxMTEw 217 | OTA1NThaMB0xGzAZBgNVBAMMEmFzMi5ldGVjby1ncm91cC5kZTCCASIwDQYJKoZI 218 | hvcNAQEBBQADggEPADCCAQoCggEBAJZ/xA4MJPPA66Ils84DwklSBxim788LzFOs 219 | i99RgO1ktfbbKFrdXIrEUYWvDmSMbiY7ALz9UsMKPA7T0/t0l1XkGuCh+/TqAQgb 220 | MkjzNkCpjedufC9ghMSUhndSGIMdsQf70styWiZVSSNnZ4cG26H+mVJXKVcr5BRZ 221 | ufv3fR+wMuADuGSE5xR5R+jhOLxgJEfvpZeuGKhGix6sdagE3MfjOH8vbtOmrblt 222 | u8H9mbXPkiz9aSvEV3ocbesVIOxjhiWzUYvbYRhABebtNDKlvb7j3aBjoSHzaEhZ 223 | OP4O+uSsoHOh0if0ukP3ksmHixEOVFzV8bPc92q3ONTH4ZNXFOkCAwEAAaM0MDIw 224 | MAYDVR0RBCkwJ4ISYXMyLmV0ZWNvLWdyb3VwLmRlggtleGFtcGxlLm5ldIcECgAA 225 | ATANBgkqhkiG9w0BAQsFAAOCAQEAOFHlT5n6IdH2xv6bi50OCwSPajVPz8hCAo6X 226 | TdRwE5InaLVgziuRfQD1s/GUjLeM89u42CgA2FNkeKc4/iGvhCueFGMRjBlhHOEo 227 | DwdcFpkLNJgtfaEmFDOHHjXgIP+MHbEQ7uu9Yspf+hdTDMT1CbCwRIWMfdt1VhGO 228 | ZXoAg2Jgzwyqszf+H6EXilqZtzYdSDV4r2XoX+n2Oe6V3ootNdtsbh2QrtiTMS32 229 | 8brydAbXFsOzr2B/ygQkLPmEITgjyDqn2oXI8YR6Mfw0MImkGByapr7g+/eLHnuP 230 | 4ULqoQh54EkiTfodzbdRkhvT1cA9U+hH8BdPDB+jDsP5BWCcwg== 231 | -----END CERTIFICATE----- 232 | ', 233 | 'private_key' => '-----BEGIN PRIVATE KEY----- 234 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCWf8QODCTzwOui 235 | JbPOA8JJUgcYpu/PC8xTrIvfUYDtZLX22yha3VyKxFGFrw5kjG4mOwC8/VLDCjwO 236 | 09P7dJdV5Brgofv06gEIGzJI8zZAqY3nbnwvYITElIZ3UhiDHbEH+9LLclomVUkj 237 | Z2eHBtuh/plSVylXK+QUWbn7930fsDLgA7hkhOcUeUfo4Ti8YCRH76WXrhioRose 238 | rHWoBNzH4zh/L27Tpq25bbvB/Zm1z5Is/WkrxFd6HG3rFSDsY4Yls1GL22EYQAXm 239 | 7TQypb2+492gY6Eh82hIWTj+DvrkrKBzodIn9LpD95LJh4sRDlRc1fGz3PdqtzjU 240 | x+GTVxTpAgMBAAECggEARd/0SwFgdrvvq00N+mzMW/Z1zQBU/zBfIcpO9tSEo7PK 241 | uF5wkh+Mw/D6WLM6X3zD94QVh6mmL2AlGk1HcsxjJ0HNKNaMgN3UtMrLwgsJ+WO/ 242 | uuAVUHnjqtG6zNOVBetXMnm9GTByorGeT43HB24rsz7eONi3HP4H21r9evshYQBb 243 | jqtlon/VGQcSWELVUNQg7i04ym0FvvmAZCYU085sZ2+3gaBtqwEHIH5DcX89Wf9I 244 | 3E3u2CrYWISPrzk4elBpjposnieM5ngxSi+WwUqkKxAsCO7VyEtG6IRgeip7mvIJ 245 | NYn4LH6QYV2pAcHh2dGxv30bscxC4tvDDibVMJ8aAQKBgQDIqWO3FAMYpd/0d9Mt 246 | f+0KFe2aiMz8Zxky1q01hhJuSA8d1OwyFUUklnJJWFs7BOLQ6JxMhBa9Ll5wAnPk 247 | Yb504ee2k4titXMTjZYSJnh/TxNvLQq1+dz7bTR6cD/VY+sHcNp1ThJDyfaUxZZG 248 | aNACc4E1U/Nf+y+5sZe4VTE/EQKBgQDAAOxcmYWuDOb4HMb9GsjEw5t6FUIY9ORB 249 | YhdWh56iJDCXkxN9QvxSkm4R/4lorPtnWWOcA8XUJq6ti/D2pdRAFbFUKSzvXA+M 250 | ZLUvkZmYT2c9NOV/tY6KTegxoGT9Rn0csw+rRqmU3bOAqX3xIedUfie29ze6gnzH 251 | X/Mv4IWoWQKBgHc6DoGNZnmStYrwV43FYPaJKPCVMBcYuyQ14hzXWMQmFLVI+j6X 252 | 3MlsiuOBmFNtB8fRLm1YXppxnrM3Ad1FJoEUaTVWXY98+K85hV2rdhVOyuFYBfEy 253 | UVci//dwEr2b7N4y89qXVMrqiZTEAhI73LxYHQGurADvot/W4aspE2XBAoGAfzsd 254 | ZW9GKkPaeed35RjumZSVXpzfo/IDn2AE3w4XjJI2sPqBG6xbz8vArKSMxZR7M80E 255 | OMo3OZI4hkAJeSgCMkUtsPtoD2UN2JaTieYNxeQ4IVMAEVSaFAP0LY5/3WXsWiw9 256 | 4d19WmxfGo82Kaexx0ehwZiokSsOzH9EgyFg8GECgYBBYtizJ2swvsyR+sMPoUEh 257 | 0hqCxCwEOJg3LetZ0/lWcdWQjuDyX+h0K/uyYo+TGL2hxPe3/Mi/POn70+5iNb9Q 258 | 2imhWJnyyQpU9rqGeIIWzcpqc7Du6S/sB84VwibT/YbWsMZW7KHCcLpWNwWfe0nJ 259 | okbKQXNdhalu6KK2joUYMg== 260 | -----END PRIVATE KEY----- 261 | ', 262 | 'content_type' => 'application/EDI-Consent', 263 | 'compression' => true, 264 | 'signature_algorithm' => 'sha256', 265 | 'encryption_algorithm' => '3des', 266 | 'mdn_mode' => PartnerInterface::MDN_MODE_SYNC, 267 | 'mdn_options' => 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha256', 268 | ], 269 | 270 | [ 271 | 'id' => 'phpas2', 272 | 'email' => 'phpas2@example.com', 273 | 'target_url' => 'http://127.0.0.1:8000', 274 | 'certificate' => $local['cert'] ?: null, 275 | 'private_key' => $local['pkey'] ?: null, 276 | // 'private_key_pass_phrase' => 'password', 277 | // 'content_type' => 'application/edi-x12', 278 | 'content_type' => 'application/EDI-Consent', 279 | 'compression' => true, 280 | 'signature_algorithm' => 'sha256', 281 | 'encryption_algorithm' => '3des', 282 | 'mdn_mode' => PartnerInterface::MDN_MODE_SYNC, 283 | 'mdn_options' => 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha256', 284 | ], 285 | ]; 286 | -------------------------------------------------------------------------------- /example/config/settings.php: -------------------------------------------------------------------------------- 1 | true, 14 | 15 | 'logHandlers' => [ 16 | // TODO: some hosting providers doesn't support 'php://stdout' 17 | new StreamHandler('php://stdout'), 18 | // new StreamHandler($storagePath.'/logs/app.log', Logger::DEBUG), 19 | ], 20 | 21 | 'management' => [ 22 | /** 23 | * @see \AS2\Management::$options 24 | */ 25 | ], 26 | 27 | 'storage' => [ 28 | 'path' => $storagePath, 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /example/public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteCond %{REQUEST_FILENAME} !-d 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule . index.php [QSA,L] 5 | -------------------------------------------------------------------------------- /example/public/index.php: -------------------------------------------------------------------------------- 1 | run(); 6 | -------------------------------------------------------------------------------- /example/resources/key3.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiamo/phpas2/52ea41fb9b4d470ecc54e28b12a478dba3d56c3b/example/resources/key3.pfx -------------------------------------------------------------------------------- /example/resources/phpas2.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiamo/phpas2/52ea41fb9b4d470ecc54e28b12a478dba3d56c3b/example/resources/phpas2.p12 -------------------------------------------------------------------------------- /example/storage/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !logs/ 3 | !messages/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /example/storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /example/storage/messages/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | ./tests 18 | 19 | 20 | 21 | 22 | ./src 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/ASN1Helper.php: -------------------------------------------------------------------------------- 1 | ASN1::TYPE_SEQUENCE, 91 | 'children' => [ 92 | 'contentType' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], 93 | 'content' => [ 94 | 'type' => $type, 95 | 'constant' => 0, 96 | 'optional' => true, 97 | 'explicit' => true, 98 | ], 99 | ], 100 | ]; 101 | } 102 | 103 | public static function getCompressedDataMap(): array 104 | { 105 | return [ 106 | 'type' => ASN1::TYPE_SEQUENCE, 107 | 'children' => [ 108 | 'version' => [ 109 | 'type' => ASN1::TYPE_INTEGER, 110 | 'mapping' => ['0', '1', '2', '4', '5'], 111 | ], 112 | 'compression' => ASN1\Maps\AlgorithmIdentifier::MAP, 113 | 'payload' => self::getContentInfoMap(ASN1::TYPE_OCTET_STRING), 114 | ], 115 | ]; 116 | } 117 | 118 | public static function certificateChoiceMap(): array 119 | { 120 | return [ 121 | 'type' => ASN1::TYPE_CHOICE, 122 | 'children' => [ 123 | 'certificate' => ASN1\Maps\Certificate::MAP, 124 | // 'extendedCertificate' => [], // Obsolete 125 | // 'v1AttrCert' => [], // Obsolete 126 | 'v2AttrCert' => [ 127 | 'type' => ASN1::TYPE_SEQUENCE, 128 | 'implicit' => true, 129 | 'children' => [ 130 | 'acinfo' => ASN1::TYPE_ANY, 131 | 'signatureAlgorithm' => ASN1\Maps\AlgorithmIdentifier::MAP, 132 | 'signatureValue' => ASN1::TYPE_BIT_STRING, 133 | ], 134 | ], 135 | 'other' => [ 136 | 'type' => ASN1::TYPE_SEQUENCE, 137 | 'implicit' => true, 138 | 'children' => [ 139 | 'otherCertFormat' => ASN1::TYPE_OBJECT_IDENTIFIER, 140 | 'otherCert' => ASN1::TYPE_ANY, 141 | ], 142 | ], 143 | ], 144 | ]; 145 | } 146 | 147 | public static function getSignerIdentifierMap(): array 148 | { 149 | return [ 150 | 'type' => ASN1::TYPE_CHOICE, 151 | 'children' => [ 152 | 'issuerAndSerialNumber' => [ 153 | 'type' => ASN1::TYPE_SEQUENCE, 154 | 'children' => [ 155 | 'issuer' => ASN1\Maps\Name::MAP, 156 | 'serialNumber' => ASN1\Maps\CertificateSerialNumber::MAP, 157 | ], 158 | ], 159 | 'subjectKeyIdentifier' => [ 160 | 'type' => ASN1::TYPE_OCTET_STRING, 161 | 'constant' => 0, 162 | 'implicit' => true, 163 | ], 164 | ], 165 | ]; 166 | } 167 | 168 | public static function getSignerInfoMap(): array 169 | { 170 | return [ 171 | 'type' => ASN1::TYPE_SEQUENCE, 172 | 'children' => [ 173 | 'version' => [ 174 | 'type' => ASN1::TYPE_INTEGER, 175 | 'mapping' => ['0', '1', '2', '4', '5'], 176 | ], 177 | 'sid' => self::getSignerIdentifierMap(), 178 | 'digestAlgorithm' => ASN1\Maps\AlgorithmIdentifier::MAP, 179 | 'signedAttrs' => ASN1\Maps\Attributes::MAP + [ 180 | 'constant' => 0, 181 | 'optional' => true, 182 | 'implicit' => true, 183 | ], 184 | 'signatureAlgorithm' => ASN1\Maps\AlgorithmIdentifier::MAP, 185 | 'signature' => ['type' => ASN1::TYPE_OCTET_STRING], 186 | // 'unsignedAttrs' => ASN1\Maps\Attributes::MAP + [ 187 | // 'constant' => 1, 188 | // 'optional' => true, 189 | // 'implicit' => true, 190 | // ], 191 | ], 192 | ]; 193 | } 194 | 195 | public static function getSignedDataMap(): array 196 | { 197 | return [ 198 | 'type' => ASN1::TYPE_SEQUENCE, 199 | 'children' => [ 200 | 'contentType' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], 201 | 'content' => [ 202 | 'type' => ASN1::TYPE_SEQUENCE, 203 | 'constant' => 0, 204 | 'optional' => true, 205 | 'explicit' => true, 206 | 'children' => [ 207 | // CMSVersion 208 | 'version' => [ 209 | 'type' => ASN1::TYPE_INTEGER, 210 | 'mapping' => ['0', '1', '2', '4', '5'], 211 | ], 212 | 'digestAlgorithms' => [ 213 | 'type' => ASN1::TYPE_SET, 214 | 'min' => 1, 215 | 'max' => -1, 216 | 'children' => ASN1\Maps\AlgorithmIdentifier::MAP, 217 | ], 218 | 'contentInfo' => self::getContentInfoMap(ASN1::TYPE_OCTET_STRING), 219 | 'certificates' => [ 220 | 'type' => ASN1::TYPE_SET, 221 | 'constant' => 0, 222 | 'implicit' => true, 223 | 'optional' => true, 224 | 'min' => 1, 225 | 'max' => -1, 226 | // 'children' => self::certificateChoiceMap(), 227 | 'children' => ASN1\Maps\Certificate::MAP, 228 | ], 229 | 'crls' => [ 230 | 'type' => ASN1::TYPE_SET, 231 | 'constant' => 1, 232 | 'implicit' => true, 233 | 'optional' => true, 234 | 'min' => 1, 235 | 'max' => -1, 236 | 'children' => ASN1\Maps\CertificateList::MAP, 237 | ], 238 | // 'a' => ['type' => ASN1::TYPE_ANY, 'optional' => true], 239 | 'signers' => [ 240 | 'type' => ASN1::TYPE_SET, 241 | 'min' => 1, 242 | 'max' => -1, 243 | 'children' => self::getSignerInfoMap(), 244 | ], 245 | ], 246 | ], 247 | ], 248 | ]; 249 | } 250 | 251 | /** 252 | * @param array|string $source 253 | * @param array|string $mapping 254 | * @param array $special 255 | * 256 | * @return string 257 | */ 258 | public static function encode($source, $mapping, $filters = [], $special = []) 259 | { 260 | ASN1::setFilters($filters); 261 | 262 | return ASN1::encodeDER($source, $mapping, $special); 263 | } 264 | 265 | /** 266 | * @param string $data 267 | * @param array $mapping 268 | * 269 | * @return array 270 | */ 271 | public static function decode($data, $mapping = []) 272 | { 273 | $decoded = ASN1::decodeBER($data); 274 | 275 | if (empty($decoded)) { 276 | throw new \RuntimeException('Invalid ASN1 Data.'); 277 | } 278 | 279 | return ASN1::asn1map($decoded[0], $mapping); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/MessageInterface.php: -------------------------------------------------------------------------------- 1 | rawMessage = $rawMessage; 60 | } 61 | 62 | $this->setHeaders($this->normalizeHeaders($headers)); 63 | 64 | if (! is_null($body)) { 65 | $this->setBody($body); 66 | } 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | public function __toString() 73 | { 74 | return $this->toString(); 75 | } 76 | 77 | /** 78 | * Instantiate from Request Object. 79 | * 80 | * @return static 81 | */ 82 | public static function fromPsrMessage(PsrMessageInterface $message) 83 | { 84 | return new static($message->getHeaders(), $message->getBody()->getContents()); 85 | } 86 | 87 | /** 88 | * Instantiate from Request Object. 89 | * 90 | * @return static 91 | * 92 | * @deprecated Please use MimePart::fromPsrMessage 93 | */ 94 | public static function fromRequest(RequestInterface $request) 95 | { 96 | return self::fromPsrMessage($request); 97 | } 98 | 99 | /** 100 | * Instantiate from raw message string. 101 | * 102 | * @param string $rawMessage 103 | * @param bool $saveRaw 104 | * 105 | * @return static 106 | */ 107 | public static function fromString($rawMessage, $saveRaw = true) 108 | { 109 | $payload = Utils::parseMessage($rawMessage); 110 | 111 | return new static($payload['headers'], $payload['body'], $saveRaw ? $rawMessage : null); 112 | } 113 | 114 | /** 115 | * Recreate message with base64 if part is binary. 116 | */ 117 | public static function createIfBinaryPart(self $message): ?self 118 | { 119 | $hasBinary = false; 120 | 121 | $temp = new self($message->getHeaders()); 122 | foreach ($message->getParts() as $part) { 123 | if (Utils::isBinary($part->getBodyString())) { 124 | $hasBinary = true; 125 | $recreatedPart = new self($part->getHeaders(), Utils::encodeBase64($part->getBodyString())); 126 | $temp->addPart($recreatedPart); 127 | } else { 128 | $temp->addPart($part); 129 | } 130 | } 131 | 132 | return $hasBinary ? $temp : null; 133 | } 134 | 135 | /** 136 | * @return bool 137 | */ 138 | public function isPkc7Mime() 139 | { 140 | $type = $this->getParsedHeader('content-type', 0, 0); 141 | $type = strtolower($type); 142 | 143 | return $type === self::TYPE_PKCS7_MIME || $type === self::TYPE_X_PKCS7_MIME; 144 | } 145 | 146 | /** 147 | * @return bool 148 | */ 149 | public function isPkc7Signature() 150 | { 151 | $type = $this->getParsedHeader('content-type', 0, 0); 152 | $type = strtolower($type); 153 | 154 | return $type === self::TYPE_PKCS7_SIGNATURE || $type === self::TYPE_X_PKCS7_SIGNATURE; 155 | } 156 | 157 | /** 158 | * @return bool 159 | */ 160 | public function isEncrypted() 161 | { 162 | return $this->getParsedHeader('content-type', 0, 'smime-type') === self::SMIME_TYPE_ENCRYPTED; 163 | } 164 | 165 | /** 166 | * @return bool 167 | */ 168 | public function isCompressed() 169 | { 170 | return $this->getParsedHeader('content-type', 0, 'smime-type') === self::SMIME_TYPE_COMPRESSED; 171 | } 172 | 173 | /** 174 | * @return bool 175 | */ 176 | public function isSigned() 177 | { 178 | return $this->getParsedHeader('content-type', 0, 0) === self::MULTIPART_SIGNED; 179 | } 180 | 181 | /** 182 | * @return bool 183 | */ 184 | public function isReport() 185 | { 186 | $isReport = $this->getParsedHeader('content-type', 0, 0) === self::MULTIPART_REPORT; 187 | 188 | if ($isReport) { 189 | return true; 190 | } 191 | 192 | if ($this->isSigned()) { 193 | foreach ($this->getParts() as $part) { 194 | if ($part->isReport()) { 195 | return true; 196 | } 197 | } 198 | } 199 | 200 | return false; 201 | } 202 | 203 | /** 204 | * @return bool 205 | */ 206 | public function isBinary() 207 | { 208 | return $this->getParsedHeader('content-transfer-encoding', 0, 0) === 'binary'; 209 | } 210 | 211 | /** 212 | * @return bool 213 | */ 214 | public function getCountParts() 215 | { 216 | return \count($this->parts); 217 | } 218 | 219 | /** 220 | * @return bool 221 | */ 222 | public function isMultiPart() 223 | { 224 | return \count($this->parts) > 1; 225 | } 226 | 227 | /** 228 | * @return MimePart[] 229 | */ 230 | public function getParts() 231 | { 232 | return $this->parts; 233 | } 234 | 235 | /** 236 | * @return static|null 237 | */ 238 | public function getPart($num) 239 | { 240 | return isset($this->parts[$num]) ? $this->parts[$num] : null; 241 | } 242 | 243 | /** 244 | * @return $this 245 | */ 246 | public function addPart($part) 247 | { 248 | if ($part instanceof static) { 249 | $this->parts[] = $part; 250 | } else { 251 | $this->parts[] = self::fromString((string) $part); 252 | } 253 | 254 | return $this; 255 | } 256 | 257 | /** 258 | * @param int $num 259 | * 260 | * @return bool 261 | */ 262 | public function removePart($num) 263 | { 264 | if (isset($this->parts[$num])) { 265 | unset($this->parts[$num]); 266 | 267 | return true; 268 | } 269 | 270 | return false; 271 | } 272 | 273 | /** 274 | * @return string 275 | */ 276 | public function getHeaderLines() 277 | { 278 | return Utils::normalizeHeaders($this->headers, self::EOL); 279 | } 280 | 281 | /** 282 | * @param string $header 283 | * @param int $index 284 | * @param int|string $param 285 | * 286 | * @return array|string|null 287 | */ 288 | public function getParsedHeader($header, $index = null, $param = null) 289 | { 290 | /** @noinspection CallableParameterUseCaseInTypeContextInspection */ 291 | $header = Utils::parseHeader($this->getHeader($header)); 292 | if ($index === null) { 293 | return $header; 294 | } 295 | $params = isset($header[$index]) ? $header[$index] : []; 296 | if ($param !== null) { 297 | return isset($params[$param]) ? $params[$param] : null; 298 | } 299 | 300 | return $params; 301 | } 302 | 303 | /** 304 | * Return the currently set message body. 305 | * 306 | * @return StreamInterface returns the body as a stream 307 | */ 308 | public function getBody(): StreamInterface 309 | { 310 | $body = $this->body; 311 | if (\count($this->parts) > 0) { 312 | $boundary = $this->getParsedHeader('content-type', 0, 'boundary'); 313 | if ($boundary) { 314 | // $body .= self::EOL; 315 | foreach ($this->getParts() as $part) { 316 | // $body .= self::EOL; 317 | $body .= '--'.$boundary.self::EOL; 318 | $body .= $part->toString().self::EOL; 319 | } 320 | $body .= '--'.$boundary.'--'.self::EOL; 321 | } 322 | } 323 | 324 | return PsrUtils::streamFor($body); 325 | } 326 | 327 | /** 328 | * Return the currently set message body as a string. 329 | * 330 | * @return string returns the body as a string 331 | */ 332 | public function getBodyString(): string 333 | { 334 | return PsrUtils::copyToString($this->getBody()); 335 | } 336 | 337 | /** 338 | * @param array|static|string $body 339 | * 340 | * @return $this 341 | */ 342 | public function setBody($body) 343 | { 344 | if ($body instanceof static) { 345 | $this->addPart($body); 346 | } elseif (\is_array($body)) { 347 | foreach ($body as $part) { 348 | $this->addPart($part); 349 | } 350 | } else { 351 | $boundary = $this->getParsedHeader('content-type', 0, 'boundary'); 352 | 353 | if ($boundary) { 354 | $parts = explode('--'.$boundary, $body); 355 | array_shift($parts); // remove unecessary first element 356 | array_pop($parts); // remove unecessary last element 357 | 358 | foreach ($parts as $part) { 359 | // $part = preg_replace('/^\r?\n|\r?\n$/','',$part); 360 | // Using substr instead of preg_replace as that option is removing multiple break lines instead of only one 361 | 362 | // /^\r?\n/ 363 | if (str_starts_with($part, "\r\n")) { 364 | $part = substr($part, 2); 365 | } elseif ($part[0] === "\n") { 366 | $part = substr($part, 1); 367 | } 368 | // /\r?\n$/ 369 | if (str_ends_with($part, "\r\n")) { 370 | $part = substr($part, 0, -2); 371 | } elseif (str_ends_with($part, "\n")) { 372 | $part = substr($part, 0, -1); 373 | } 374 | 375 | $this->addPart($part); 376 | } 377 | } else { 378 | $this->body = $body; 379 | } 380 | } 381 | 382 | return $this; 383 | } 384 | 385 | /** 386 | * @return $this|self 387 | */ 388 | public function withoutRaw() 389 | { 390 | $this->rawMessage = null; 391 | 392 | return $this; 393 | } 394 | 395 | /** 396 | * Serialize to string. 397 | * 398 | * @return string 399 | */ 400 | public function toString() 401 | { 402 | if ($this->rawMessage) { 403 | return $this->rawMessage; 404 | } 405 | 406 | return $this->getHeaderLines().self::EOL.$this->getBodyString(); 407 | } 408 | 409 | /** 410 | * @return array 411 | */ 412 | private function normalizeHeaders($headers) 413 | { 414 | if (\is_array($headers)) { 415 | foreach ($headers as $key => $value) { 416 | if (strtolower($key) === 'content-type') { 417 | $headers[$key] = str_replace('x-pkcs7-', 'pkcs7-', $headers[$key]); 418 | } 419 | } 420 | } 421 | 422 | return $headers; 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /src/PartnerInterface.php: -------------------------------------------------------------------------------- 1 | manager = $management; 42 | $this->partnerRepository = $partnerRepository; 43 | $this->messageRepository = $messageRepository; 44 | } 45 | 46 | /** 47 | * Function receives AS2 requests from partner. 48 | * Checks whether it's an AS2 message or an MDN and acts accordingly. 49 | * 50 | * @return Response 51 | */ 52 | public function execute(ServerRequestInterface $request = null) 53 | { 54 | if (! $request) { 55 | $request = ServerRequest::fromGlobals(); 56 | } 57 | 58 | $responseStatus = 200; 59 | $responseHeaders = []; 60 | $responseBody = null; 61 | 62 | $message = null; 63 | 64 | try { 65 | if ($request->getMethod() !== 'POST') { 66 | return new Response(200, [], 'To submit an AS2 message, you must POST the message to this URL.'); 67 | } 68 | 69 | $this->getLogger()->debug( 70 | sprintf( 71 | 'Received an HTTP POST from `%s`.', 72 | isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'Unknown' 73 | ) 74 | ); 75 | 76 | foreach (['message-id', 'as2-from', 'as2-to'] as $header) { 77 | if (! $request->hasHeader($header)) { 78 | throw new \InvalidArgumentException(sprintf('Missing required header `%s`.', $header)); 79 | } 80 | } 81 | 82 | // Get the message id, sender and receiver AS2 IDs 83 | $messageId = trim($request->getHeaderLine('message-id'), '<>'); 84 | $senderId = $request->getHeaderLine('as2-from'); 85 | $receiverId = $request->getHeaderLine('as2-to'); 86 | 87 | $this->getLogger()->debug('Check payload to see if its an AS2 Message or ASYNC MDN.'); 88 | 89 | // Load the request header and body as a MIME Email Message 90 | $payload = MimePart::fromPsrMessage($request); 91 | 92 | // If this is an MDN, get the message ID and check if it exists 93 | if ($payload->isReport()) { 94 | $this->getLogger()->info( 95 | sprintf( 96 | 'Asynchronous MDN received for AS2 message `%s` to organization `%s` from partner `%s`.', 97 | $messageId, 98 | $receiverId, 99 | $senderId 100 | ) 101 | ); 102 | 103 | // Get Original Message-Id 104 | $origMessageId = null; 105 | foreach ($payload->getParts() as $part) { 106 | if ($part->getParsedHeader('content-type', 0, 0) === 'message/disposition-notification') { 107 | $bodyPayload = MimePart::fromString($part->getBodyString()); 108 | $origMessageId = trim($bodyPayload->getParsedHeader('original-message-id', 0, 0), '<>'); 109 | } 110 | } 111 | 112 | $message = $this->messageRepository->findMessageById($origMessageId); 113 | if (! $message) { 114 | throw new \RuntimeException('Unknown AS2 MDN received. Will not be processed'); 115 | } 116 | 117 | // TODO: check if mdn already exists 118 | $this->manager->processMdn($message, $payload); 119 | $this->messageRepository->saveMessage($message); 120 | 121 | $responseBody = 'AS2 ASYNC MDN has been received'; 122 | } else { 123 | // Process the received AS2 message from partner 124 | 125 | // Raise duplicate message error in case message already exists in the system 126 | $message = $this->messageRepository->findMessageById($messageId); 127 | if ($message) { 128 | throw new \RuntimeException('An identical message has already been sent to our server'); 129 | } 130 | 131 | $sender = $this->findPartner($senderId); 132 | $receiver = $this->findPartner($receiverId); 133 | 134 | // Create a new message 135 | $message = $this->messageRepository->createMessage(); 136 | $message->setMessageId($messageId); 137 | $message->setDirection(MessageInterface::DIR_INBOUND); 138 | $message->setStatus(MessageInterface::STATUS_IN_PROCESS); 139 | $message->setSender($sender); 140 | $message->setReceiver($receiver); 141 | $message->setHeaders($payload->getHeaderLines()); 142 | 143 | try { 144 | // Process the received payload to extract the actual message from partner 145 | $payload = $this->manager->processMessage($message, $payload); 146 | 147 | $message->setPayload($payload); 148 | 149 | // If MDN enabled than send notification 150 | // Create MDN if it requested by partner 151 | $mdnMode = $sender->getMdnMode(); 152 | 153 | if ($mdnMode && ($mdn = $this->manager->buildMdn($message))) { 154 | $mdnMessageId = trim($mdn->getHeaderLine('message-id'), '<>'); 155 | $message->setMdnPayload($mdn->toString()); 156 | if ($mdnMode === PartnerInterface::MDN_MODE_SYNC) { 157 | $this->getLogger()->debug( 158 | sprintf( 159 | 'Synchronous MDN with id `%s` sent as answer to message `%s`.', 160 | $mdnMessageId, 161 | $messageId 162 | ) 163 | ); 164 | $responseHeaders = $mdn->getHeaders(); 165 | $responseBody = $mdn->getBodyString(); 166 | } else { 167 | $this->getLogger()->debug( 168 | sprintf( 169 | 'Asynchronous MDN with id `%s` sent as answer to message `%s`.', 170 | $mdnMessageId, 171 | $messageId 172 | ) 173 | ); 174 | 175 | // TODO: async, event, queue, etc. 176 | $this->manager->sendMdn($message); 177 | } 178 | } 179 | 180 | $message->setStatus(MessageInterface::STATUS_SUCCESS); 181 | } catch (\Exception $e) { 182 | $message->setStatus(MessageInterface::STATUS_ERROR); 183 | $message->setStatusMsg($e->getMessage()); 184 | 185 | throw $e; 186 | } finally { 187 | $this->messageRepository->saveMessage($message); 188 | } 189 | } 190 | } catch (\Exception $e) { 191 | $this->getLogger()->critical($e->getMessage()); 192 | if ($message !== null) { 193 | // TODO: check 194 | // Build the mdn for the message based on processing status 195 | $mdn = $this->manager->buildMdn($message, null, $e->getMessage()); 196 | $responseHeaders = $mdn->getHeaders(); 197 | $responseBody = $mdn->getBodyString(); 198 | } else { 199 | $responseStatus = 500; 200 | $responseBody = $e->getMessage(); 201 | } 202 | } 203 | 204 | if (empty($responseBody)) { 205 | $responseBody = 'AS2 message has been received'; 206 | } 207 | 208 | return new Response($responseStatus, $responseHeaders, $responseBody); 209 | } 210 | 211 | /** 212 | * @return LoggerInterface 213 | */ 214 | public function getLogger() 215 | { 216 | if (! $this->logger) { 217 | $this->logger = $this->manager->getLogger(); 218 | } 219 | 220 | if (! $this->logger) { 221 | $this->logger = new NullLogger(); 222 | } 223 | 224 | return $this->logger; 225 | } 226 | 227 | /** 228 | * @return $this 229 | */ 230 | public function setLogger(LoggerInterface $logger) 231 | { 232 | $this->logger = $logger; 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * @param string $id 239 | * 240 | * @return PartnerInterface 241 | */ 242 | protected function findPartner($id) 243 | { 244 | $partner = $this->partnerRepository->findPartnerById($id); 245 | if (! $partner) { 246 | throw new \RuntimeException(sprintf('Unknown AS2 Partner with id `%s`.', $id)); 247 | } 248 | 249 | return $partner; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 | &$v) { 130 | if (strtolower($k) === 'content-type') { 131 | $v[0] .= $line; 132 | 133 | break; 134 | } 135 | } 136 | } 137 | } 138 | 139 | return $headers; 140 | } 141 | 142 | /** 143 | * Parse an array of header values containing ";" separated data into an 144 | * array of associative arrays representing the header key value pair 145 | * data of the header. When a parameter does not contain a value, but just 146 | * contains a key, this function will inject a key with a '' string value. 147 | * 148 | * @param array|string $header header to parse into components 149 | * 150 | * @return array returns the parsed header values 151 | */ 152 | public static function parseHeader($header) 153 | { 154 | static $trimmed = "'\" \t\n\r\0\x0B"; 155 | $params = []; 156 | foreach (self::normalizeHeader($header) as $val) { 157 | $part = []; 158 | foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) { 159 | $m = explode('=', $kvp, 2); 160 | if (isset($m[1])) { 161 | $part[trim($m[0], $trimmed)] = trim($m[1], $trimmed); 162 | } else { 163 | $part[] = trim($m[0], $trimmed); 164 | } 165 | } 166 | if ($part) { 167 | $params[] = $part; 168 | } 169 | } 170 | 171 | return $params; 172 | } 173 | 174 | /** 175 | * Converts an array of header values that may contain comma separated 176 | * headers into an array of headers with no comma separated values. 177 | * 178 | * @param array|string $header header to normalize 179 | * 180 | * @return array returns the normalized header field values 181 | */ 182 | public static function normalizeHeader($header) 183 | { 184 | if (! \is_array($header)) { 185 | return array_map('trim', explode(',', $header)); 186 | } 187 | $result = []; 188 | foreach ($header as $value) { 189 | foreach ((array) $value as $v) { 190 | if (! str_contains($v, ',')) { 191 | $result[] = $v; 192 | 193 | continue; 194 | } 195 | foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $v) as $vv) { 196 | $result[] = trim($vv); 197 | } 198 | } 199 | } 200 | 201 | return $result; 202 | } 203 | 204 | /** 205 | * Converts an array of header values that may contain comma separated 206 | * headers into a string representation. 207 | * 208 | * @param string[][] $headers 209 | * @param string $eol 210 | * 211 | * @return string 212 | */ 213 | public static function normalizeHeaders($headers, $eol = "\r\n") 214 | { 215 | $result = ''; 216 | foreach ($headers as $name => $values) { 217 | $values = implode(', ', (array) $values); 218 | if ($name === 'Content-Type') { 219 | // some servers don't support "x-" 220 | $values = str_replace('x-pkcs7-', 'pkcs7-', $values); 221 | } 222 | $result .= $name.': '.$values.$eol; 223 | } 224 | 225 | return $result; 226 | } 227 | 228 | /** 229 | * Encode a given string in base64 encoding and break lines 230 | * according to the maximum line length. 231 | * 232 | * @param string $str 233 | * @param int $lineLength 234 | * @param string $lineEnd 235 | * 236 | * @return string 237 | */ 238 | public static function encodeBase64($str, $lineLength = 64, $lineEnd = "\r\n") 239 | { 240 | $lineLength -= ($lineLength % 4); 241 | 242 | return rtrim(chunk_split(base64_encode($str), $lineLength, $lineEnd)); 243 | } 244 | 245 | /** 246 | * Generate Unique Message Id 247 | * TODO: uuid4. 248 | * 249 | * @return string 250 | */ 251 | public static function generateMessageID($partner = null) 252 | { 253 | if ($partner instanceof PartnerInterface) { 254 | $partner = $partner->getAs2Id(); 255 | } 256 | 257 | return date('Y-m-d') 258 | .'-'. 259 | uniqid('', true) 260 | .'@'. 261 | ($partner ? strtolower($partner).'.' : '') 262 | . 263 | str_replace(' ', '', php_uname('n')); 264 | } 265 | 266 | /** 267 | * Generate random string. 268 | * 269 | * @param int $length 270 | * @param string $charList 271 | * 272 | * @return string 273 | * @throws \Exception 274 | */ 275 | public static function random($length = 10, $charList = '0-9a-z') 276 | { 277 | /** @noinspection CallableParameterUseCaseInTypeContextInspection */ 278 | $charList = count_chars( 279 | preg_replace_callback( 280 | '#.-.#', 281 | static function (array $m) { 282 | return implode('', range($m[0][0], $m[0][2])); 283 | }, 284 | $charList 285 | ), 286 | 3 287 | ); 288 | $chLen = \strlen($charList); 289 | 290 | if ($length < 1) { 291 | throw new \InvalidArgumentException('Length must be greater than zero.'); 292 | } 293 | 294 | if ($chLen < 2) { 295 | throw new \InvalidArgumentException('Character list must contain as least two chars.'); 296 | } 297 | 298 | $res = ''; 299 | for ($i = 0; $i < $length; $i++) { 300 | $res .= $charList[random_int(0, $chLen - 1)]; 301 | } 302 | 303 | return $res; 304 | } 305 | 306 | /** 307 | * Checks if the string is valid for UTF-8 encoding. 308 | * 309 | * @param string $s 310 | * 311 | * @return bool 312 | */ 313 | public static function checkEncoding($s) 314 | { 315 | return $s === self::fixEncoding($s); 316 | } 317 | 318 | /** 319 | * Removes invalid code unit sequences from UTF-8 string. 320 | * 321 | * @param string $s 322 | * 323 | * @return bool 324 | */ 325 | public static function fixEncoding($s) 326 | { 327 | // removes xD800-xDFFF, x110000 and higher 328 | return htmlspecialchars_decode(htmlspecialchars($s, ENT_NOQUOTES | ENT_IGNORE, 'UTF-8'), ENT_NOQUOTES); 329 | } 330 | 331 | /** 332 | * Verify if the content is binary. 333 | */ 334 | public static function isBinary($str): bool 335 | { 336 | $str = str_ireplace(["\t", "\n", "\r"], ['', '', ''], $str); 337 | 338 | return \is_string($str) && ctype_print($str) === false; 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /tests/Mock/DataObject.php: -------------------------------------------------------------------------------- 1 | _data = $data; 30 | } 31 | 32 | /** 33 | * Set/Get attribute wrapper. 34 | * 35 | * @param string $method 36 | * @param array $args 37 | * 38 | * @throws \Exception 39 | */ 40 | public function __call($method, $args) 41 | { 42 | switch (substr($method, 0, 3)) { 43 | case 'get': 44 | $key = $this->_underscore(substr($method, 3)); 45 | $index = $args[0] ?: null; 46 | 47 | return $this->getData($key, $index); 48 | 49 | case 'set': 50 | $key = $this->_underscore(substr($method, 3)); 51 | $value = $args[0] ?: null; 52 | 53 | return $this->setData($key, $value); 54 | 55 | case 'uns': 56 | $key = $this->_underscore(substr($method, 3)); 57 | 58 | return $this->unsetData($key); 59 | 60 | case 'has': 61 | $key = $this->_underscore(substr($method, 3)); 62 | 63 | return isset($this->_data[$key]); 64 | } 65 | 66 | throw new \Exception(sprintf('Invalid method %s::%s', static::class, $method)); 67 | } 68 | 69 | /** 70 | * Add data to the object. 71 | * 72 | * Retains previous data in the object. 73 | * 74 | * @return $this 75 | */ 76 | public function addData(array $arr) 77 | { 78 | foreach ($arr as $index => $value) { 79 | $this->setData($index, $value); 80 | } 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Overwrite data in the object. 87 | * 88 | * The $key parameter can be string or array. 89 | * If $key is string, the attribute value will be overwritten by $value 90 | * 91 | * If $key is an array, it will overwrite all the data in the object. 92 | * 93 | * @param array|string $key 94 | * 95 | * @return $this 96 | */ 97 | public function setData($key, $value = null) 98 | { 99 | if ($key === (array) $key) { 100 | $this->_data = $key; 101 | } else { 102 | $this->_data[$key] = $value; 103 | } 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Unset data from the object. 110 | * 111 | * @param array|string|null $key 112 | * 113 | * @return $this 114 | */ 115 | public function unsetData($key = null) 116 | { 117 | if (null === $key) { 118 | $this->setData([]); 119 | } elseif (\is_string($key)) { 120 | if (isset($this->_data[$key]) || \array_key_exists($key, $this->_data)) { 121 | unset($this->_data[$key]); 122 | } 123 | } elseif ($key === (array) $key) { 124 | foreach ($key as $element) { 125 | $this->unsetData($element); 126 | } 127 | } 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Object data getter. 134 | * 135 | * If $key is not defined will return all the data as an array. 136 | * Otherwise it will return value of the element specified by $key. 137 | * It is possible to use keys like a/b/c for access nested array data 138 | * 139 | * If $index is specified it will assume that attribute data is an array 140 | * and retrieve corresponding member. If data is the string - it will be explode 141 | * by new line character and converted to array. 142 | * 143 | * @param string $key 144 | * @param int|string $index 145 | */ 146 | public function getData($key = '', $index = null) 147 | { 148 | if ('' === $key) { 149 | return $this->_data; 150 | } 151 | // process a/b/c key as ['a']['b']['c'] 152 | if (strpos($key, '/')) { 153 | $data = $this->getDataByPath($key); 154 | } else { 155 | $data = $this->_getData($key); 156 | } 157 | if (null !== $index) { 158 | if ($data === (array) $data) { 159 | $data = $data[$index] ?: null; 160 | } elseif (\is_string($data)) { 161 | $data = explode(PHP_EOL, $data); 162 | $data = $data[$index] ?: null; 163 | } elseif ($data instanceof static) { 164 | $data = $data->getData($index); 165 | } else { 166 | $data = null; 167 | } 168 | } 169 | 170 | return $data; 171 | } 172 | 173 | /** 174 | * Get object data by path. 175 | * 176 | * Method consider the path as chain of keys: a/b/c => ['a']['b']['c'] 177 | * 178 | * @param string $path 179 | */ 180 | public function getDataByPath($path) 181 | { 182 | $keys = explode('/', $path); 183 | $data = $this->_data; 184 | foreach ($keys as $key) { 185 | if ((array) $data === $data && isset($data[$key])) { 186 | $data = $data[$key]; 187 | } elseif ($data instanceof static) { 188 | $data = $data->getDataByKey($key); 189 | } else { 190 | return null; 191 | } 192 | } 193 | 194 | return $data; 195 | } 196 | 197 | /** 198 | * Get object data by particular key. 199 | * 200 | * @param string $key 201 | */ 202 | public function getDataByKey($key) 203 | { 204 | return $this->_getData($key); 205 | } 206 | 207 | /** 208 | * Get object data by key with calling getter method. 209 | * 210 | * @param string $key 211 | */ 212 | public function getDataUsingMethod($key, $args = null) 213 | { 214 | $method = 'get'.str_replace(' ', '', ucwords(str_replace('_', ' ', $key))); 215 | 216 | return $this->{$method}($args); 217 | } 218 | 219 | /** 220 | * If $key is empty, checks whether there's any data in the object 221 | * Otherwise checks if the specified attribute is set. 222 | * 223 | * @param string $key 224 | * 225 | * @return bool 226 | */ 227 | public function hasData($key = '') 228 | { 229 | if (empty($key) || ! \is_string($key)) { 230 | return ! empty($this->_data); 231 | } 232 | 233 | return \array_key_exists($key, $this->_data); 234 | } 235 | 236 | /** 237 | * Implementation of \ArrayAccess::offsetSet(). 238 | * 239 | * @param string $offset 240 | * 241 | * @see http://www.php.net/manual/en/arrayaccess.offsetset.php 242 | */ 243 | public function offsetSet($offset, $value): void 244 | { 245 | $this->_data[$offset] = $value; 246 | } 247 | 248 | /** 249 | * Implementation of \ArrayAccess::offsetExists(). 250 | * 251 | * @param string $offset 252 | * 253 | * @return bool 254 | * 255 | * @see http://www.php.net/manual/en/arrayaccess.offsetexists.php 256 | */ 257 | public function offsetExists($offset) 258 | { 259 | return isset($this->_data[$offset]) || \array_key_exists($offset, $this->_data); 260 | } 261 | 262 | /** 263 | * Implementation of \ArrayAccess::offsetUnset(). 264 | * 265 | * @param string $offset 266 | * 267 | * @see http://www.php.net/manual/en/arrayaccess.offsetunset.php 268 | */ 269 | public function offsetUnset($offset): void 270 | { 271 | unset($this->_data[$offset]); 272 | } 273 | 274 | /** 275 | * Implementation of \ArrayAccess::offsetGet(). 276 | * 277 | * @param string $offset 278 | * 279 | * @see http://www.php.net/manual/en/arrayaccess.offsetget.php 280 | */ 281 | public function offsetGet($offset) 282 | { 283 | if (isset($this->_data[$offset])) { 284 | return $this->_data[$offset]; 285 | } 286 | 287 | return null; 288 | } 289 | 290 | /** 291 | * Get value from _data array without parse key. 292 | * 293 | * @param string $key 294 | */ 295 | protected function _getData($key) 296 | { 297 | if (isset($this->_data[$key])) { 298 | return $this->_data[$key]; 299 | } 300 | 301 | return null; 302 | } 303 | 304 | /** 305 | * Converts field names for setters and getters. 306 | * 307 | * $this->setMyField($value) === $this->setData('my_field', $value) 308 | * Uses cache to eliminate unnecessary preg_replace 309 | * 310 | * @param string $name 311 | * 312 | * @return string 313 | */ 314 | protected function _underscore($name) 315 | { 316 | if (isset(self::$_underscoreCache[$name])) { 317 | return self::$_underscoreCache[$name]; 318 | } 319 | $result = strtolower(trim(preg_replace('/([A-Z]|[0-9]+)/', '_$1', $name), '_')); 320 | self::$_underscoreCache[$name] = $result; 321 | 322 | return $result; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /tests/Mock/Message.php: -------------------------------------------------------------------------------- 1 | getData('id'); 18 | } 19 | 20 | /** 21 | * @param string $id 22 | * 23 | * @return $this 24 | */ 25 | public function setMessageId($id) 26 | { 27 | return $this->setData('id', $id); 28 | } 29 | 30 | /** 31 | * @param string $dir 32 | * 33 | * @return $this 34 | */ 35 | public function setDirection($dir) 36 | { 37 | return $this->setData('direction', $dir); 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getDirection() 44 | { 45 | return $this->getData('direction'); 46 | } 47 | 48 | /** 49 | * @param string $id 50 | * 51 | * @return $this 52 | */ 53 | public function setSenderId($id) 54 | { 55 | return $this->setData('sender_id', $id); 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getSenderId() 62 | { 63 | return $this->getData('sender_id'); 64 | } 65 | 66 | /** 67 | * @return PartnerInterface 68 | */ 69 | public function getSender() 70 | { 71 | return $this->getData('sender'); 72 | } 73 | 74 | /** 75 | * @return $this 76 | */ 77 | public function setSender(PartnerInterface $partner) 78 | { 79 | $this->setSenderId($partner->getAs2Id()); 80 | 81 | return $this->setData('sender', $partner); 82 | } 83 | 84 | /** 85 | * @param string $id 86 | * 87 | * @return $this 88 | */ 89 | public function setReceiverId($id) 90 | { 91 | return $this->setData('receiver_id', $id); 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | public function getReceiverId() 98 | { 99 | return $this->getData('receiver_id'); 100 | } 101 | 102 | /** 103 | * @return PartnerInterface 104 | */ 105 | public function getReceiver() 106 | { 107 | return $this->getData('receiver'); 108 | } 109 | 110 | /** 111 | * @return $this 112 | */ 113 | public function setReceiver(PartnerInterface $partner) 114 | { 115 | $this->setReceiverId($partner->getAs2Id()); 116 | 117 | return $this->setData('receiver', $partner); 118 | } 119 | 120 | /** 121 | * @return string 122 | */ 123 | public function getHeaders() 124 | { 125 | return $this->getData('headers'); 126 | } 127 | 128 | /** 129 | * @param string $headers 130 | * 131 | * @return $this 132 | */ 133 | public function setHeaders($headers) 134 | { 135 | return $this->setData('headers', $headers); 136 | } 137 | 138 | /** 139 | * @return string 140 | */ 141 | public function getPayload() 142 | { 143 | return $this->getData('payload'); 144 | } 145 | 146 | /** 147 | * @param string $payload 148 | * 149 | * @return $this 150 | */ 151 | public function setPayload($payload) 152 | { 153 | return $this->setData('payload', $payload); 154 | } 155 | 156 | /** 157 | * @return string 158 | */ 159 | public function getStatus() 160 | { 161 | return $this->getData('status'); 162 | } 163 | 164 | /** 165 | * @param string $status 166 | * 167 | * @return $this 168 | */ 169 | public function setStatus($status) 170 | { 171 | return $this->setData('status', $status); 172 | } 173 | 174 | /** 175 | * @return string 176 | */ 177 | public function getStatusMsg() 178 | { 179 | return $this->getData('status_msg'); 180 | } 181 | 182 | /** 183 | * @param string $msg 184 | * 185 | * @return $this 186 | */ 187 | public function setStatusMsg($msg) 188 | { 189 | return $this->setData('status_msg', $msg); 190 | } 191 | 192 | /** 193 | * @return string 194 | */ 195 | public function getMdnMode() 196 | { 197 | return $this->getData('mdn_mode'); 198 | } 199 | 200 | /** 201 | * @param string $status 202 | * 203 | * @return $this 204 | */ 205 | public function setMdnMode($status) 206 | { 207 | return $this->setData('mdn_mode', $status); 208 | } 209 | 210 | /** 211 | * @return string 212 | */ 213 | public function getMdnStatus() 214 | { 215 | return $this->getData('mdn_status'); 216 | } 217 | 218 | /** 219 | * @param string $status 220 | * 221 | * @return $this 222 | */ 223 | public function setMdnStatus($status) 224 | { 225 | return $this->setData('mdn_status', $status); 226 | } 227 | 228 | /** 229 | * @return string 230 | */ 231 | public function getMdnPayload() 232 | { 233 | return $this->getData('mdn'); 234 | } 235 | 236 | /** 237 | * @return $this 238 | */ 239 | public function setMdnPayload($mdn) 240 | { 241 | return $this->setData('mdn', $mdn); 242 | } 243 | 244 | /** 245 | * @return string 246 | */ 247 | public function getMic() 248 | { 249 | return $this->getData('mic'); 250 | } 251 | 252 | /** 253 | * @param string $mic 254 | * 255 | * @return $this 256 | */ 257 | public function setMic($mic) 258 | { 259 | return $this->setData('mic', $mic); 260 | } 261 | 262 | /** 263 | * @return bool 264 | */ 265 | public function getSigned() 266 | { 267 | return $this->getData('signed'); 268 | } 269 | 270 | /** 271 | * @param bool $val 272 | * 273 | * @return $this 274 | */ 275 | public function setSigned($val = true) 276 | { 277 | return $this->setData('signed', $val); 278 | } 279 | 280 | /** 281 | * @return bool 282 | */ 283 | public function getEncrypted() 284 | { 285 | return $this->getData('encrypted'); 286 | } 287 | 288 | /** 289 | * @param bool $val 290 | * 291 | * @return $this 292 | */ 293 | public function setEncrypted($val = true) 294 | { 295 | return $this->setData('encrypted', $val); 296 | } 297 | 298 | /** 299 | * @return bool 300 | */ 301 | public function getCompressed() 302 | { 303 | return $this->getData('compressed'); 304 | } 305 | 306 | /** 307 | * @param bool $val 308 | * 309 | * @return $this 310 | */ 311 | public function setCompressed($val = true) 312 | { 313 | return $this->setData('compressed', $val); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /tests/Mock/MessageRepository.php: -------------------------------------------------------------------------------- 1 | getData('id'); 17 | } 18 | 19 | /** 20 | * @return string 21 | */ 22 | public function getEmail() 23 | { 24 | return $this->getData('email'); 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getTargetUrl() 31 | { 32 | return $this->getData('target_url'); 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getContentType() 39 | { 40 | return $this->getData('content_type'); 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getSubject() 47 | { 48 | return $this->getData('subject'); 49 | } 50 | 51 | /** 52 | * @return string|null 53 | */ 54 | public function getAuthMethod() 55 | { 56 | return $this->getData('auth'); 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getAuthUser() 63 | { 64 | return $this->getData('auth_user'); 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getAuthPassword() 71 | { 72 | return $this->getData('auth_password'); 73 | } 74 | 75 | /** 76 | * @return string|null 77 | */ 78 | public function getSignatureAlgorithm() 79 | { 80 | return $this->getData('signature_algorithm'); 81 | } 82 | 83 | /** 84 | * @return string|null 85 | */ 86 | public function getEncryptionAlgorithm() 87 | { 88 | return $this->getData('encryption_algorithm'); 89 | } 90 | 91 | /** 92 | * @return string 93 | */ 94 | public function getCertificate() 95 | { 96 | return $this->getData('certificate'); 97 | } 98 | 99 | /** 100 | * @return string 101 | */ 102 | public function getPrivateKey() 103 | { 104 | return $this->getData('private_key'); 105 | } 106 | 107 | /** 108 | * @return string 109 | */ 110 | public function getPrivateKeyPassPhrase() 111 | { 112 | // TODO: Implement getPrivateKeyPassPhrase() method. 113 | return $this->getData('private_key_pass_phrase'); 114 | } 115 | 116 | /** 117 | * @return string [null, zlib, deflate] 118 | */ 119 | public function getCompressionType() 120 | { 121 | return $this->getData('compression'); 122 | } 123 | 124 | /** 125 | * @return string [null, sync, async] 126 | */ 127 | public function getMdnMode() 128 | { 129 | return $this->getData('mdn_mode'); 130 | } 131 | 132 | /** 133 | * @return string (Example: signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, SHA256) 134 | */ 135 | public function getMdnOptions() 136 | { 137 | return $this->getData('mdn_options'); 138 | } 139 | 140 | /** 141 | * @return string (Example: Your requested MDN response from $receiver.as2_id$) 142 | */ 143 | public function getMdnSubject() 144 | { 145 | return $this->getData('mdn_subject'); 146 | } 147 | 148 | /** 149 | * @return string 150 | */ 151 | public function getContentTransferEncoding() 152 | { 153 | return $this->getData('content_transfer_encoding'); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tests/Mock/PartnerRepository.php: -------------------------------------------------------------------------------- 1 | partners = [ 18 | [ 19 | 'id' => 'A', 20 | // 'target_url' => 'php://memory', 21 | 'private_key' => $pkey, 22 | 'certificate' => $cert, 23 | 'content_type' => 'application/EDI-Consent', 24 | 'compression' => true, 25 | 'signature_algorithm' => 'sha256', 26 | 'encryption_algorithm' => '3des', 27 | 'content_transfer_encoding' => 'binary', 28 | 'mdn_mode' => PartnerInterface::MDN_MODE_SYNC, 29 | 'mdn_options' => 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha256', 30 | ], 31 | [ 32 | 'id' => 'B', 33 | // 'target_url' => 'php://memory', 34 | 'private_key' => $pkey, 35 | 'certificate' => $cert, 36 | 'content_type' => 'application/EDI-Consent', 37 | 'compression' => true, 38 | 'signature_algorithm' => 'sha256', 39 | 'encryption_algorithm' => '3des', 40 | 'content_transfer_encoding' => 'binary', 41 | 'mdn_mode' => PartnerInterface::MDN_MODE_SYNC, 42 | 'mdn_options' => 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha256', 43 | ], 44 | ]; 45 | } 46 | 47 | public function findPartnerById($id) 48 | { 49 | foreach ($this->partners as $partner) { 50 | if ($id === $partner['id']) { 51 | return new Partner($partner); 52 | } 53 | } 54 | 55 | throw new \RuntimeException(sprintf('Unknown partner `%s`.', $id)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | partnerRepository = new PartnerRepository(); 33 | $this->messageRepository = new MessageRepository(); 34 | $this->management = new Management(); 35 | } 36 | 37 | protected function loadFixture($name) 38 | { 39 | $res = file_get_contents(__DIR__.\DIRECTORY_SEPARATOR.'fixtures'.\DIRECTORY_SEPARATOR.$name); 40 | 41 | if ($res === false) { 42 | throw new \RuntimeException("Failed to load fixture $name"); 43 | } 44 | 45 | return $res; 46 | } 47 | 48 | protected function getCerts($name = 'server') 49 | { 50 | $cert = $this->loadFixture($name.'.crt'); 51 | $pkey = $this->loadFixture($name.'.pem'); 52 | 53 | return [ 54 | "cert" => $cert, 55 | "pkey" => $pkey, 56 | ]; 57 | } 58 | 59 | protected function initMessage($file = 'test.edi') 60 | { 61 | $body = $this->loadFixture($file); 62 | 63 | return "Content-type: application/EDI-Consent\r\nContent-Transfer-Encoding: binary\r\nContent-Disposition: attachment; filename=payload.txt\r\n\r\n{$body}"; 64 | // return new MimePart( 65 | // [ 66 | // 'Content-type' => 'application/EDI-Consent', 67 | // 'Content-Transfer-Encoding' => 'binary', 68 | // 'Content-Disposition' => 'attachment; filename=payload.txt', 69 | // ], $this->loadFixture($file) 70 | // ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Unit/Asn1HelperTest.php: -------------------------------------------------------------------------------- 1 | ASN1::TYPE_SET, 28 | 'min' => 1, 29 | 'max' => -1, 30 | 'children' => ASN1Helper::getSignerInfoMap(), 31 | ]); 32 | 33 | self::assertSame($payload[0]['version'], '1'); 34 | self::assertSame($payload[0]['signatureAlgorithm']['algorithm'], '1.2.840.113549.1.1.1'); 35 | } 36 | 37 | public function testSignedData(): void 38 | { 39 | $data = 'MIIGsAYJKoZIhvcNAQcCoIIGoTCCBp0CAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0BBwGgggPC 40 | MIIDvjCCAqagAwIBAgIEXXmKiDANBgkqhkiG9w0BAQUFADCBljEhMB8GCSqGSIb3DQEJARYSdmsudGlh 41 | bW9AZ21haWwuY29tMQswCQYDVQQGEwJ1azETMBEGA1UECAwKU3RhdGUgbmFtZTEQMA4GA1UEBwwHVWty 42 | YWluZTEPMA0GA1UECgwGcGhwYXMyMQ8wDQYDVQQLDAZwaHBhczIxGzAZBgNVBAMMEnBocGFzMi5leGFt 43 | cGxlLmNvbTAeFw0xOTA5MTIwMDAwMDhaFw0yMDA5MTEwMDAwMDhaMIGWMSEwHwYJKoZIhvcNAQkBFhJ2 44 | ay50aWFtb0BnbWFpbC5jb20xCzAJBgNVBAYTAnVrMRMwEQYDVQQIDApTdGF0ZSBuYW1lMRAwDgYDVQQH 45 | DAdVa3JhaW5lMQ8wDQYDVQQKDAZwaHBhczIxDzANBgNVBAsMBnBocGFzMjEbMBkGA1UEAwwScGhwYXMy 46 | LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl81EOzilspTHNbf2kNSL 47 | hc36sRvI6PPhhQq85NC+dWpJpDo1JOYEwv38ZXqgs5mNdVBplzMW6Uqg3pEDw4IIkrf+2xk/uC2urR3O 48 | OFO/7DrZhKJGYA6xOl30L4ms3CiD2/l/73iQ3uL0NVEjCLYTQQh01Q0q9HQFt0ev/1rNK2BZCg5LMRLN 49 | gQqfgDx49Iqs7EVs3oXmaZmczhskRg9IhP9/NdoTv5L5+fkRwWXO7vtkhbfMUlztmuUTvU14AaIkYd3O 50 | +7pdafykvqLHm4yHE6gc/2Uev+MXklWeUNamTUwk9aVBIqp0Jwcbwkeg5ClnPwPgF7A5l765Ziod9I8S 51 | TQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQEFBQADggEBAIOVAtfT4fmoL5BmVUhF 52 | 4KxcFgM2WNwNWdXfjj/Fnk9h79ruUlvh3vcooD7IbcZvRU2jSJkU1dQZgSOvERuVlR1cmt9yLRalfrVJ 53 | Cy05G2CXl0Ce+9UO6UbXcz+z0LprkeyZ7MrVdaKDdQSJT1u8Kt0w+izv9oDpD70zo7+jp4Y5b7oVhQf2 54 | eckQQaA79GmO08HqCcd0JRyFGXdymPo6AA/Do0x5WsPWt8vQ5BM6lXhbfW1keOe6NFduLGHejrbBNcU3 55 | 9R069YF2RSlNK24q/TPxwZpgdtuTVxQf5bl2SPBB+8gmLHIV1GaD9BSerPf4RubMUIz8rcS3g6dELsyO 56 | 5EQxggKyMIICrgIBATCBnzCBljEhMB8GCSqGSIb3DQEJARYSdmsudGlhbW9AZ21haWwuY29tMQswCQYD 57 | VQQGEwJ1azETMBEGA1UECAwKU3RhdGUgbmFtZTEQMA4GA1UEBwwHVWtyYWluZTEPMA0GA1UECgwGcGhw 58 | YXMyMQ8wDQYDVQQLDAZwaHBhczIxGzAZBgNVBAMMEnBocGFzMi5leGFtcGxlLmNvbQIEXXmKiDANBglg 59 | hkgBZQMEAgEFAKCB5DAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDA2 60 | MjYxNDQ3MjZaMC8GCSqGSIb3DQEJBDEiBCASsBnqQaKfiLg9QNGaR2Qq3NN3u0D2PvszEl7/I6iDEDB5 61 | BgkqhkiG9w0BCQ8xbDBqMAsGCWCGSAFlAwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqG 62 | SIb3DQMHMA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG9w0DAgIB 63 | KDANBgkqhkiG9w0BAQEFAASCAQApCY982Lh/WY8CPP/j7hhwP4Cl56rRhpcv5KeOYC7U7+IcbitPKrbk 64 | 0GKADj1zw6PT+yOpmjXj3LRt8nCdaeHr4V3uNA2KHsvO8QCrRblMMHz7jEKnjXNArRkHjit0cuiCbyjv 65 | dCBji0LaFrF+WlaUtyQypzUXffb5tSLQiZF1TDrhn+MB9DBIRvovAwcyjDJgKwl3hCK799B8ldt0Cp38 66 | tdcGpdQeQRp46eMwp6Gdz9TIlRUliorK4LjodvpgDm33Yuq4aqIaiYZZwYaKVVDBYrf+ZULpi7yClM1s 67 | BVma546vFer4ipotvk8ALTsPqoAH/liE3bzChzs0MP1HjMdQ'; 68 | 69 | $data = Utils::normalizeBase64($data); 70 | 71 | $payload = ASN1Helper::decode($data, ASN1Helper::getSignedDataMap()); 72 | 73 | self::assertSame(ASN1Helper::OID_SIGNED_DATA, $payload['contentType']); 74 | self::assertSame('1', $payload['content']['version']); 75 | self::assertSame(ASN1Helper::OID_SHA256, $payload['content']['digestAlgorithms'][0]['algorithm']); 76 | self::assertSame(ASN1Helper::OID_DATA, $payload['content']['contentInfo']['contentType']); 77 | 78 | self::assertSame( 79 | (string) new BigInteger(1568246408), 80 | (string) $payload['content']['certificates'][0]['tbsCertificate']['serialNumber'] 81 | ); 82 | self::assertSame(ASN1Helper::OID_SHA256, $payload['content']['signers'][0]['digestAlgorithm']['algorithm']); 83 | self::assertSame( 84 | ASN1Helper::OID_RSA_ENCRYPTION, 85 | $payload['content']['signers'][0]['signatureAlgorithm']['algorithm'] 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/Unit/CryptoHelperTest.php: -------------------------------------------------------------------------------- 1 | loadFixture('mic-calculation'); 21 | self::assertSame( 22 | '9IVZAN9QhjQINLzl/tdUvTMhMOSQ+96TjK7brHXQFys=, sha256', 23 | CryptoHelper::calculateMIC($contents, 'sha256') 24 | ); 25 | } 26 | 27 | public function testSign(): void 28 | { 29 | $payload = $this->initMessage(); 30 | $certs = $this->getCerts(); 31 | 32 | $payload = CryptoHelper::sign($payload, $certs['cert'], $certs['pkey']); 33 | 34 | self::assertTrue($payload->isSigned()); 35 | 36 | $hasSignature = false; 37 | foreach ($payload->getParts() as $part) { 38 | if ($part->isPkc7Signature()) { 39 | $hasSignature = true; 40 | 41 | break; 42 | } 43 | } 44 | self::assertTrue($hasSignature); 45 | } 46 | 47 | public function testVerifyBase64(): void 48 | { 49 | $contents = $this->loadFixture('signed-msg.txt'); 50 | $payload = MimePart::fromString($contents); 51 | 52 | $certs = $this->getCerts(); 53 | 54 | self::assertTrue($payload->isSigned()); 55 | self::assertTrue(CryptoHelper::verify($payload, $certs['cert'])); 56 | } 57 | 58 | public function testVerifyBinary(): void 59 | { 60 | $contents = $this->loadFixture('si_signed.mdn'); 61 | $payload = MimePart::fromString($contents); 62 | 63 | $certs = $this->getCerts(); 64 | 65 | self::assertTrue($payload->isSigned()); 66 | self::assertTrue(CryptoHelper::verify($payload, $certs['cert'])); 67 | } 68 | 69 | public function testEncrypt(): void 70 | { 71 | $payload = $this->initMessage(); 72 | $certs = $this->getCerts(); 73 | 74 | $payload = CryptoHelper::encrypt($payload, $certs['cert']); 75 | 76 | self::assertTrue($payload->isEncrypted()); 77 | } 78 | 79 | public function testDecrypt(): void 80 | { 81 | $payload = $this->initMessage(); 82 | $certs = $this->getCerts(); 83 | 84 | $payload = CryptoHelper::encrypt($payload, $certs['cert']); 85 | $payload = CryptoHelper::decrypt($payload, $certs['cert'], $certs['pkey']); 86 | 87 | self::assertSame('application/EDI-Consent', $payload->getHeaderLine('content-type')); 88 | } 89 | 90 | public function testCompress(): void 91 | { 92 | $payload = $this->initMessage(); 93 | $payload = CryptoHelper::compress($payload); 94 | 95 | self::assertSame( 96 | 'compressed-data', 97 | $payload->getParsedHeader('content-type', 0, 'smime-type') 98 | ); 99 | $body = base64_decode($payload->getBodyString(), true); 100 | self::assertSame(0x30, \ord($body[0])); 101 | self::assertSame(0x82, \ord($body[1])); 102 | self::assertSame(0x02, \ord($body[2])); 103 | } 104 | 105 | public function testDecompress(): void 106 | { 107 | $payload = MimePart::fromString($this->loadFixture('si_signed_cmp.msg')); 108 | 109 | self::assertTrue($payload->isSigned()); 110 | 111 | foreach ($payload->getParts() as $part) { 112 | if (! $part->isPkc7Signature()) { 113 | $payload = $part; 114 | } 115 | } 116 | 117 | self::assertTrue($payload->isCompressed()); 118 | 119 | $payload = CryptoHelper::decompress($payload); 120 | 121 | self::assertSame('Application/EDI-X12', $payload->getHeaderLine('content-type')); 122 | self::assertSame(2247, \strlen($payload->getBodyString())); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/Unit/ManagementTest.php: -------------------------------------------------------------------------------- 1 | partnerRepository->findPartnerById($senderId); 24 | $receiver = $this->partnerRepository->findPartnerById($receiverId); 25 | 26 | // Initialize empty message 27 | $message = $this->messageRepository->createMessage(); 28 | $message->setMessageId('test'); 29 | $message->setSender($sender); 30 | $message->setReceiver($receiver); 31 | 32 | $contents = $this->loadFixture('test.edi'); 33 | 34 | // generate message payload 35 | $payload = $this->management->buildMessage($message, $contents); 36 | 37 | self::assertTrue($payload->isEncrypted()); 38 | self::assertSame($senderId, $payload->getHeaderLine('as2-from')); 39 | self::assertSame($receiverId, $payload->getHeaderLine('as2-to')); 40 | } 41 | 42 | public function testProcessMessage(): void 43 | { 44 | $payload = MimePart::fromString($this->loadFixture('phpas2.raw')); 45 | 46 | $sender = $this->partnerRepository->findPartnerById('A'); 47 | $receiver = $this->partnerRepository->findPartnerById('B'); 48 | 49 | $messageId = $payload->getHeaderLine('message-id'); 50 | 51 | $message = $this->messageRepository->createMessage(); 52 | $message->setMessageId($messageId); 53 | $message->setDirection(MessageInterface::DIR_INBOUND); 54 | $message->setStatus(MessageInterface::STATUS_IN_PROCESS); 55 | $message->setSender($sender); 56 | $message->setReceiver($receiver); 57 | $message->setHeaders($payload->getHeaderLines()); 58 | 59 | $processedPayload = $this->management->processMessage($message, $payload); 60 | 61 | self::assertTrue($message->getCompressed()); 62 | self::assertTrue($message->getEncrypted()); 63 | self::assertTrue($message->getSigned()); 64 | self::assertSame($message->getMic(), 'oVDpnrSnpq+V99dXaarQ9HFyRUaFNsp9tdBBSmRhX4s=, sha256'); 65 | self::assertSame(trim((string) $processedPayload), Utils::canonicalize($this->loadFixture('test.edi'))); 66 | } 67 | 68 | public function testProcessMessageBiggerMessage(): void 69 | { 70 | $payload = MimePart::fromString($this->loadFixture('phpas2_new.raw')); 71 | 72 | $sender = $this->partnerRepository->findPartnerById('A'); 73 | $receiver = $this->partnerRepository->findPartnerById('B'); 74 | 75 | $messageId = $payload->getHeaderLine('message-id'); 76 | 77 | $message = $this->messageRepository->createMessage(); 78 | $message->setMessageId($messageId); 79 | $message->setDirection(MessageInterface::DIR_INBOUND); 80 | $message->setStatus(MessageInterface::STATUS_IN_PROCESS); 81 | $message->setSender($sender); 82 | $message->setReceiver($receiver); 83 | $message->setHeaders($payload->getHeaderLines()); 84 | 85 | $processedPayload = $this->management->processMessage($message, $payload); 86 | 87 | self::assertTrue($message->getEncrypted()); 88 | self::assertTrue($message->getSigned()); 89 | self::assertSame($message->getMic(), 'EuCiZZoJY+PJlJYsvwbqRcYGPNeHfVAvU7f6q2AxGLg=, sha256'); 90 | self::assertSame(trim($processedPayload), Utils::canonicalize($this->loadFixture('test.xml'))); 91 | } 92 | 93 | public function testSendMessage(): void 94 | { 95 | $senderId = 'A'; 96 | $receiverId = 'B'; 97 | 98 | $sender = $this->partnerRepository->findPartnerById($senderId); 99 | $receiver = $this->partnerRepository->findPartnerById($receiverId); 100 | 101 | $messageId = Utils::generateMessageID($sender); 102 | 103 | // Initialize empty message 104 | $message = $this->messageRepository->createMessage(); 105 | $message->setMessageId($messageId); 106 | $message->setSender($sender); 107 | $message->setReceiver($receiver); 108 | 109 | self::assertEmpty($message->getStatus()); 110 | 111 | $payload = $this->management->buildMessage($message, $this->loadFixture('test.edi')); 112 | 113 | self::assertSame(MessageInterface::STATUS_PENDING, $message->getStatus()); 114 | 115 | $response = $this->management->sendMessage($message, $payload); 116 | 117 | self::assertFalse($response); 118 | self::assertSame(MessageInterface::STATUS_ERROR, $message->getStatus()); 119 | } 120 | 121 | public function testBuildMdn(): void 122 | { 123 | $sender = $this->partnerRepository->findPartnerById('A'); 124 | $receiver = $this->partnerRepository->findPartnerById('B'); 125 | 126 | // Initialize empty message 127 | $message = $this->messageRepository->createMessage(); 128 | $message->setMessageId('test'); 129 | $message->setSender($sender); 130 | $message->setReceiver($receiver); 131 | 132 | $report = $this->management->buildMdn($message); 133 | 134 | self::assertEmpty($report->getBodyString()); 135 | 136 | $message->setHeaders('disposition-notification-to: test@example.com'); 137 | 138 | $report = $this->management->buildMdn($message, 'custom', 'error'); 139 | 140 | self::assertTrue($report->isReport()); 141 | self::assertSame($report->getHeaderLine('as2-to'), $sender->getAs2Id()); 142 | self::assertSame($report->getHeaderLine('as2-from'), $receiver->getAs2Id()); 143 | self::assertSame('custom', trim($report->getPart(0)->getBodyString())); 144 | 145 | $headers = new MimePart( 146 | Utils::parseHeaders($report->getPart(1)->getBodyString()) 147 | ); 148 | 149 | self::assertSame('', $headers->getHeaderLine('Original-Message-ID')); 150 | self::assertSame('rfc822; B', $headers->getHeaderLine('Original-Recipient')); 151 | self::assertSame('rfc822; B', $headers->getHeaderLine('Final-Recipient')); 152 | self::assertSame( 153 | 'automatic-action/MDN-sent-automatically; processed/error: error', 154 | $headers->getHeaderLine('Disposition') 155 | ); 156 | 157 | self::assertSame(MessageInterface::MDN_STATUS_SENT, $message->getMdnStatus()); 158 | self::assertSame(PartnerInterface::MDN_MODE_SYNC, $message->getMdnMode()); 159 | 160 | // test signed 161 | 162 | $message->setHeaders("disposition-notification-to: test@example.com\ndisposition-notification-options: signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha256"); 163 | 164 | $report = $this->management->buildMdn($message, 'custom', 'error'); 165 | 166 | self::assertTrue($report->isSigned()); 167 | } 168 | 169 | // public function testSendMdn() 170 | // { 171 | // $sender = $this->partnerRepository->findPartnerById('A'); 172 | // $sender->setTargetUrl('http://localhost'); 173 | // 174 | // $receiver = $this->partnerRepository->findPartnerById('B'); 175 | // 176 | // $messageId = Utils::generateMessageID($sender); 177 | // 178 | // // Initialize empty message 179 | // $message = $this->messageRepository->createMessage(); 180 | // $message->setMessageId($messageId); 181 | // $message->setSender($sender); 182 | // $message->setReceiver($receiver); 183 | // $message->setMdnMode(PartnerInterface::MDN_MODE_ASYNC); 184 | // $message->setMdnPayload($this->loadFixture('si_signed.mdn')); 185 | // 186 | // $response = $this->management->sendMdn($message); 187 | // 188 | // self::assertTrue(true); 189 | // } 190 | } 191 | -------------------------------------------------------------------------------- /tests/Unit/MimePartTest.php: -------------------------------------------------------------------------------- 1 | $cType, 28 | ] 29 | ); 30 | self::assertTrue($mimePart->isPkc7Mime()); 31 | self::assertTrue($mimePart->isEncrypted()); 32 | } 33 | } 34 | 35 | public function testIsCompressed(): void 36 | { 37 | $cTypes = [ 38 | 'application/pkcs7-mime; name="smime.p7m"; smime-type=compressed-data', 39 | 'application/x-pkcs7-mime; name="smime.p7m"; smime-type=compressed-data', 40 | ]; 41 | 42 | foreach ($cTypes as $cType) { 43 | $mimePart = new MimePart( 44 | [ 45 | 'Content-Type' => $cType, 46 | ] 47 | ); 48 | self::assertTrue($mimePart->isCompressed()); 49 | } 50 | } 51 | 52 | public function testIsSigned(): void 53 | { 54 | $mimePart = new MimePart( 55 | [ 56 | 'Content-Type' => 'multipart/signed;protocol="application/pkcs7-signature";micalg=sha1;boundary="_=A=_"', 57 | ] 58 | ); 59 | self::assertTrue($mimePart->isSigned()); 60 | } 61 | 62 | public function testIsPkc7Signature(): void 63 | { 64 | $mimePart = new MimePart( 65 | [ 66 | 'Content-Type' => 'Application/pkcs7-signature;name=EDIINTSIG.p7s', 67 | ] 68 | ); 69 | self::assertTrue($mimePart->isPkc7Signature()); 70 | } 71 | 72 | public function testIsReport(): void 73 | { 74 | $mimePart = new MimePart( 75 | [ 76 | 'Content-Type' => 'multipart/report;Report-Type=disposition-notification;', 77 | ] 78 | ); 79 | self::assertTrue($mimePart->isReport()); 80 | } 81 | 82 | public function testIsMultipart(): void 83 | { 84 | $boundary = '_=A=_'; 85 | 86 | $mimePart = new MimePart( 87 | [ 88 | 'Content-Type' => 'multipart/mixed; boundary="' . $boundary . '"', 89 | ] 90 | ); 91 | $mimePart->addPart('1'); 92 | $mimePart->addPart('2'); 93 | 94 | self::assertTrue($mimePart->isMultiPart()); 95 | self::assertSame($boundary, $mimePart->getParsedHeader('content-type', 0, 'boundary')); 96 | 97 | $mimePart = new MimePart( 98 | [ 99 | 'Content-Type' => 'multipart/mixed; boundary="' . $boundary . '"', 100 | ] 101 | ); 102 | $mimePart->addPart('1'); 103 | $mimePart->addPart('2'); 104 | 105 | self::assertTrue($mimePart->isMultiPart()); 106 | self::assertSame($boundary, $mimePart->getParsedHeader('content-type', 0, 'boundary')); 107 | } 108 | 109 | public function testBody(): void 110 | { 111 | $mimePart = MimePart::fromString("content-type:text/plain;\n\ntest"); 112 | self::assertSame('test', $mimePart->getBodyString()); 113 | 114 | $mimePart->setBody('test2'); 115 | self::assertSame('test2', $mimePart->getBodyString()); 116 | 117 | $mimePart = MimePart::fromString("content-type:multipart/mixed;\r\n\r\ntest"); 118 | self::assertSame('test', $mimePart->getBodyString()); 119 | 120 | $mimePart->setBody(new MimePart([], '1')); 121 | self::assertSame(1, $mimePart->getCountParts()); 122 | 123 | $mimePart->setBody(['2', '3']); 124 | self::assertSame(3, $mimePart->getCountParts()); 125 | } 126 | 127 | public function testMultipart(): void 128 | { 129 | $mime = MimePart::fromString($this->loadFixture('signed-msg.txt')); 130 | 131 | self::assertStringStartsWith('multipart/signed', $mime->getHeaderLine('content-type')); 132 | self::assertSame(2, $mime->getCountParts()); 133 | 134 | self::assertStringStartsWith('application/pkcs7-signature', $mime->getPart(1)->getHeaderLine('content-type')); 135 | self::assertSame('application/EDI-Consent', $mime->getPart(0)->getHeaderLine('content-type')); 136 | self::assertSame('binary', $mime->getPart(0)->getHeaderLine('Content-Transfer-Encoding')); 137 | self::assertStringStartsWith('UNB+UNOA', $mime->getPart(0)->getBodyString()); 138 | } 139 | 140 | public function testBodyWithoutHeaders(): void 141 | { 142 | $res = MimePart::fromString($this->loadFixture('test.edi')); 143 | 144 | self::assertEmpty($res->getHeaders()); 145 | self::assertStringStartsWith('UNB+UNOA', $res->getBodyString()); 146 | } 147 | 148 | public function testCreateIfBinaryPartNotBinary(): void 149 | { 150 | $contents = $this->loadFixture('signed-msg.txt'); 151 | $payload = MimePart::fromString($contents); 152 | 153 | self::assertNull(MimePart::createIfBinaryPart($payload)); 154 | } 155 | 156 | public function testCreateIfBinaryPartBinary(): void 157 | { 158 | $contents = $this->loadFixture('si_signed.mdn'); 159 | $payload = MimePart::fromString($contents); 160 | 161 | self::assertInstanceOf(MimePart::class, MimePart::createIfBinaryPart($payload)); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /tests/Unit/ServerTest.php: -------------------------------------------------------------------------------- 1 | server = new Server( 28 | $this->management, 29 | $this->partnerRepository, 30 | $this->messageRepository 31 | ); 32 | } 33 | 34 | public function testGet(): void 35 | { 36 | $response = $this->server->execute(new ServerRequest('GET', 'http:://localhost')); 37 | self::assertStringContainsString('To submit an AS2 message', $response->getBody()->getContents()); 38 | } 39 | 40 | public function testSignedMdn(): void 41 | { 42 | $message = $this->loadFixture('phpas2.raw'); 43 | $payload = Utils::parseMessage($message); 44 | 45 | $response = $this->server->execute( 46 | new ServerRequest( 47 | 'POST', 48 | 'http:://localhost', 49 | $payload['headers'], 50 | $payload['body'], 51 | '1.1', 52 | [ 53 | 'REMOTE_ADDR' => '127.0.0.1', 54 | ] 55 | ) 56 | ); 57 | 58 | $headers = $response->getHeaders(); 59 | $body = $response->getBody()->getContents(); 60 | 61 | $mime = new MimePart($headers, $body); 62 | 63 | self::assertTrue($mime->isSigned()); 64 | self::assertSame(2, $mime->getCountParts()); 65 | 66 | $report = $mime->getPart(0); 67 | self::assertTrue($report->isReport()); 68 | 69 | $content = $report->getPart(0); 70 | self::assertSame("Your message was successfully received and processed.\r\n", $content->getBodyString()); 71 | self::assertSame('7bit', $content->getHeaderLine('Content-Transfer-Encoding')); 72 | 73 | $disposition = $report->getPart(1); 74 | self::assertStringContainsString('Original-Recipient: rfc822; B', $disposition->getBodyString()); 75 | self::assertStringContainsString('Original-Recipient: rfc822; B', $disposition->getBodyString()); 76 | self::assertStringContainsString('Final-Recipient: rfc822; B', $disposition->getBodyString()); 77 | self::assertStringContainsString('Original-Message-ID: ', $disposition->getBodyString()); 78 | self::assertStringContainsString( 79 | 'Disposition: automatic-action/MDN-sent-automatically; processed', 80 | $disposition->getBodyString() 81 | ); 82 | self::assertStringContainsString('oVDpnrSnpq+V99dXaarQ9HFyRUaFNsp9tdBBSmRhX4s=, sha256', 83 | $disposition->getBodyString()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | :14+:14+140407:0910+5++++1+EANCOM' 10 | UNH+1+ORDERS:D:96A:UN:EAN008' 11 | BGM+220+1AA1TEST+9' 12 | DTM+137:20140407:102' 13 | DTM+63:20140421:102' 14 | DTM+64:20140414:102' 15 | RFF+ADE:1234' 16 | RFF+PD:1704' 17 | NAD+BY+5450534000024::9' 18 | NAD+SU+::9' 19 | NAD+DP+5450534000109::9+++++++GB' 20 | NAD+IV+5450534000055::9++AMAZON EU SARL:5 RUE PLAETIS LUXEMBOURG+CO PO BOX 4558+SLOUGH++SL1 0TX+GB' 21 | RFF+VA:GB727255821' 22 | CUX+2:EUR:9' 23 | LIN+1++9783898307529:EN' 24 | QTY+21:5' 25 | PRI+AAA:27.5' 26 | LIN+2++390787706322:UP' 27 | QTY+21:1' 28 | PRI+AAA:10.87' 29 | LIN+3' 30 | PIA+5+3899408268X-39:SA' 31 | QTY+21:3' 32 | PRI+AAA:3.85' 33 | UNS+S' 34 | CNT+2:3' 35 | UNT+26+1' 36 | UNZ+1+5' 37 | 38 | ------B8CF1F8C3A8A8D8C8396C60B8524BD77 39 | Content-Transfer-Encoding: base64 40 | Content-Disposition: attachment; filename="smime.p7s" 41 | Content-Type: application/pkcs7-signature; name=smime.p7s; smime-type=signed-data 42 | 43 | MIIGsAYJKoZIhvcNAQcCoIIGoTCCBp0CAQExDzANBglghkgBZQMEAgEFADALBgkq 44 | hkiG9w0BBwGgggPCMIIDvjCCAqagAwIBAgIEXXmKiDANBgkqhkiG9w0BAQUFADCB 45 | ljEhMB8GCSqGSIb3DQEJARYSdmsudGlhbW9AZ21haWwuY29tMQswCQYDVQQGEwJ1 46 | azETMBEGA1UECAwKU3RhdGUgbmFtZTEQMA4GA1UEBwwHVWtyYWluZTEPMA0GA1UE 47 | CgwGcGhwYXMyMQ8wDQYDVQQLDAZwaHBhczIxGzAZBgNVBAMMEnBocGFzMi5leGFt 48 | cGxlLmNvbTAeFw0xOTA5MTIwMDAwMDhaFw0yMDA5MTEwMDAwMDhaMIGWMSEwHwYJ 49 | KoZIhvcNAQkBFhJ2ay50aWFtb0BnbWFpbC5jb20xCzAJBgNVBAYTAnVrMRMwEQYD 50 | VQQIDApTdGF0ZSBuYW1lMRAwDgYDVQQHDAdVa3JhaW5lMQ8wDQYDVQQKDAZwaHBh 51 | czIxDzANBgNVBAsMBnBocGFzMjEbMBkGA1UEAwwScGhwYXMyLmV4YW1wbGUuY29t 52 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl81EOzilspTHNbf2kNSL 53 | hc36sRvI6PPhhQq85NC+dWpJpDo1JOYEwv38ZXqgs5mNdVBplzMW6Uqg3pEDw4II 54 | krf+2xk/uC2urR3OOFO/7DrZhKJGYA6xOl30L4ms3CiD2/l/73iQ3uL0NVEjCLYT 55 | QQh01Q0q9HQFt0ev/1rNK2BZCg5LMRLNgQqfgDx49Iqs7EVs3oXmaZmczhskRg9I 56 | hP9/NdoTv5L5+fkRwWXO7vtkhbfMUlztmuUTvU14AaIkYd3O+7pdafykvqLHm4yH 57 | E6gc/2Uev+MXklWeUNamTUwk9aVBIqp0Jwcbwkeg5ClnPwPgF7A5l765Ziod9I8S 58 | TQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQEFBQADggEBAIOV 59 | AtfT4fmoL5BmVUhF4KxcFgM2WNwNWdXfjj/Fnk9h79ruUlvh3vcooD7IbcZvRU2j 60 | SJkU1dQZgSOvERuVlR1cmt9yLRalfrVJCy05G2CXl0Ce+9UO6UbXcz+z0LprkeyZ 61 | 7MrVdaKDdQSJT1u8Kt0w+izv9oDpD70zo7+jp4Y5b7oVhQf2eckQQaA79GmO08Hq 62 | Ccd0JRyFGXdymPo6AA/Do0x5WsPWt8vQ5BM6lXhbfW1keOe6NFduLGHejrbBNcU3 63 | 9R069YF2RSlNK24q/TPxwZpgdtuTVxQf5bl2SPBB+8gmLHIV1GaD9BSerPf4RubM 64 | UIz8rcS3g6dELsyO5EQxggKyMIICrgIBATCBnzCBljEhMB8GCSqGSIb3DQEJARYS 65 | dmsudGlhbW9AZ21haWwuY29tMQswCQYDVQQGEwJ1azETMBEGA1UECAwKU3RhdGUg 66 | bmFtZTEQMA4GA1UEBwwHVWtyYWluZTEPMA0GA1UECgwGcGhwYXMyMQ8wDQYDVQQL 67 | DAZwaHBhczIxGzAZBgNVBAMMEnBocGFzMi5leGFtcGxlLmNvbQIEXXmKiDANBglg 68 | hkgBZQMEAgEFAKCB5DAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3 69 | DQEJBTEPFw0yMDA2MjYxNDQ3MjZaMC8GCSqGSIb3DQEJBDEiBCASsBnqQaKfiLg9 70 | QNGaR2Qq3NN3u0D2PvszEl7/I6iDEDB5BgkqhkiG9w0BCQ8xbDBqMAsGCWCGSAFl 71 | AwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMA4GCCqG 72 | SIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG9w0DAgIB 73 | KDANBgkqhkiG9w0BAQEFAASCAQApCY982Lh/WY8CPP/j7hhwP4Cl56rRhpcv5KeO 74 | YC7U7+IcbitPKrbk0GKADj1zw6PT+yOpmjXj3LRt8nCdaeHr4V3uNA2KHsvO8QCr 75 | RblMMHz7jEKnjXNArRkHjit0cuiCbyjvdCBji0LaFrF+WlaUtyQypzUXffb5tSLQ 76 | iZF1TDrhn+MB9DBIRvovAwcyjDJgKwl3hCK799B8ldt0Cp38tdcGpdQeQRp46eMw 77 | p6Gdz9TIlRUliorK4LjodvpgDm33Yuq4aqIaiYZZwYaKVVDBYrf+ZULpi7yClM1s 78 | BVma546vFer4ipotvk8ALTsPqoAH/liE3bzChzs0MP1HjMdQ 79 | 80 | ------B8CF1F8C3A8A8D8C8396C60B8524BD77-- 81 | -------------------------------------------------------------------------------- /tests/fixtures/signed1.txt: -------------------------------------------------------------------------------- 1 | MIME-Version: 1.0 2 | Content-type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha256; boundary="----=_54e86d8581e0cd7678fd65ee1a7d60d046487f53" 3 | 4 | ------=_54e86d8581e0cd7678fd65ee1a7d60d046487f53 5 | Content-type: application/EDI-Consent 6 | Content-Transfer-Encoding: binary 7 | Content-Disposition: attachment; filename=payload.txt 8 | 9 | UNB+UNOA:2+:14+:14+140407:0910+5++++1+EANCOM' 10 | UNH+1+ORDERS:D:96A:UN:EAN008' 11 | BGM+220+1AA1TEST+9' 12 | DTM+137:20140407:102' 13 | DTM+63:20140421:102' 14 | DTM+64:20140414:102' 15 | RFF+ADE:1234' 16 | RFF+PD:1704' 17 | NAD+BY+5450534000024::9' 18 | NAD+SU+::9' 19 | NAD+DP+5450534000109::9+++++++GB' 20 | NAD+IV+5450534000055::9++AMAZON EU SARL:5 RUE PLAETIS LUXEMBOURG+CO PO BOX 4558+SLOUGH++SL1 0TX+GB' 21 | RFF+VA:GB727255821' 22 | CUX+2:EUR:9' 23 | LIN+1++9783898307529:EN' 24 | QTY+21:5' 25 | PRI+AAA:27.5' 26 | LIN+2++390787706322:UP' 27 | QTY+21:1' 28 | PRI+AAA:10.87' 29 | LIN+3' 30 | PIA+5+3899408268X-39:SA' 31 | QTY+21:3' 32 | PRI+AAA:3.85' 33 | UNS+S' 34 | CNT+2:3' 35 | UNT+26+1' 36 | UNZ+1+5' 37 | 38 | ------=_54e86d8581e0cd7678fd65ee1a7d60d046487f53 39 | Content-Transfer-Encoding: base64 40 | Content-Disposition: attachment; filename="smime.p7s" 41 | Content-Type: application/pkcs7-signature; name=smime.p7s; smime-type=signed-data 42 | 43 | MIIFfgYJKoZIhvcNAQcCoIIFbzCCBWsCAQExDzANBglghkgBZQMEAgEFADALBgkq 44 | hkiG9w0BBwGgggN3MIIDczCCA1+gAwIBAgIEXXmKiDALBglghkgBZQMEAgEwgZYx 45 | ITAfBgkqhkiG9w0BCQEWEnZrLnRpYW1vQGdtYWlsLmNvbTELMAkGA1UEBhMCdWsx 46 | EzARBgNVBAgMClN0YXRlIG5hbWUxEDAOBgNVBAcMB1VrcmFpbmUxDzANBgNVBAoM 47 | BnBocGFzMjEPMA0GA1UECwwGcGhwYXMyMRswGQYDVQQDDBJwaHBhczIuZXhhbXBs 48 | ZS5jb20wHhcNMTkwOTEyMDAwMDA4WhcNMjAwOTExMDAwMDA4WjCBljEhMB8GCSqG 49 | SIb3DQEJARYSdmsudGlhbW9AZ21haWwuY29tMQswCQYDVQQGEwJ1azETMBEGA1UE 50 | CAwKU3RhdGUgbmFtZTEQMA4GA1UEBwwHVWtyYWluZTEPMA0GA1UECgwGcGhwYXMy 51 | MQ8wDQYDVQQLDAZwaHBhczIxGzAZBgNVBAMMEnBocGFzMi5leGFtcGxlLmNvbTCC 52 | Ad0wDQYJKoZIhvcNAQEBBQADggHKLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0N 53 | Ck1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbDgx 54 | RU96aWxzcFRITmJmMmtOU0wNCmhjMzZzUnZJNlBQaGhRcTg1TkMrZFdwSnBEbzFK 55 | T1lFd3YzOFpYcWdzNW1OZFZCcGx6TVc2VXFnM3BFRHc0SUkNCmtyZisyeGsvdUMy 56 | dXJSM09PRk8vN0RyWmhLSkdZQTZ4T2wzMEw0bXMzQ2lEMi9sLzczaVEzdUwwTlZF 57 | akNMWVQNClFRaDAxUTBxOUhRRnQwZXYvMXJOSzJCWkNnNUxNUkxOZ1FxZmdEeDQ5 58 | SXFzN0VWczNvWG1hWm1jemhza1JnOUkNCmhQOS9OZG9UdjVMNStma1J3V1hPN3Z0 59 | a2hiZk1VbHp0bXVVVHZVMTRBYUlrWWQzTys3cGRhZnlrdnFMSG00eUgNCkU2Z2Mv 60 | MlVlditNWGtsV2VVTmFtVFV3azlhVkJJcXAwSndjYndrZWc1Q2xuUHdQZ0Y3QTVs 61 | NzY1WmlvZDlJOFMNClRRSURBUUFCDQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS2j 62 | EjAQMA4GA1UdDwEB/wQEAwIFoDALBgkqhkiG9w0BAQsDAWExggHLMIIBxwIBATCB 63 | nzCBljEhMB8GCSqGSIb3DQEJARYSdmsudGlhbW9AZ21haWwuY29tMQswCQYDVQQG 64 | EwJ1azETMBEGA1UECAwKU3RhdGUgbmFtZTEQMA4GA1UEBwwHVWtyYWluZTEPMA0G 65 | A1UECgwGcGhwYXMyMQ8wDQYDVQQLDAZwaHBhczIxGzAZBgNVBAMMEnBocGFzMi5l 66 | eGFtcGxlLmNvbQIEXXmKiDANBglghkgBZQMEAgEFADANBgkqhkiG9w0BAQEFAASC 67 | AQCSMG/S+PRqYGUzdud/BG4/n/RhCWc9ulMeZRyklV6+cmiJMzNagW+SikurHQ0/ 68 | G7k5fWM4oJ/9NPPI4YeRzp1g5++izPnrWbhYVGPyYn26cPLqn6/2qSJIIZXTYr8m 69 | muSU7t7Y17TSdTfnWytqHRx9SDLFGngM2EfWiA5fXE6hm6qUi+JfRoi0C+47zEvE 70 | 4JN/1gkDUbvMxwFHoOKt917BA4FTVtdeZexLPrePz4t39m7m+/6CLo6GC60vi6Xt 71 | +p2xoeVeqMbXDNUzTJhm8CU41UupfgcbebdfPTqQjPjBisYeJzp2zNIOudc4MfIM 72 | db9RhvFFC+Mlbsu/yutDvBEQ 73 | ------=_54e86d8581e0cd7678fd65ee1a7d60d046487f53-- 74 | -------------------------------------------------------------------------------- /tests/fixtures/signed2.txt: -------------------------------------------------------------------------------- 1 | MIME-Version: 1.0 2 | Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg="sha-256"; boundary="----25B7A0535EB50D50B930D5C3873D6133" 3 | 4 | ------25B7A0535EB50D50B930D5C3873D6133 5 | Content-type: application/EDI-Consent 6 | Content-Transfer-Encoding: binary 7 | Content-Disposition: attachment; filename=payload.txt 8 | 9 | UNB+UNOA:2+:14+:14+140407:0910+5++++1+EANCOM' 10 | UNH+1+ORDERS:D:96A:UN:EAN008' 11 | BGM+220+1AA1TEST+9' 12 | DTM+137:20140407:102' 13 | DTM+63:20140421:102' 14 | DTM+64:20140414:102' 15 | RFF+ADE:1234' 16 | RFF+PD:1704' 17 | NAD+BY+5450534000024::9' 18 | NAD+SU+::9' 19 | NAD+DP+5450534000109::9+++++++GB' 20 | NAD+IV+5450534000055::9++AMAZON EU SARL:5 RUE PLAETIS LUXEMBOURG+CO PO BOX 4558+SLOUGH++SL1 0TX+GB' 21 | RFF+VA:GB727255821' 22 | CUX+2:EUR:9' 23 | LIN+1++9783898307529:EN' 24 | QTY+21:5' 25 | PRI+AAA:27.5' 26 | LIN+2++390787706322:UP' 27 | QTY+21:1' 28 | PRI+AAA:10.87' 29 | LIN+3' 30 | PIA+5+3899408268X-39:SA' 31 | QTY+21:3' 32 | PRI+AAA:3.85' 33 | UNS+S' 34 | CNT+2:3' 35 | UNT+26+1' 36 | UNZ+1+5' 37 | 38 | ------25B7A0535EB50D50B930D5C3873D6133 39 | Content-Transfer-Encoding: base64 40 | Content-Disposition: attachment; filename="smime.p7s" 41 | Content-Type: application/pkcs7-signature; name=smime.p7s; smime-type=signed-data 42 | 43 | MIIGsAYJKoZIhvcNAQcCoIIGoTCCBp0CAQExDzANBglghkgBZQMEAgEFADALBgkq 44 | hkiG9w0BBwGgggPCMIIDvjCCAqagAwIBAgIEXXmKiDANBgkqhkiG9w0BAQUFADCB 45 | ljEhMB8GCSqGSIb3DQEJARYSdmsudGlhbW9AZ21haWwuY29tMQswCQYDVQQGEwJ1 46 | azETMBEGA1UECAwKU3RhdGUgbmFtZTEQMA4GA1UEBwwHVWtyYWluZTEPMA0GA1UE 47 | CgwGcGhwYXMyMQ8wDQYDVQQLDAZwaHBhczIxGzAZBgNVBAMMEnBocGFzMi5leGFt 48 | cGxlLmNvbTAeFw0xOTA5MTIwMDAwMDhaFw0yMDA5MTEwMDAwMDhaMIGWMSEwHwYJ 49 | KoZIhvcNAQkBFhJ2ay50aWFtb0BnbWFpbC5jb20xCzAJBgNVBAYTAnVrMRMwEQYD 50 | VQQIDApTdGF0ZSBuYW1lMRAwDgYDVQQHDAdVa3JhaW5lMQ8wDQYDVQQKDAZwaHBh 51 | czIxDzANBgNVBAsMBnBocGFzMjEbMBkGA1UEAwwScGhwYXMyLmV4YW1wbGUuY29t 52 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl81EOzilspTHNbf2kNSL 53 | hc36sRvI6PPhhQq85NC+dWpJpDo1JOYEwv38ZXqgs5mNdVBplzMW6Uqg3pEDw4II 54 | krf+2xk/uC2urR3OOFO/7DrZhKJGYA6xOl30L4ms3CiD2/l/73iQ3uL0NVEjCLYT 55 | QQh01Q0q9HQFt0ev/1rNK2BZCg5LMRLNgQqfgDx49Iqs7EVs3oXmaZmczhskRg9I 56 | hP9/NdoTv5L5+fkRwWXO7vtkhbfMUlztmuUTvU14AaIkYd3O+7pdafykvqLHm4yH 57 | E6gc/2Uev+MXklWeUNamTUwk9aVBIqp0Jwcbwkeg5ClnPwPgF7A5l765Ziod9I8S 58 | TQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQEFBQADggEBAIOV 59 | AtfT4fmoL5BmVUhF4KxcFgM2WNwNWdXfjj/Fnk9h79ruUlvh3vcooD7IbcZvRU2j 60 | SJkU1dQZgSOvERuVlR1cmt9yLRalfrVJCy05G2CXl0Ce+9UO6UbXcz+z0LprkeyZ 61 | 7MrVdaKDdQSJT1u8Kt0w+izv9oDpD70zo7+jp4Y5b7oVhQf2eckQQaA79GmO08Hq 62 | Ccd0JRyFGXdymPo6AA/Do0x5WsPWt8vQ5BM6lXhbfW1keOe6NFduLGHejrbBNcU3 63 | 9R069YF2RSlNK24q/TPxwZpgdtuTVxQf5bl2SPBB+8gmLHIV1GaD9BSerPf4RubM 64 | UIz8rcS3g6dELsyO5EQxggKyMIICrgIBATCBnzCBljEhMB8GCSqGSIb3DQEJARYS 65 | dmsudGlhbW9AZ21haWwuY29tMQswCQYDVQQGEwJ1azETMBEGA1UECAwKU3RhdGUg 66 | bmFtZTEQMA4GA1UEBwwHVWtyYWluZTEPMA0GA1UECgwGcGhwYXMyMQ8wDQYDVQQL 67 | DAZwaHBhczIxGzAZBgNVBAMMEnBocGFzMi5leGFtcGxlLmNvbQIEXXmKiDANBglg 68 | hkgBZQMEAgEFAKCB5DAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3 69 | DQEJBTEPFw0yMTA3MjgwOTQ3MzVaMC8GCSqGSIb3DQEJBDEiBCDIfbzcvQWvPwdz 70 | hjOqGlyt/tOnZ082JvlAd3Ds5JDVajB5BgkqhkiG9w0BCQ8xbDBqMAsGCWCGSAFl 71 | AwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMA4GCCqG 72 | SIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG9w0DAgIB 73 | KDANBgkqhkiG9w0BAQEFAASCAQB8uDLUmI9eIO0OAYSb2withXQ42QpYJTTBp4cx 74 | ytdDPX366DU3E8NaXfCATv+rWGDJJ/w7dk01ExFu9n5zchbREhKZAZt8r7unkbh7 75 | 0fGp26boqBZAKTt12aJJGnlZkfiNCVfhm+gXd67A4otDMC9LK04zAqckZqk7w7d0 76 | 8fnFS8IYY0d0VpXZZS4Yl6nesjxJwoySHewkfoifKZomDJDupgEax2wLmK1Ry8Qm 77 | QMSFjpdI/USPHsYxTxTf0G+Oju9gFH3V6jSGfzGLaRf//yBDFD3mAQ9Uw0Cb14mX 78 | siY6+xEAyfLwBU1d1kfkuNL6lWAYF+IX+1VgTKs9D4DZ1EYn 79 | ------25B7A0535EB50D50B930D5C3873D6133-- 80 | -------------------------------------------------------------------------------- /tests/fixtures/test.edi: -------------------------------------------------------------------------------- 1 | UNB+UNOA:2+:14+:14+140407:0910+5++++1+EANCOM' 2 | UNH+1+ORDERS:D:96A:UN:EAN008' 3 | BGM+220+1AA1TEST+9' 4 | DTM+137:20140407:102' 5 | DTM+63:20140421:102' 6 | DTM+64:20140414:102' 7 | RFF+ADE:1234' 8 | RFF+PD:1704' 9 | NAD+BY+5450534000024::9' 10 | NAD+SU+::9' 11 | NAD+DP+5450534000109::9+++++++GB' 12 | NAD+IV+5450534000055::9++AMAZON EU SARL:5 RUE PLAETIS LUXEMBOURG+CO PO BOX 4558+SLOUGH++SL1 0TX+GB' 13 | RFF+VA:GB727255821' 14 | CUX+2:EUR:9' 15 | LIN+1++9783898307529:EN' 16 | QTY+21:5' 17 | PRI+AAA:27.5' 18 | LIN+2++390787706322:UP' 19 | QTY+21:1' 20 | PRI+AAA:10.87' 21 | LIN+3' 22 | PIA+5+3899408268X-39:SA' 23 | QTY+21:3' 24 | PRI+AAA:3.85' 25 | UNS+S' 26 | CNT+2:3' 27 | UNT+26+1' 28 | UNZ+1+5' -------------------------------------------------------------------------------- /tests/fixtures/test.mdn: -------------------------------------------------------------------------------- 1 | Server: nginx 2 | Content-Type: text/html; charset=UTF-8 3 | Transfer-Encoding: chunked 4 | Connection: keep-alive 5 | Vary: Accept-Encoding 6 | Message-Id: <2021-07-23-60faae80cf63b1.85861604@as2eteco.eteco-group.com> 7 | Date: Fri, 23 Jul 2021 11:56:48 +0000 8 | As2-From: as2eteco 9 | As2-To: as2polini 10 | As2-Version: 1.2 11 | User-Agent: PHPAS2 12 | Ediint-Features: CEM 13 | Mime-Version: 1.0 14 | 15 | ------AEE2C100D7804E037C619C123BA3D8ED 16 | Content-Type: multipart/report; report-type=disposition-notification; boundary="----=_0f177cb3dbdfcfaf3bd6fb813325ecc5380995a5" 17 | 18 | ------=_0f177cb3dbdfcfaf3bd6fb813325ecc5380995a5 19 | Content-Type: text/plain 20 | Content-Transfer-Encoding: 7bit 21 | 22 | Your message was successfully received and processed. 23 | 24 | ------=_0f177cb3dbdfcfaf3bd6fb813325ecc5380995a5 25 | Content-Type: message/disposition-notification 26 | Content-Transfer-Encoding: 7bit 27 | 28 | Reporting-UA: PHPAS2 29 | Original-Recipient: rfc822; as2eteco 30 | Final-Recipient: rfc822; as2eteco 31 | Original-Message-ID: <2021-07-23-60faae80cc5de2.70892947@as2polini.eteco-group.com> 32 | Disposition: automatic-action/MDN-sent-automatically; processed 33 | Received-Content-MIC: IyhQhOsx+1suwiEjkJjlaskVWHyQW4kxmQP3rsq16sM=, sha256 34 | 35 | ------=_0f177cb3dbdfcfaf3bd6fb813325ecc5380995a5-- 36 | ------AEE2C100D7804E037C619C123BA3D8ED 37 | Content-Transfer-Encoding: base64 38 | Content-Disposition: attachment; filename="smime.p7s" 39 | Content-Type: application/pkcs7-signature; name=smime.p7s; smime-type=signed-data 40 | 41 | MIIFbQYJKoZIhvcNAQcCoIIFXjCCBVoCAQExDzANBglghkgBZQMEAgEFADALBgkq 42 | hkiG9w0BBwGgggL1MIIC8TCCAdmgAwIBAgIJAI+A9000plXZMA0GCSqGSIb3DQEB 43 | CwUAMB0xGzAZBgNVBAMMEmFzMi5ldGVjby1ncm91cC5kZTAeFw0xOTAxMTQwOTA1 44 | NThaFw0yOTAxMTEwOTA1NThaMB0xGzAZBgNVBAMMEmFzMi5ldGVjby1ncm91cC5k 45 | ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJZ/xA4MJPPA66Ils84D 46 | wklSBxim788LzFOsi99RgO1ktfbbKFrdXIrEUYWvDmSMbiY7ALz9UsMKPA7T0/t0 47 | l1XkGuCh+/TqAQgbMkjzNkCpjedufC9ghMSUhndSGIMdsQf70styWiZVSSNnZ4cG 48 | 26H+mVJXKVcr5BRZufv3fR+wMuADuGSE5xR5R+jhOLxgJEfvpZeuGKhGix6sdagE 49 | 3MfjOH8vbtOmrbltu8H9mbXPkiz9aSvEV3ocbesVIOxjhiWzUYvbYRhABebtNDKl 50 | vb7j3aBjoSHzaEhZOP4O+uSsoHOh0if0ukP3ksmHixEOVFzV8bPc92q3ONTH4ZNX 51 | FOkCAwEAAaM0MDIwMAYDVR0RBCkwJ4ISYXMyLmV0ZWNvLWdyb3VwLmRlggtleGFt 52 | cGxlLm5ldIcECgAAATANBgkqhkiG9w0BAQsFAAOCAQEAOFHlT5n6IdH2xv6bi50O 53 | CwSPajVPz8hCAo6XTdRwE5InaLVgziuRfQD1s/GUjLeM89u42CgA2FNkeKc4/iGv 54 | hCueFGMRjBlhHOEoDwdcFpkLNJgtfaEmFDOHHjXgIP+MHbEQ7uu9Yspf+hdTDMT1 55 | CbCwRIWMfdt1VhGOZXoAg2Jgzwyqszf+H6EXilqZtzYdSDV4r2XoX+n2Oe6V3oot 56 | Ndtsbh2QrtiTMS328brydAbXFsOzr2B/ygQkLPmEITgjyDqn2oXI8YR6Mfw0MImk 57 | GByapr7g+/eLHnuP4ULqoQh54EkiTfodzbdRkhvT1cA9U+hH8BdPDB+jDsP5BWCc 58 | wjGCAjwwggI4AgEBMCowHTEbMBkGA1UEAwwSYXMyLmV0ZWNvLWdyb3VwLmRlAgkA 59 | j4D3TTSmVdkwDQYJYIZIAWUDBAIBBQCggeQwGAYJKoZIhvcNAQkDMQsGCSqGSIb3 60 | DQEHATAcBgkqhkiG9w0BCQUxDxcNMjEwNzIzMTE1NjQ4WjAvBgkqhkiG9w0BCQQx 61 | IgQgTdC7qREPIZnRru+xhWwxB5KzWu1LPxibitb49WDxrUwweQYJKoZIhvcNAQkP 62 | MWwwajALBglghkgBZQMEASowCwYJYIZIAWUDBAEWMAsGCWCGSAFlAwQBAjAKBggq 63 | hkiG9w0DBzAOBggqhkiG9w0DAgICAIAwDQYIKoZIhvcNAwICAUAwBwYFKw4DAgcw 64 | DQYIKoZIhvcNAwICASgwDQYJKoZIhvcNAQEBBQAEggEAEVf41yjJkd7BiMw3gdqK 65 | 0WK5z081rZTF8Z9RaMtgEfF/xHGZmrXXs8qBYdO6m9rgvj47m4lC0mQfgFe86Qqm 66 | js+aIqNjbRwN8dX8SSC8Y+5pN3v6XgiQD/NAfWZsQjnRr1NwjqEIkp+1zzyLXA9S 67 | Aozo+Z+dsjI9XKMeczf1RYXMThHjBFCVO7ECrzcmYt6vM7mgViafLBsf+jIF5DQR 68 | V2HDSGGmRlXscaM3vFsH/TTM557vkYOnLmoF5daqftzAEEkQ5wiHrzjG8ZS28ZOE 69 | BIzQCvo6FgDPzCbOxsLzNguOvOMV1h6GAnvfwW095P3X0g+AD4Ux9CGnoEI1Nlhu 70 | RA== 71 | ------AEE2C100D7804E037C619C123BA3D8ED-- 72 | -------------------------------------------------------------------------------- /tests/fixtures/test.xml: -------------------------------------------------------------------------------- 1 | Content-Type: application/EDI-Consent 2 | Content-Disposition: attachment; filename="test1.xml" 3 | Content-Transfer-Encoding: binary 4 | 5 | 6 | 7 | 1.0 8 | 9 | 999888777666 10 | 11 | 12 | 1234567890123 13 | 14 | 15 | GS1 16 | 3.2 17 | 6a8709ad-d0c0-454f-aab7-20a6f4395eaf 18 | Invoice 19 | 2017-12-22T10:13:02 20 | 21 | 22 | 23 | 2017-12-22T10:13:02 24 | ORIGINAL 25 | 26 | 99339718 27 | 28 | 999888777666 29 | 30 | 31 | CREDIT_NOTE 32 | GBP 33 | XCAW00000101554 34 | 35 | 1234567890123 36 | 38711469 37 | 12005008 38 | 39 | 40 | 999888777666 41 | 42 | 43 | 285.58 44 | 285.58 45 | 285.58 46 | 285.58 47 | 0.00 48 | 0 49 | 50 | 0.00 51 | 285.58 52 | OEX 53 | 0.00 54 | VAT 55 | 56 | 57 | 58 | ORDER12345 59 | 2017-12-18T00:00:00 60 | 61 | 62 | 78024801 63 | 2017-12-19T00:00:00 64 | 65 | 66 | 1 67 | 1 68 | 33.63 69 | 33.63 70 | Credit 71 | SHORT_DELIVERY 72 | 33.6300 73 | 74 | 99999999999999 75 | PHASE SPREAD DAWN$ 76 | 05389 77 | 78 | 1 x 12.5Kg 79 | 80 | 81 | 82 | 0 83 | 33.63 84 | OEX 85 | 0 86 | VAT 87 | 88 | 89 | ORDER12345 90 | 91 | 1234567890123 92 | 93 | 94 | 95 | 78024801 96 | 2017-12-19T00:00:00 97 | 1 98 | 99 | 100 | 101 | 2 102 | 3 103 | 75.29 104 | 75.29 105 | Credit 106 | AGREED_SETTLEMENT 107 | 25.0967 108 | 109 | 99999999999999 110 | PREP PREMIUM SUNFLOWER OIL PET 111 | 86162 112 | 113 | 6 x 2Ltr 114 | 115 | 116 | 117 | 0 118 | 75.29 119 | OEX 120 | 0 121 | VAT 122 | 123 | 124 | ORDER12345 125 | 126 | 1234567890123 127 | 128 | 129 | 130 | 78024801 131 | 2017-12-19T00:00:00 132 | 2 133 | 134 | 135 | 136 | 3 137 | 2 138 | 8.36 139 | 8.36 140 | Credit 141 | AGREED_SETTLEMENT 142 | 4.1800 143 | 144 | 99999999999999 145 | PREP PREMIUM SUNFLOWER OIL PET 146 | 86162S 147 | 148 | 1 x 2Ltr 149 | 150 | 151 | 152 | 0 153 | 8.36 154 | OEX 155 | 0 156 | VAT 157 | 158 | 159 | ORDER12345 160 | 161 | 1234567890123 162 | 163 | 164 | 165 | 78024801 166 | 2017-12-19T00:00:00 167 | 3 168 | 169 | 170 | 171 | 4 172 | 3 173 | 151.47 174 | 151.47 175 | Credit 176 | AGREED_SETTLEMENT 177 | 50.4900 178 | 179 | 99999999999999 180 | LA ESPANOLA XTRA VRG OLIVE OIL 181 | 00068 182 | 183 | 3 x 5Ltr 184 | 185 | 186 | 187 | 0 188 | 151.47 189 | OEX 190 | 0 191 | VAT 192 | 193 | 194 | ORDER12345 195 | 196 | 1234567890123 197 | 198 | 199 | 200 | 78024801 201 | 2017-12-19T00:00:00 202 | 4 203 | 204 | 205 | 206 | 5 207 | 1 208 | 16.83 209 | 16.83 210 | Credit 211 | AGREED_SETTLEMENT 212 | 16.8300 213 | 214 | 99999999999999 215 | LA ESPANOLA XTRA VRG OLIVE OIL 216 | 00068S 217 | 218 | 1 x 5Ltr 219 | 220 | 221 | 222 | 0 223 | 16.83 224 | OEX 225 | 0 226 | VAT 227 | 228 | 229 | ORDER12345 230 | 231 | 1234567890123 232 | 233 | 234 | 235 | 78024801 236 | 2017-12-19T00:00:00 237 | 5 238 | 239 | 240 | 241 | --------------------------------------------------------------------------------