source: contrib/davical/inc/iSchedule.php @ 3733

Revision 3733, 11.0 KB checked in by gabriel.malheiros, 13 years ago (diff)

Ticket #1541 - <Davical customizado para o Expresso.Utiliza Caldav e CardDav?>

Line 
1<?PHP
2/**
3* Functions that are needed for iScheduling requests
4*
5*  - verifying Domain Key signatures
6*  - delivering remote scheduling requests to local users inboxes
7*  - Utility functions which we can use to decide whether this
8*    is a permitted activity for this user.
9*
10* @package   davical
11* @subpackage   iSchedule
12* @author    Rob Ostensen <rob@boxacle.net>
13* @copyright Rob Ostensen
14* @license   http://gnu.org/copyleft/gpl.html GNU GPL v3 or later
15*/
16
17require_once("XMLDocument.php");
18
19/**
20* A class for handling iScheduling requests.
21*
22* @package   davical
23* @subpackage   iSchedule
24*/
25class iSchedule
26{
27        public $parsed;
28        public $selector;
29        public $domain;
30        private $dk;
31        private $DKSig;
32        private $try_anyway = false;
33        private $failed = false;
34        private $failOnError = true;
35        private $subdomainsOK = true;
36        private $remote_public_key ;
37
38        function __construct ( )
39        {
40                $this->selector = 'cal';
41                if ( is_object ( $c ) && isset ( $c->scheduling_dkim_selector ) )
42                        $this->scheduling_dkim_selector = $c->scheduling_dkim_selector ;
43        }
44
45  /**
46        *       gets the domainkey TXT record from DNS 
47        */
48        function getTxt ()
49        {
50                // TODO handle parents of subdomains and procuration records
51                $dkim = dns_get_record ( $this->remote_selector . '._domainkey.' . $this->remote_server , DNS_TXT );
52                if ( count ( $dkim ) > 0 )
53                        $this->dk = $dkim [ 0 ] [ 'txt' ];
54                else
55                {
56                        $this->failed = true;
57                        return false;
58                }
59                return true;   
60        }
61
62  /**
63        * parses DNS TXT record from domainkey lookup
64        */
65        function parseTxt ( )
66        {
67                if ( $this->failed == true )
68                        return false;
69                $clean = preg_replace ( '/[\s\t]*([;=])[\s\t]*/', '$1', $this->dk );
70                $pairs = preg_split ( '/;/', $clean );
71                $this->parsed = array();
72                foreach ( $pairs  as $v )
73                {
74                        list($key,$value) = preg_split ( '/=/', $v, 2 );
75                        if ( preg_match ( '/(g|k|n|p|s|t|v)/', $key ) )
76                                $this->parsed [ $key ] = $value;
77                        else
78                                $this->parsed_ignored [ $key ] = $value;
79                }
80                return true;
81        }
82       
83  /**
84        * validates that domainkey is acceptable for the current request
85        */
86        function validateKey ( )
87        {
88                $this->failed = true;
89                if ( isset ( $this->parsed [ 's' ] ) )
90                {
91                        if ( ! preg_match ( '/(\*|calendar)/', $this->parsed [ 's' ] ) )
92                                return 'foo';
93                }
94                if ( isset ( $this->parsed [ 'k' ] ) && $this->parsed [ 'k' ] != 'rsa' )
95                        return false;
96                if ( isset ( $this->parsed [ 't' ] ) && ! preg_match ( '/^[y:s]+$/', $this->parsed [ 't' ] ) )
97                        return false;
98                else
99                {
100                        if ( preg_match ( '/y/', $this->parsed [ 't' ] ) )
101                                $this->failOnError = false;
102                        if ( preg_match ( '/s/', $this->parsed [ 't' ] ) )
103                                $this->subdomainsOK = false;
104                }
105                if ( isset ( $this->parsed [ 'g' ] ) )
106                        $this->remote_user_rule = $this->parsed [ 'g' ];
107                if ( isset ( $this->parsed [ 'p' ] ) )
108                {
109                        $data = "-----BEGIN PUBLIC KEY-----\n" . implode ("\n",str_split ( preg_replace ( '/_/', '', $this->parsed [ 'p' ] ), 64 )) . "\n-----END PUBLIC KEY-----";
110                        if ( $data === false )
111                                return false;
112                        $this->remote_public_key = $data;
113                }
114                else
115                        return false;
116                $this->failed = false;
117                return true;
118        }
119
120  /**
121        * finds a remote calender server via DNS SRV records
122        */
123        function getServer ( )
124        {
125                $this->remote_ssl = false;
126                $r = dns_get_record ( '_ischedules._tcp.' . $this->domain , DNS_SRV );
127                if ( 0 < count ( $r ) )
128                {
129                        $remote_server            = $r [ 0 ] [ 'target' ];
130                        $remote_port              = $r [ 0 ] [ 'port' ];
131                        $this->remote_ssl = true;
132                }
133                if ( ! isset ( $remote_server ) )
134                {
135                        $r = dns_get_record ( '_ischedule._tcp.' . $this->domain , DNS_SRV );
136                        if ( 0 < count ( $r ) )
137                        {
138                                $remote_server            = $r [ 0 ] [ 'target' ];
139                                $remote_port              = $r [ 0 ] [ 'port' ];
140                        }
141                }
142                elseif ( $this->try_anyway == true )
143                {
144                        if ( ! isset ( $remote_server ) )
145                                $remote_server = $this->domain;
146                        if ( ! isset ( $remote_port ) )
147                                $remote_port = 80;
148                }
149                if ( ! isset ( $remote_server ) )
150                        return false;
151                $this->remote_server = $remote_server;
152                $this->remote_port = $remote_port;
153        }
154
155  /**
156        *       get capabilities from remote server
157        */
158        function getCapabilities ( )
159        {
160                $remote_capabilities = file_get_contents ( 'http'. ( $this->remote_ssl ? 's' : '' ) . '://' .
161                        $this->remote_server . ':' . $this->remote_port .
162                        '/.well-known/ischedule?query=capabilities' );
163                $xmltree = BuildXMLTree( $request->xml_tags, $position);
164                if ( !is_object($xmltree) ) {
165                          $request->DoResponse( 406, translate("REPORT body is not valid XML data!") );
166                }
167        }
168
169
170  /**
171  * signs a POST body and headers
172  *
173        * @param string $body the body of the POST
174        * @param array  $headers the headers to sign as passed to header ();
175  */
176        function signDKIM ( $body, $headers )
177        {
178                $b = '';
179                if ( ! is_array ( $headers ) )
180                        return false;
181                foreach ( $headers as $v )
182                        $b .= $v . "\n";
183                $dk['s'] = $this->selector;
184                $dk['d'] = $this->domain;
185                $dk['c'] = 'simple-http';
186                $dk['q'] = 'dns/txt';
187                $dk['bh'] = base64_encode ( hash ( 'sha256', $body , true ) );
188                 //a=rsa-sha1; d=caveman.name; s=cal; c=simple-http; q=dns/txt; h=Originator:Recipient:Host:Content-Type; b
189                // XXX finish me
190        }
191
192  /**
193  * parses and validates DK header
194  *
195        * @param string $sig the value of the DKIM-Signature header
196  */
197        function parseDKIM ( $sig )
198        {
199
200                $this->failed = true;
201                $tags = preg_split ( '/;[\s\t]/', $sig );
202                foreach ( $tags as $v )
203                {
204                        list($key,$value) = preg_split ( '/=/', $v, 2 );
205                        $dkim[$key] = $value;
206                }
207                // the canonicalization method is currently undefined as of draft-01 of the iSchedule spec
208                // but it does define the value, it should be simple-http.  RFC4871 also defines two methods
209                // simple and relaxed, simple is probably the same as simple http
210                // relaxed allows for header case folding and whitespace folding, see section 3.4.4 or RFC4871
211                if ( ! preg_match ( '{(simple|simple-http|relaxed)(/(simple|simple-http|relaxed))?}', $dkim['c'], $matches ) ) // canonicalization method
212                        return 'bad canonicalization:' . $dkim['c'] ;
213                if ( count ( $matches ) > 2 )
214                        $this->body_cannon = $matches[2];
215                else
216                        $this->body_cannon = $matches[1];
217                $this->header_cannon = $matches[1];
218                // signing algorythm REQUIRED
219                if ( $dkim['a'] != 'rsa-sha1' && $dkim['a'] != 'rsa-sha256' )
220                        return 'bad signing algorythm:' . $dkim['a'] ;
221    // query method to retrieve public key, could/should we add https to the spec?  REQUIRED
222                if ( $dkim['q'] != 'dns/txt' )
223                        return 'bad query method';
224    // domain of the signing entity REQUIRED
225                if ( ! isset ( $dkim['d'] ) ) 
226                        return 'missing signing domain';
227                $this->remote_server = $dkim['d'];
228    // identity of signing agent, OPTIONAL
229                if ( isset ( $dkim['i'] ) )   
230      // if present, domain of the signing agent must be a match or a subdomain of the signing domain
231                        if ( ! stristr ( $dkim['i'], $dkim['d'] ) ) // RFC4871 does not specify a case match requirement
232                                return 'signing domain mismatch';
233                // grab the local part of the signing agent if it's an email address
234                if ( strstr ( $dkim [ 'i' ], '@' ) )
235                        $this->remote_user = substr ( $dkim [ 'i' ], 0, strpos ( $dkim [ 'i' ], '@' ) - 1 );
236    // selector used to retrieve public key REQUIRED
237                if ( ! isset ( $dkim['s'] ) ) 
238                        return 'missing selector';
239                $this->remote_selector = $dkim['s'];
240    // signed header fields, colon seperated  REQUIRED
241                if ( ! isset ( $dkim['h'] ) ) 
242                        return 'missing list of signed headers';
243                $this->signed_headers = preg_split ( '/:/', $dkim['h'] );
244                foreach ( $this->signed_headers as $h )
245      // signed header fields MUST actually be present in the request
246                        // DKIM Signature is NOT allowed in signed header fields per RFC4871
247                        if ( ( ! isset ( $_SERVER['HTTP_' . strtr ( strtoupper ( $h ), '-', '_' ) ] ) &&
248             ! isset ( $_SERVER[ strtr ( strtoupper ( $h ), '-', '_' ) ] ) )
249                                  || strtolower ( $h ) == 'dkim-signature' )
250                                return "header $h is signed but missing from request";
251    // body hash REQUIRED
252                if ( ! isset ( $dkim['bh'] ) )
253                        return 'missing body signature';
254    // signed header hash REQUIRED
255                if ( ! isset ( $dkim['b'] ) ) 
256                        return 'missing signature in b field';
257                // length of body used for signing
258                if ( isset ( $dkim['l'] ) )
259                        $this->signed_length = $dkim['l'];
260                $this->failed = false;
261                $this->DKSig = $dkim;
262                return true;
263        }       
264       
265  /**
266        * split up a mailto uri into domain and user components
267        */
268        function parseURI ( $uri )
269        {
270                if ( preg_match ( '/^mailto:([^@]+)@([^\s\t\n]+)/', $uri, $matches ) )
271                {
272                        $this->remote_user = $matches[1];
273                        $this->domain = $matches[2];
274                }
275                else
276                        return false;
277        }
278
279  /**
280        *       verifies parsed DKIM header is valid for current message with a signature from the public key in DNS
281        */
282        function verifySignature ( )
283        {
284                global $request,$c;
285                $this->failed = true;
286                $signed = '';
287                foreach ( $this->signed_headers as $h )
288                        if ( isset ( $_SERVER['HTTP_' . strtoupper ( strtr ( $h, '-', '_' ) ) ] ) )
289                                $signed .= "$h: " . $_SERVER['HTTP_' . strtoupper ( strtr ( $h, '-', '_' ) ) ] . "\n";
290                  else
291                                $signed .= "$h: " . $_SERVER[ strtoupper ( strtr ( $h, '-', '_' ) ) ] . "\n";
292                $body = $request->raw_post;
293                if ( ! isset ( $this->signed_length ) )
294                        $this->signed_length = strlen ( $body );
295                else
296                        $body = substr ( $body, 0, $this->signed_length );
297                if ( isset ( $this->remote_user_rule ) )
298                        if ( $this->remote_user_rule != '*' && ! stristr ( $this->remote_user, $this->remote_user_rule ) )
299                                return false;
300                $body_hash = base64_encode ( hash ( preg_replace ( '/^.*(sha[1256]+).*/','$1', $this->DKSig['a'] ), $body , true ) );
301                if ( $this->DKSig['bh'] != $body_hash )
302                  return false;
303                $sig = $_SERVER['HTTP_DKIM_SIGNATURE'];
304                $sig = preg_replace ( '/ b=[^;\s\n\t]+/', ' b=', $sig );
305                $sig = preg_replace ( '/[\r\n]*$/', '', $sig );
306                $signed .= 'DKIM-Signature: ' . $sig;
307                $verify = openssl_verify ( $signed, base64_decode ( $this->DKSig['b'] ), $this->remote_public_key );
308                if (  $verify != 1 )
309                        return false;
310                $this->failed = false;
311                return true;
312        }
313
314  /**
315        * checks that current request has a valid DKIM signature signed by a currently valid key from DNS
316        */
317        function validateRequest ( )
318        {
319                global $request;
320                if ( isset ( $_SERVER['HTTP_DKIM_SIGNATURE'] ) )
321                        $sig = $_SERVER['HTTP_DKIM_SIGNATURE'];
322                else
323                        $request->DoResponse( 403, translate('DKIM signature missing') );
324               
325    $err = $this->parseDKIM ( $sig );
326                if ( $err !== true || $this->failed )
327                        $request->DoResponse( 403, translate('DKIM signature invalid ' ) . "\n" . $err . "\n" . $sig );
328                if ( ! $this->getTxt () || $this->failed )
329                        $request->DoResponse( 403, translate('DKIM signature validation failed(DNS ERROR)') );
330                if ( ! $this->parseTxt () || $this->failed )
331                        $request->DoResponse( 403, translate('DKIM signature validation failed(KEY Parse ERROR)') );
332                if ( ! $this->validateKey () || $this->failed )
333                        $request->DoResponse( 403, translate('DKIM signature validation failed(KEY Validation ERROR)') );
334                if ( ! $this->verifySignature () || $this->failed )
335                        $request->DoResponse( 403, translate('DKIM signature validation failed(Signature verification ERROR)') . $this->verifySignature() );
336                return true;
337        }
338}
339
340$d = new iSchedule ();
341if ( $d->validateRequest ( ) )
342{
343        include ( 'caldav-POST.php' );
344        // TODO
345        // handle request.
346}
Note: See TracBrowser for help on using the repository browser.