1<?php
2namespace IXR\Client;
3
4use IXR\Exception\ClientException;
5use IXR\Message\Message;
6use 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 */
16class 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