fixed bad if statement, actually works for returning freebusy
[davical.git] / inc / iSchedule.php
blobdaebe1c097d45126814e8d81f038431d68140be6
1 <?PHP
2 /**
3 * Functions that are needed for iScheduling requests
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.
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
17 require_once("XMLDocument.php");
19 /**
20 * A class for handling iScheduling requests.
22 * @package davical
23 * @subpackage iSchedule
25 class iSchedule
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 ;
38 function __construct ( )
40 $this->selector = 'cal';
41 if ( is_object ( $c ) && isset ( $c->scheduling_dkim_selector ) )
42 $this->scheduling_dkim_selector = $c->scheduling_dkim_selector ;
45 /**
46 * gets the domainkey TXT record from DNS
47 */
48 function getTxt ()
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
56 $this->failed = true;
57 return false;
59 return true;
62 /**
63 * parses DNS TXT record from domainkey lookup
64 */
65 function parseTxt ( )
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 )
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;
80 return true;
83 /**
84 * validates that domainkey is acceptable for the current request
85 */
86 function validateKey ( )
88 $this->failed = true;
89 if ( isset ( $this->parsed [ 's' ] ) )
91 if ( ! preg_match ( '/(\*|calendar)/', $this->parsed [ 's' ] ) )
92 return 'foo';
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
100 if ( preg_match ( '/y/', $this->parsed [ 't' ] ) )
101 $this->failOnError = false;
102 if ( preg_match ( '/s/', $this->parsed [ 't' ] ) )
103 $this->subdomainsOK = false;
105 if ( isset ( $this->parsed [ 'g' ] ) )
106 $this->remote_user_rule = $this->parsed [ 'g' ];
107 if ( isset ( $this->parsed [ 'p' ] ) )
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;
114 else
115 return false;
116 $this->failed = false;
117 return true;
121 * finds a remote calender server via DNS SRV records
123 function getServer ( )
125 $this->remote_ssl = false;
126 $r = dns_get_record ( '_ischedules._tcp.' . $this->domain , DNS_SRV );
127 if ( 0 < count ( $r ) )
129 $remote_server = $r [ 0 ] [ 'target' ];
130 $remote_port = $r [ 0 ] [ 'port' ];
131 $this->remote_ssl = true;
133 if ( ! isset ( $remote_server ) )
135 $r = dns_get_record ( '_ischedule._tcp.' . $this->domain , DNS_SRV );
136 if ( 0 < count ( $r ) )
138 $remote_server = $r [ 0 ] [ 'target' ];
139 $remote_port = $r [ 0 ] [ 'port' ];
142 elseif ( $this->try_anyway == true )
144 if ( ! isset ( $remote_server ) )
145 $remote_server = $this->domain;
146 if ( ! isset ( $remote_port ) )
147 $remote_port = 80;
149 if ( ! isset ( $remote_server ) )
150 return false;
151 $this->remote_server = $remote_server;
152 $this->remote_port = $remote_port;
156 * get capabilities from remote server
158 function getCapabilities ( )
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!") );
171 * signs a POST body and headers
173 * @param string $body the body of the POST
174 * @param array $headers the headers to sign as passed to header ();
176 function signDKIM ( $body, $headers )
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
193 * parses and validates DK header
195 * @param string $sig the value of the DKIM-Signature header
197 function parseDKIM ( $sig )
200 $this->failed = true;
201 $tags = preg_split ( '/;[\s\t]/', $sig );
202 foreach ( $tags as $v )
204 list($key,$value) = preg_split ( '/=/', $v, 2 );
205 $dkim[$key] = $value;
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;
266 * split up a mailto uri into domain and user components
268 function parseURI ( $uri )
270 if ( preg_match ( '/^mailto:([^@]+)@([^\s\t\n]+)/', $uri, $matches ) )
272 $this->remote_user = $matches[1];
273 $this->domain = $matches[2];
275 else
276 return false;
280 * verifies parsed DKIM header is valid for current message with a signature from the public key in DNS
282 function verifySignature ( )
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;
315 * checks that current request has a valid DKIM signature signed by a currently valid key from DNS
317 function validateRequest ( )
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') );
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;
340 $d = new iSchedule ();
341 if ( $d->validateRequest ( ) )
343 include ( 'caldav-POST.php' );
344 // TODO
345 // handle request.