1 <?php
2 namespace IXR\Client;
3 
4 use IXR\Exception\ClientException;
5 use IXR\Message\Message;
6 use IXR\Request\Request;
7 
8 /**
9  * Client for communicating with a XML-RPC Server over HTTPS.
10  *
11  * @author Jason Stirk <jstirk@gmm.com.au> (@link http://blog.griffin.homelinux.org/projects/xmlrpc/)
12  * @version 0.2.0 26May2005 08:34 +0800
13  * @copyright (c) 2004-2005 Jason Stirk
14  * @package IXR
15  */
16 class ClientSSL extends Client
17 {
18     /**
19      * Filename of the SSL Client Certificate
20      * @access private
21      * @since 0.1.0
22      * @var string
23      */
24     private $_certFile;
25 
26     /**
27      * Filename of the SSL CA Certificate
28      * @access private
29      * @since 0.1.0
30      * @var string
31      */
32     private $_caFile;
33 
34     /**
35      * Filename of the SSL Client Private Key
36      * @access private
37      * @since 0.1.0
38      * @var string
39      */
40     private $_keyFile;
41 
42     /**
43      * Passphrase to unlock the private key
44      * @access private
45      * @since 0.1.0
46      * @var string
47      */
48     private $_passphrase;
49 
50     /**
51      * Constructor
52      * @param string $server URL of the Server to connect to
53      * @since 0.1.0
54      */
55     public function __construct($server, $path = false, $port = 443, $timeout = false, $timeout_io = null)
56     {
57         parent::__construct($server, $path, $port, $timeout, $timeout_io);
58         $this->useragent = 'The Incutio XML-RPC PHP Library for SSL';
59 
60         // Set class fields
61         $this->_certFile = false;
62         $this->_caFile = false;
63         $this->_keyFile = false;
64         $this->_passphrase = '';
65     }
66 
67     /**
68      * Set the client side certificates to communicate with the server.
69      *
70      * @since 0.1.0
71      * @param string $certificateFile Filename of the client side certificate to use
72      * @param string $keyFile         Filename of the client side certificate's private key
73      * @param string $keyPhrase       Passphrase to unlock the private key
74      * @throws ClientException
75      */
76     public function setCertificate($certificateFile, $keyFile, $keyPhrase = '')
77     {
78         // Check the files all exist
79         if (is_file($certificateFile)) {
80             $this->_certFile = $certificateFile;
81         } else {
82             throw new ClientException('Could not open certificate: ' . $certificateFile);
83         }
84 
85         if (is_file($keyFile)) {
86             $this->_keyFile = $keyFile;
87         } else {
88             throw new ClientException('Could not open private key: ' . $keyFile);
89         }
90 
91         $this->_passphrase = (string)$keyPhrase;
92     }
93 
94     public function setCACertificate($caFile)
95     {
96         if (is_file($caFile)) {
97             $this->_caFile = $caFile;
98         } else {
99             throw new ClientException('Could not open CA certificate: ' . $caFile);
100         }
101     }
102 
103     /**
104      * Sets the connection timeout (in seconds)
105      * @param int $newTimeOut Timeout in seconds
106      * @returns void
107      * @since 0.1.2
108      */
109     public function setTimeOut($newTimeOut)
110     {
111         $this->timeout = (int)$newTimeOut;
112     }
113 
114     /**
115      * Returns the connection timeout (in seconds)
116      * @returns int
117      * @since 0.1.2
118      */
119     public function getTimeOut()
120     {
121         return $this->timeout;
122     }
123 
124     /**
125      * Set the query to send to the XML-RPC Server
126      * @since 0.1.0
127      */
128     public function query()
129     {
130         $args = func_get_args();
131         $method = array_shift($args);
132         $request = new Request($method, $args);
133         $length = $request->getLength();
134         $xml = $request->getXml();
135 
136         $this->debugOutput('<pre>' . htmlspecialchars($xml) . PHP_EOL . '</pre>');
137 
138         //This is where we deviate from the normal query()
139         //Rather than open a normal sock, we will actually use the cURL
140         //extensions to make the calls, and handle the SSL stuff.
141 
142         $curl = curl_init('https://' . $this->server . $this->path);
143 
144         curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
145 
146         //Since 23Jun2004 (0.1.2) - Made timeout a class field
147         curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $this->timeout);
148         if (null !== $this->timeout_io) {
149             curl_setopt($curl, CURLOPT_TIMEOUT, $this->timeout_io);
150         }
151 
152         if ($this->debug) {
153             curl_setopt($curl, CURLOPT_VERBOSE, 1);
154         }
155 
156         curl_setopt($curl, CURLOPT_HEADER, 1);
157         curl_setopt($curl, CURLOPT_POST, 1);
158         curl_setopt($curl, CURLOPT_POSTFIELDS, $xml);
159         if($this->port !== 443) {
160             curl_setopt($curl, CURLOPT_PORT, $this->port);
161         }
162         curl_setopt($curl, CURLOPT_HTTPHEADER, [
163             "Content-Type: text/xml",
164             "Content-length: {$length}"
165         ]);
166 
167         // Process the SSL certificates, etc. to use
168         if (!($this->_certFile === false)) {
169             // We have a certificate file set, so add these to the cURL handler
170             curl_setopt($curl, CURLOPT_SSLCERT, $this->_certFile);
171             curl_setopt($curl, CURLOPT_SSLKEY, $this->_keyFile);
172 
173             if ($this->debug) {
174                 $this->debugOutput('SSL Cert at : ' . $this->_certFile);
175                 $this->debugOutput('SSL Key at : ' . $this->_keyFile);
176             }
177 
178             // See if we need to give a passphrase
179             if (!($this->_passphrase === '')) {
180                 curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $this->_passphrase);
181             }
182 
183             if ($this->_caFile === false) {
184                 // Don't verify their certificate, as we don't have a CA to verify against
185                 curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
186             } else {
187                 // Verify against a CA
188                 curl_setopt($curl, CURLOPT_CAINFO, $this->_caFile);
189             }
190         }
191 
192         // Call cURL to do it's stuff and return us the content
193         $contents = curl_exec($curl);
194         curl_close($curl);
195 
196         // Check for 200 Code in $contents
197         if (!strstr($contents, '200 OK')) {
198             //There was no "200 OK" returned - we failed
199             return $this->handleError(-32300, 'transport error - HTTP status code was not 200');
200         }
201 
202         if ($this->debug) {
203             $this->debugOutput('<pre>' . htmlspecialchars($contents) . PHP_EOL . '</pre>');
204         }
205         // Now parse what we've got back
206         // Since 20Jun2004 (0.1.1) - We need to remove the headers first
207         // Why I have only just found this, I will never know...
208         // So, remove everything before the first <
209         $contents = substr($contents, strpos($contents, '<'));
210 
211         $this->message = new Message($contents);
212         if (!$this->message->parse()) {
213             // XML error
214             return $this->handleError(-32700, 'parse error. not well formed');
215         }
216         // Is the message a fault?
217         if ($this->message->messageType == 'fault') {
218             return $this->handleError($this->message->faultCode, $this->message->faultString);
219         }
220 
221         // Message must be OK
222         return true;
223     }
224 
225     /**
226      * Debug output, if debug is enabled
227      * @param $message
228      */
229     private function debugOutput($message)
230     {
231         if ($this->debug) {
232             echo $message . PHP_EOL;
233         }
234     }
235 }
236