pprf.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <?php
  2. include("config.php");
  3. include_once("ppcl.php");
  4. function send_message($type, $message) {
  5. header("Content-type: application/pprf");
  6. echo("PPRF\x00");
  7. $len = strlen($message) + 1;
  8. echo(pack("PC", $len, $type));
  9. echo($message);
  10. exit();
  11. }
  12. function send_failure($code, $message) {
  13. $len = strlen($message);
  14. $msg = pack("vv", $code, $len);
  15. $msg .= $message;
  16. send_message(193, $msg);
  17. }
  18. function send_confirmation() {
  19. send_message(192, "");
  20. }
  21. function get_ppcl() {
  22. $ppcl = new Ppcl();
  23. $ppcl->from_string(file_get_contents(PUBLICATION_DIR . "/collection.ppcl"));
  24. return $ppcl;
  25. }
  26. function get_member($ppcl, $name) {
  27. foreach($ppcl->members as $m) {
  28. if($m->name == $name) {
  29. return $m;
  30. }
  31. }
  32. send_failure(3, "No member \"" . $member_name . "\" in collection");
  33. }
  34. function verify_collection_message($handle, $ppcl) {
  35. $id = fread($handle, SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES);
  36. if($ppcl->identifier != $id) {
  37. send_failure(2, "Requested collection not hosted by this server");
  38. }
  39. }
  40. function read_upload_session_message($handle) {
  41. $data = array();
  42. $tsize = unpack("v", fread($handle, 2))[1];
  43. $data["token"] = fread($handle, $tsize);
  44. $asize = unpack("v", fread($handle, 2))[1];
  45. $data["auth"] = fread($handle, $asize);
  46. return $data;
  47. }
  48. function get_upload_session($session_header) {
  49. $file = fopen(PPRF_DATA_DIR . "/upload-session-info-" . bin2hex($session_header["token"]), 'rb');
  50. if($file == null) {
  51. send_failure(4, "Invalid upload session token");
  52. }
  53. $session = array();
  54. $session["file_size"] = unpack("P", fread($file, 8))[1];
  55. $session["key"] = fread($file, SODIUM_CRYPTO_BOX_KEYPAIRBYTES);
  56. $name_size = unpack("C", fread($file, 1))[1];
  57. $session["member_name"] = fread($file, $name_size);
  58. return $session;
  59. }
  60. function verify_upload_auth($session_header, $session, $member, $checksum) {
  61. $decrypted = sodium_crypto_box_seal_open($session_header["auth"], $session["key"]);
  62. if($decrypted == null) {
  63. send_failure(5, "Could not decrypt upload auth");
  64. }
  65. $verified = sodium_crypto_sign_open($decrypted, $member->signing_public_key);
  66. if($verified == null) {
  67. send_failure(5, "Could not verify upload auth");
  68. }
  69. if($verified != $checksum) {
  70. send_failure(5, "Invalid checksum on upload auth");
  71. }
  72. }
  73. function cleanup_upload_session($token) {
  74. unlink(PPRF_DATA_DIR . "/upload-session-info-" . bin2hex($token));
  75. unlink(PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($token));
  76. }
  77. function read_authenticated_message($handle, $ppcl) {
  78. $data = array();
  79. $name_len = unpack("C", fread($handle, 1))[1];
  80. $data["member_name"] = fread($handle, $name_len);
  81. $auth_len = unpack("v", fread($handle, 2))[1];
  82. $data["authentication"] = fread($handle, $auth_len);
  83. $data["member"] = get_member($ppcl, $data["member_name"]);
  84. return $data;
  85. }
  86. if(!ENABLE_PPRF) {
  87. send_failure(1, "PPRF has been disabled on this server");
  88. }
  89. $handle = fopen("php://input", "rb");
  90. if(fread($handle, 5) != "PPRF\x00") {
  91. send_failure(0, "Stream did not begin with PPRF magic number.");
  92. }
  93. $message_info = unpack("Psize/Ctype", fread($handle, 9));
  94. // Get collection
  95. if($message_info["type"] == 6) {
  96. $ppcl = get_ppcl();
  97. verify_collection_message($handle, $ppcl);
  98. $data = file_get_contents(PUBLICATION_DIR . "/collection.ppcl");
  99. $message = pack("V", strlen($data));
  100. $message .= $data;
  101. send_message(135, $message);
  102. }
  103. // Register name
  104. if($message_info["type"] == 32) {
  105. $ppcl = get_ppcl();
  106. verify_collection_message($handle, $ppcl);
  107. $auth = read_authenticated_message($handle, $ppcl);
  108. $member = $auth["member"];
  109. $name_size = unpack("C", fread($handle, 1))[1];
  110. $name = fread($handle, $name_size);
  111. $name_auth = sodium_crypto_sign_open($auth["authentication"], $member->signing_public_key);
  112. if($name_auth == null) {
  113. send_failure(11, "Could not authenticate register name request");
  114. }
  115. if($name_auth != hash("sha512", $name, true)) {
  116. send_failure(11, "Invalid checksum");
  117. }
  118. if(strtolower(substr($name, strlen($name)-5, 5)) != ".ppub") {
  119. send_failure(7, "Name must end in \".ppub\"");
  120. }
  121. $path = PUBLICATION_DIR . "/" . $name;
  122. if(file_exists($path)) {
  123. send_failure(8, "Name already exists");
  124. }
  125. file_put_contents($path, "");
  126. send_confirmation();
  127. }
  128. // Begin upload
  129. if($message_info["type"] == 33) {
  130. $ppcl = get_ppcl();
  131. verify_collection_message($handle, $ppcl);
  132. $details = unpack("Pfile_size/Cname_size", fread($handle, 9));
  133. $member_name = fread($handle, $details["name_size"]);
  134. $member = get_member($ppcl, $member_name);
  135. // Create upload session
  136. $token = random_bytes(16);
  137. $key = sodium_crypto_box_keypair();
  138. $session_data = pack("P", $details["file_size"]);
  139. $session_data .= $key;
  140. $session_data .= pack("C", $details["name_size"]);
  141. $session_data .= $member_name;
  142. file_put_contents(PPRF_DATA_DIR . "/upload-session-info-" . bin2hex($token), $session_data);
  143. file_put_contents(PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($token), "");
  144. $session_auth = sodium_crypto_box_publickey($key);
  145. $session_auth .= $token;
  146. // Build and send reply
  147. $challenge = sodium_crypto_box_seal($session_auth, $member->sealing_public_key);
  148. $reply = pack("v", strlen($challenge));
  149. $reply .= $challenge;
  150. $reply .= pack("V", PPRF_MAX_UPLOAD_CHUNK_SIZE);
  151. send_message(160, $reply);
  152. }
  153. // Upload message
  154. if($message_info["type"] == 35) {
  155. $ppcl = get_ppcl();
  156. verify_collection_message($handle, $ppcl);
  157. $session_header = read_upload_session_message($handle);
  158. $session = get_upload_session($session_header);
  159. $path = PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($session_header["token"]);
  160. $offset = unpack("P", fread($handle, 8))[1];
  161. $chunk_size = unpack("P", fread($handle, 8))[1];
  162. $chunk_path = PPRF_DATA_DIR . "/upload-session-file-chunk-offset-" . $offset . "-" . bin2hex($session_header["token"]);
  163. $member = get_member($ppcl, $session["member_name"]);
  164. if($offset != filesize($path)) {
  165. $expected = filesize($path);
  166. cleanup_upload_session($session_header["token"]);
  167. send_failure(6, "Invalid offset, got " . $offset . " expected " . $expected);
  168. }
  169. $read = 0;
  170. while($read < $chunk_size) {
  171. $data = fread($handle, $chunk_size);
  172. file_put_contents($chunk_path, $data, FILE_APPEND);
  173. file_put_contents($path, $data, FILE_APPEND);
  174. $read += strlen($data);
  175. }
  176. // Verify data
  177. $checksum = hash_file("sha512", $chunk_path, true);
  178. unlink($chunk_path);
  179. error_log("Received " . strlen($data) . " bytes from " . $message_info["size"] . " byte message");
  180. verify_upload_auth($session_header, $session, $member, $checksum);
  181. send_confirmation();
  182. }
  183. // Finalise upload message
  184. if($message_info["type"] == 34) {
  185. $ppcl = get_ppcl();
  186. verify_collection_message($handle, $ppcl);
  187. $session_header = read_upload_session_message($handle);
  188. $session = get_upload_session($session_header);
  189. $upload_path = PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($session_header["token"]);
  190. $member = get_member($ppcl, $session["member_name"]);
  191. $flags = unpack('C', fread($handle, 1))[1];
  192. $dest_len = unpack('v', fread($handle, 2))[1];
  193. $dest_name = fread($handle, $dest_len);
  194. $dest_path = PUBLICATION_DIR . "/" . $dest_name;
  195. $flag_cancel = ($flags & 1 << 0) != 0;
  196. $flag_replace = ($flags & 1 << 1) != 0;
  197. $flag_vcdiff = ($flags & 1 << 2) != 0;
  198. // If cancel flag set
  199. if($flag_cancel) {
  200. cleanup_upload_session($session_header["token"]);
  201. send_confirmation();
  202. }
  203. if(!file_exists($dest_path)) {
  204. cleanup_upload_session($session_header["token"]);
  205. send_failure(9, "The name \"" . $dest_name . "\" is not registered");
  206. }
  207. // If overwrite flag NOT set, and file is not empty
  208. if(!$flag_replace && filesize($dest_path) != 0) {
  209. cleanup_upload_session($session_header["token"]);
  210. send_failure(10, "The destination \"" . $dest_name . "\" is not empty");
  211. }
  212. // If destination is already published
  213. foreach($ppcl->publications as $pub) {
  214. if($pub->name == $dest_name) {
  215. send_failure(15, "The destination \"" . $dest_name . "\" cannot be overwritten as it is currently published");
  216. }
  217. }
  218. // Verify data
  219. $checksum = hash_file("sha512", $upload_path, true);
  220. verify_upload_auth($session_header, $session, $member, $checksum);
  221. // Copy to destination
  222. if(!$flag_vcdiff) {
  223. rename($upload_path, $dest_path);
  224. }
  225. else if(!ENABLE_PPRF_VCDIFF) {
  226. send_failure(1, "VCDIFF is not supported by this server");
  227. }
  228. else {
  229. $old_hash = hash_file("sha512", $dest_path, true);
  230. $patched_path = PPRF_DATA_DIR . "/upload-session-patch-" . bin2hex($session_header["token"]);
  231. $output=null;
  232. $retval=null;
  233. exec(XDELTA3_PATH . " -d -s " . escapeshellarg($dest_path) . " " . escapeshellarg($upload_path) . " " . escapeshellarg($patched_path), $output, $retval);
  234. if($retval != 0) {
  235. cleanup_upload_session($session_header["token"]);
  236. send_failure(13, "xdelta3 failed with exit code " . $retval);
  237. }
  238. rename($patched_path, $dest_path);
  239. }
  240. cleanup_upload_session($session_header["token"]);
  241. send_confirmation();
  242. }
  243. // Publish message
  244. if($message_info["type"] == 36) {
  245. $ppcl = get_ppcl();
  246. verify_collection_message($handle, $ppcl);
  247. $auth = read_authenticated_message($handle, $ppcl);
  248. $str_len = unpack("v", fread($handle, 2))[1];
  249. $pub_str = fread($handle, $str_len);
  250. try {
  251. $pub_entry = new CollectionPublication($pub_str, $ppcl->members);
  252. $signature = sodium_crypto_sign_open($auth["authentication"], $auth["member"]->signing_public_key);
  253. if($signature == null) {
  254. send_failure(5, "Could not verify member signature");
  255. }
  256. $expected_auth = hash("sha512", $pub_str, true);
  257. $expected_auth .= $ppcl->current_state_token;
  258. if($expected_auth != $signature) {
  259. send_failure(5, "Invalid authorisation token");
  260. }
  261. array_push($ppcl->publications, $pub_entry);
  262. file_put_contents(PUBLICATION_DIR . "/collection.ppcl", $ppcl->to_string());
  263. send_confirmation();
  264. }
  265. catch(Exception $e) {
  266. send_failure(14, $e->getMessage());
  267. }
  268. }
  269. // Unpublish message
  270. if($message_info["type"] == 37) {
  271. $ppcl = get_ppcl();
  272. verify_collection_message($handle, $ppcl);
  273. $auth = read_authenticated_message($handle, $ppcl);
  274. $name_len = unpack("C", fread($handle, 1))[1];
  275. $name = fread($handle, $name_len);
  276. $pub_entry = null;
  277. foreach($ppcl->publications as $pub) {
  278. if($pub->name == $name) {
  279. $pub_entry = $pub;
  280. break;
  281. }
  282. }
  283. if($pub_entry == null) {
  284. send_failure(16, "Publication \"" . $name . "\" not found");
  285. }
  286. $signature = sodium_crypto_sign_open($auth["authentication"], $auth["member"]->signing_public_key);
  287. if($signature == null) {
  288. send_failure(5, "Could not verify member signature");
  289. }
  290. $expected_auth = hash("sha512", $name, true);
  291. $expected_auth .= $ppcl->current_state_token;
  292. if($expected_auth != $signature) {
  293. send_failure(5, "Invalid authorisation token");
  294. }
  295. try {
  296. $i = array_search($pub_entry, $ppcl->publications);
  297. array_splice($ppcl->publications, $i, 1);
  298. file_put_contents(PUBLICATION_DIR . "/collection.ppcl", $ppcl->to_string());
  299. send_confirmation();
  300. }
  301. catch(Exception $e) {
  302. send_failure(14, $e->getMessage());
  303. }
  304. }
  305. // Rebuild index message
  306. if($message_info["type"] == 39) {
  307. $ppcl = get_ppcl();
  308. verify_collection_message($handle, $ppcl);
  309. $auth = read_authenticated_message($handle, $ppcl);
  310. $signature = sodium_crypto_sign_open($auth["authentication"], $auth["member"]->signing_public_key);
  311. if($signature == null) {
  312. send_failure(5, "Could not verify member signature");
  313. }
  314. $expected_auth = "PPIX\xFF" . $ppcl->current_state_token;
  315. if($expected_auth != $signature) {
  316. send_failure(5, "Invalid authorisation token");
  317. }
  318. try {
  319. include_once("ppix-gen.php");
  320. generate_ppix_from_ppcl();
  321. }
  322. catch(Exception $e) {
  323. send_failure(12, $e->getMessage());
  324. }
  325. send_confirmation();
  326. }
  327. // // Get identity message
  328. // if($message_info["type"] == 41) {
  329. // $ppcl = get_ppcl();
  330. // verify_collection_message($handle, $ppcl);
  331. // $member_name_size = unpack("C", fread($handle, 1))[1];
  332. // $member_name = fread($handle, $member_name_size);
  333. // $member = get_member($ppcl, $member_name);
  334. // $message = pack("v", strlen($ppcl->shared_signature_key));
  335. // $message .= $ppcl->shared_signature_key;
  336. // $message = pack("v", strlen($ppcl->current_state_token));
  337. // $message .= $ppcl->current_state_token;
  338. // $message .= pack("v", strlen($member->collection_secret));
  339. // $message .= $member->collection_secret;
  340. // send_message(161, $message);
  341. // }
  342. send_failure(1, "Messages of type " . $message_info["type"] . " are not supported by this server");
  343. ?>