From 5edee3c4d1f84c07c1c54775072601188075a542 Mon Sep 17 00:00:00 2001
From: Mike Macgirvin <mike@macgirvin.com>
Date: Thu, 21 Oct 2010 04:53:43 -0700
Subject: [PATCH] magic-envelope verification, status.net appears to do it
 wrong. Ultimately we need to do it right (or why bother having a spec?), and
 fallback to doing it wrong if we're talking to a broken system - which
 ironically seems to include most of the federated social web projects.

---
 boot.php           |  73 ++++++++++++++-----
 database.sql       |   1 +
 include/items.php  |   2 +-
 include/salmon.php | 109 +++++++++++++++++++++++++++++
 library/asn1.php   |   2 +-
 mod/salmon.php     | 170 +++++++++++++++++++++++++++++++++++++++++++--
 update.php         |   6 +-
 view/fake_feed.tpl |  18 +++++
 8 files changed, 354 insertions(+), 27 deletions(-)
 create mode 100644 view/fake_feed.tpl

diff --git a/boot.php b/boot.php
index 8fec10afaa..55c244e827 100644
--- a/boot.php
+++ b/boot.php
@@ -2,7 +2,7 @@
 
 set_time_limit(0);
 
-define ( 'BUILD_ID',               1010   );
+define ( 'BUILD_ID',               1011   );
 define ( 'DFRN_PROTOCOL_VERSION',  '2.0'  );
 
 define ( 'EOL',                    "<br />\r\n"     );
@@ -116,6 +116,7 @@ class App {
 	private $db;
 
 	private $curl_code;
+	private $curl_headers;
 
 	function __construct() {
 
@@ -204,6 +205,15 @@ class App {
 		return $this->curl_code;
 	}
 
+	function set_curl_headers($headers) {
+		$this->curl_headers = $headers;
+	}
+
+	function get_curl_headers() {
+		return $this->curl_headers;
+	}
+
+
 }}
 
 // retrieve the App structure
@@ -339,13 +349,12 @@ function t($s) {
 // results. 
 
 if(! function_exists('fetch_url')) {
-function fetch_url($url,$binary = false) {
+function fetch_url($url,$binary = false, &$redirects = 0) {
 	$ch = curl_init($url);
-	if(! $ch) return false;
+	if(($redirects > 8) || (! $ch)) 
+		return false;
 
-	curl_setopt($ch, CURLOPT_HEADER, 0);
-	curl_setopt($ch, CURLOPT_FOLLOWLOCATION,true);
-	curl_setopt($ch, CURLOPT_MAXREDIRS,8);
+	curl_setopt($ch, CURLOPT_HEADER, true);
 	curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);
 
 	// by default we will allow self-signed certs
@@ -366,26 +375,41 @@ function fetch_url($url,$binary = false) {
 		curl_setopt($ch, CURLOPT_BINARYTRANSFER,1);
 
 	$s = curl_exec($ch);
-	$info = curl_getinfo($ch);
+
+	$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+	$header = substr($s,0,strpos($s,"\r\n\r\n"));
+	if($http_code == 301 || $http_code == 302 || $http_code == 303) {
+        $matches = array();
+        preg_match('/(Location:|URI:)(.*?)\n/', $header, $matches);
+        $url = trim(array_pop($matches));
+        $url_parsed = parse_url($url);
+        if (isset($url_parsed)) {
+            $redirects++;
+            return fetch_url($url,$binary,$redirects);
+        }
+    }
 	$a = get_app();
-	$a->set_curl_code($info['http_code']);
+	$a->set_curl_code($http_code);
+	$body = substr($s,strlen($header)+4);
+	$a->set_curl_headers($header);
+
 	curl_close($ch);
-	return($s);
+	return($body);
 }}
 
 // post request to $url. $params is an array of post variables.
 
 if(! function_exists('post_url')) {
-function post_url($url,$params) {
+function post_url($url,$params, &$redirects = 0) {
 	$ch = curl_init($url);
-	if(! $ch) return false;
+	if(($redirects > 8) || (! $ch)) 
+		return false;
 
-	curl_setopt($ch, CURLOPT_HEADER, 0);
-	curl_setopt($ch, CURLOPT_FOLLOWLOCATION,true);
-	curl_setopt($ch, CURLOPT_MAXREDIRS,8);
+	curl_setopt($ch, CURLOPT_HEADER, true);
 	curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);
 	curl_setopt($ch, CURLOPT_POST,1);
 	curl_setopt($ch, CURLOPT_POSTFIELDS,$params);
+
 	$check_cert = get_config('system','verifyssl');
 	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false));
 	$prx = get_config('system','proxy');
@@ -398,11 +422,26 @@ function post_url($url,$params) {
 	}
 
 	$s = curl_exec($ch);
-	$info = curl_getinfo($ch);
+
+	$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+	$header = substr($s,0,strpos($s,"\r\n\r\n"));
+	if($http_code == 301 || $http_code == 302 || $http_code == 303) {
+        $matches = array();
+        preg_match('/(Location:|URI:)(.*?)\n/', $header, $matches);
+        $url = trim(array_pop($matches));
+        $url_parsed = parse_url($url);
+        if (isset($url_parsed)) {
+            $redirects++;
+            return post_url($url,$binary,$redirects);
+        }
+    }
 	$a = get_app();
-	$a->set_curl_code($info['http_code']);
+	$a->set_curl_code($http_code);
+	$body = substr($s,strlen($header)+4);
+	$a->set_curl_headers($header);
+
 	curl_close($ch);
-	return($s);
+	return($body);
 }}
 
 // random hash, 64 chars
diff --git a/database.sql b/database.sql
index 7526be41d1..a3d78d79a3 100644
--- a/database.sql
+++ b/database.sql
@@ -62,6 +62,7 @@ CREATE TABLE IF NOT EXISTS `contact` (
   `issued-id` char(255) NOT NULL,
   `dfrn-id` char(255) NOT NULL,
   `url` char(255) NOT NULL,
+  `lrdd` char(255) NOT NULL,
   `pubkey` text NOT NULL,
   `prvkey` text NOT NULL,
   `request` text NOT NULL,
diff --git a/include/items.php b/include/items.php
index f04bf0bd87..eca050d4fe 100644
--- a/include/items.php
+++ b/include/items.php
@@ -139,7 +139,7 @@ function get_feed_for(&$a, $dfrn_id, $owner_id, $last_update, $direction = 0) {
 	}
 
 	$salmon = '<link rel="salmon" href="' . xmlify($a->get_baseurl() . '/salmon/' . $owner_nick) . '" />' . "\n" ; 
-	$salmon = ''; // remove this line when salmon handler is finished
+//	$salmon = ''; // remove this line when salmon handler is finished
 
 	$atom .= replace_macros($feed_template, array(
 		'$feed_id'      => xmlify($a->get_baseurl() . '/profile/' . $owner_nick),
diff --git a/include/salmon.php b/include/salmon.php
index 7198f07c60..bd2d620a81 100644
--- a/include/salmon.php
+++ b/include/salmon.php
@@ -16,3 +16,112 @@ function salmon_key($pubkey) {
 
 	return 'RSA' . '.' . $m . '.' . $e ;
 }
+
+
+function base64url_encode($s) {
+	return strtr(base64_encode($s),'+/','-_');
+}
+
+function base64url_decode($s) {
+	return base64_decode(strtr($s,'-_','+/'));
+}
+
+function get_salmon_key($uri,$keyhash) {
+	$ret = array();
+
+	$debugging = get_config('system','debugging');
+	if($debugging)		
+		file_put_contents('salmon.out', "\n" . 'Fetch key' . "\n", FILE_APPEND);
+
+	if(strstr($uri,'@')) {	
+		$arr = webfinger($uri);
+		if($debugging)
+			file_put_contents('salmon.out', "\n" . 'Fetch key from webfinger' . "\n", FILE_APPEND);
+	}
+	else {
+		$html = fetch_url($uri);
+		$a = get_app();
+		$h = $a->get_curl_headers();
+		if($debugging)
+			file_put_contents('salmon.out', "\n" . 'Fetch key via HTML header: ' . $h . "\n", FILE_APPEND);
+
+		$l = explode("\n",$h);
+		if(count($l)) {
+			foreach($l as $line) {
+				
+				if($debugging)
+					file_put_contents('salmon.out', "\n" . $line . "\n", FILE_APPEND);
+				if((stristr($line,'link:')) && preg_match('/<([^>].*)>.*rel\=[\'\"]lrdd[\'\"]/',$line,$matches)) {
+					$link = $matches[1];
+					if($debugging)
+						file_put_contents('salmon.out', "\n" . 'Fetch key via Link from header: ' . $link . "\n", FILE_APPEND);
+					break;
+				}
+			}
+		}
+	}
+
+	if(! isset($link)) {
+		require_once('library/HTML5/Parser.php');
+		$dom = HTML5_Parser::parse($html);
+
+		if(! $dom)
+			return '';
+
+		$items = $dom->getElementsByTagName('link');
+
+		foreach($items as $item) {
+			$x = $item->getAttribute('rel');
+			if($x == "lrdd") {
+				$link = $item->getAttribute('href');
+				if($debugging)
+					file_put_contents('salmon.out', "\n" . 'Fetch key via HTML body' . $link . "\n", FILE_APPEND);
+				break;
+			}
+		}
+	}
+
+	if(! isset($link))
+		return '';
+
+	$arr = fetch_xrd_links($link);
+
+	if($arr) {
+		foreach($arr as $a) {
+			if($a['@attributes']['rel'] === 'magic-public-key') {
+				$ret[] = $a['@attributes']['href'];
+			}
+		}
+	}
+	if(count($ret)) {
+		for($x = 0; $x < count($ret); $x ++) {
+			if(substr($ret[$x],0,5) === 'data:') {
+				if(strstr($ret[$x],','))
+					$ret[$x] = substr($ret[$x],strpos($ret[$x],',')+1);
+				else
+					$ret[$x] = substr($ret[$x],5);
+			}
+			else
+				$ret[$x] = fetch_url($ret[$x]);
+		}
+	}
+	if($debugging)
+		file_put_contents('salmon.out', "\n" . 'Key located: ' . print_r($ret,true) . "\n", FILE_APPEND);
+
+	if(count($ret) == 1) {
+		return $ret[0];
+	}
+	else {
+		foreach($ret as $a) {
+			$hash = base64url_encode(hash('sha256',$a));
+			if($hash == $keyhash)
+				return $a;
+		}
+	}
+
+	return '';
+}
+
+	
+		
+				
\ No newline at end of file
diff --git a/library/asn1.php b/library/asn1.php
index 132b032480..713978e8c1 100644
--- a/library/asn1.php
+++ b/library/asn1.php
@@ -186,7 +186,7 @@ class ASN_BASE {
 			case ASN_BOOLEAN:
 				return new ASN_BOOLEAN((bool)$data);
 			case ASN_INTEGER:
-				return new ASN_INTEGER(strtr(base64_encode($data),'+/=','-_,'));
+				return new ASN_INTEGER(strtr(base64_encode($data),'+/','-_'));
 //				return new ASN_INTEGER(ord($data));
 			case ASN_BIT_STR:
 				return new ASN_BIT_STR(self::parseASNString($data, $level+1, $maxLevels));
diff --git a/mod/salmon.php b/mod/salmon.php
index d68c746584..7ae29aafe2 100644
--- a/mod/salmon.php
+++ b/mod/salmon.php
@@ -1,5 +1,15 @@
 <?php
 
+
+// TODO: pass keyhash to key discovery
+// add relevant contacts so they can use this
+
+// There is a lot of debug stuff in here because this is quite a
+// complicated process to try and sort out. 
+
+require_once('include/salmon.php');
+require_once('simplepie/simplepie.inc');
+
 function salmon_return($val) {
 
 	if($val >= 500)
@@ -18,7 +28,7 @@ function salmon_post(&$a) {
 	
 	$debugging = get_config('system','debugging');
 	if($debugging)
-		file_put_contents('salmon.out',$xml,FILE_APPEND);
+		file_put_contents('salmon.out','New Salmon: ' . $xml . "\n",FILE_APPEND);
 
 	$nick       = (($a->argc > 1) ? notags(trim($a->argv[1])) : '');
 	$mentions   = (($a->argc > 2 && $a->argv[2] === 'mention') ? true : false);
@@ -31,22 +41,168 @@ function salmon_post(&$a) {
 
 	$importer = $r[0];
 
-	require_once('include/items.php');
+	// parse the xml
+
+	$dom = simplexml_load_string($xml,'SimpleXMLElement',0,NAMESPACE_SALMON_ME);
+
+
+	if($debugging)
+		file_put_contents('salmon.out', "\n" . print_r($dom,true) . "\n" , FILE_APPEND);
+
+	// figure out where in the DOM tree our data is hiding
+
+	if($dom->provenance->data)
+		$base = $dom->provenance;
+	elseif($dom->env->data)
+		$base = $dom->env;
+	elseif($dom->data)
+		$base = $dom;
+	
+	if(! $base) {
+		if($debugging)
+			file_put_contents('salmon.out', "\n" . 'Unable to find salmon data in XML' . "\n" , FILE_APPEND);
+		salmon_return(500);
+	}
+
+	// Stash the signature away for now. We have to find their key or it won't be good for anything.
+
+
+	$signature = base64url_decode($base->sig);
+	if($debugging)
+		file_put_contents('salmon.out', "\n" . 'Encoded Signature: ' . $base->sig . "\n" , FILE_APPEND);
+
+	// unpack our data element.
+
+	// strip whitespace
+	$data = str_replace(array(" ","\t","\r","\n"),array("","","",""),$base->data);
+	$type = $base->data[0]->attributes()->type[0];
+	$encoding = $base->encoding;
+	$alg = $base->alg;
+
+	$signed_data = $data;
+	// . '.' . base64url_encode($type) . '.' . base64url_encode($encoding) . '.' . base64url_encode($alg);
+	// decode it
+	$data = base64url_decode($data);
+
+	if($debugging)
+		file_put_contents('salmon.out', "\n" . 'Signed data:>>>' . $signed_data . "<<<\n" , FILE_APPEND);
+
+	// Remove the xml declaration
+	$data = preg_replace('/\<\?xml[^\?].*\?\>/','',$data);
 
 	// Create a fake feed wrapper so simplepie doesn't choke
 
-	$tpl = load_view_file('view/atom_feed.tpl');
+	$tpl = load_view_file('view/fake_feed.tpl');
 	
-	$base = substr($xml,strpos($xml,'<entry'));
+	$base = substr($data,strpos($data,'<entry'));
 
-	$xml = $tpl . $base . '</feed>';
+	$feedxml = $tpl . $base . '</feed>';
 
-salmon_return(500); // until the handler is finished
+	if($debugging) {
+		file_put_contents('salmon.out', 'Processed feed: ' . $feedxml . "\n", FILE_APPEND);
+	}
 
-//	consume_salmon($xml,$importer);
+	// Now parse it like a normal atom feed to scrape out the author URI
+	
+    $feed = new SimplePie();
+    $feed->set_raw_data($feedxml);
+    $feed->enable_order_by_date(false);
+    $feed->init();
+
+	if($debugging) {
+		file_put_contents('salmon.out', "\n" . 'Feed parsed.' . "\n", FILE_APPEND);
+	}
+
+
+	if($feed->get_item_quantity()) {
+		foreach($feed->get_items() as $item) {
+			$author = $item->get_author();
+			$author_link = unxmlify($author->get_link());
+			break;
+		}
+	}
+
+	if(! $author_link) {
+		if($debugging)
+			file_put_contents('salmon.out',"\n" . 'Could not retrieve author URI.' . "\n", FILE_APPEND);
+		salmon_return(500);
+	}
+
+	// Once we have the author URI, go to the web and find their public key
+
+	if($debugging) {
+		file_put_contents('salmon.out', "\n" . 'Fetching key for ' . $author_link . "\n", FILE_APPEND);
+	}
+
+	$key = get_salmon_key($author_link,$keyhash);
+
+	if(! $key) {
+		if($debugging)
+			file_put_contents('salmon.out',"\n" . 'Could not retrieve author key.' . "\n", FILE_APPEND);
+		salmon_return(500);
+	}
+
+	// Setup RSA stuff to verify the signature
+
+	set_include_path(get_include_path() . PATH_SEPARATOR . 'phpsec');
+
+	require_once('phpsec/Crypt/RSA.php');
+
+	$key_info = explode('.',$key);
+
+	$m = base64url_decode($key_info[1]);
+	$e = base64url_decode($key_info[2]);
+	if($debugging)
+		file_put_contents('salmon.out',"\n" . print_r($key_info,true) . "\n", FILE_APPEND);
+
+    $rsa = new CRYPT_RSA();
+    $rsa->signatureMode = CRYPT_RSA_SIGNATURE_PKCS1;
+    $rsa->setHash('sha256');
+
+    $rsa->modulus = new Math_BigInteger($m, 256);
+    $rsa->k = strlen($rsa->modulus->toBytes());
+    $rsa->exponent = new Math_BigInteger($e, 256);
+
+	// We should have everything we need now. Let's see if it verifies.
+
+    $verify = $rsa->verify($signed_data,$signature);
+
+	if(! $verify) {
+		if($debugging)
+			file_put_contents('salmon.out',"\n" . 'Message did not verify. Discarding.' . "\n", FILE_APPEND);
+		salmon_return(500);
+	}
+
+	if($debugging)
+		file_put_contents('salmon.out',"\n" . 'Message verified.' . "\n", FILE_APPEND);
+
+
+	/*
+	*
+	* If we reached this point, the message is good. Now let's figure out if the author is allowed to send us stuff.
+	*
+	*/
+
+	$r = q("SELECT * FROM `contact` WHERE `network` = 'stat' AND `lrdd` = '%s' AND `uid` = %d LIMIT 1",
+		dbesc($author_link),
+		intval($importer['uid'])
+	);
+	if(! count($r)) {
+		if($debugging)
+			file_put_contents('salmon.out',"\n" . 'Author unknown to us.' . "\n", FILE_APPEND);
+		salmon_return(500);
+	}	
+
+
+	require_once('include/items.php');
+
+	$hub = '';
+
+	consume_feed($feedxml,$importer,$r[0],$hub);
 
 	salmon_return(200);
 }
 
 
 
+
diff --git a/update.php b/update.php
index 3785cad2b2..65713583b3 100644
--- a/update.php
+++ b/update.php
@@ -75,4 +75,8 @@ function update_1008() {
 
 function update_1009() {
 	q("ALTER TABLE `user` ADD `allow_location` TINYINT( 1 ) NOT NULL DEFAULT '0' AFTER `default-location` ");
-}
\ No newline at end of file
+}
+
+function update_1010() {
+	q("ALTER TABLE `contact` ADD `lrdd` CHAR( 255 ) NOT NULL AFTER `url` ");
+}
diff --git a/view/fake_feed.tpl b/view/fake_feed.tpl
new file mode 100644
index 0000000000..49b17e34da
--- /dev/null
+++ b/view/fake_feed.tpl
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<feed xmlns="http://www.w3.org/2005/Atom"
+      xmlns:thr="http://purl.org/syndication/thread/1.0"
+      xmlns:at="http://purl.org/atompub/tombstones/1.0"
+      xmlns:media="http://purl.org/syndication/atommedia"
+      xmlns:dfrn="http://purl.org/macgirvin/dfrn/1.0" 
+      xmlns:as="http://activitystrea.ms/spec/1.0/"
+      xmlns:georss="http://www.georss.org/georss" >
+
+  <id>fake feed</id>
+  <title>fake title</title>
+
+  <updated>1970-01-01T00:00:00Z</updated>
+
+  <author>
+    <name>Fake Name</name>
+    <uri>http://example.com</uri>
+  </author>