pprf.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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 chunk 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 chunk auth");
  68. }
  69. if($verified != $checksum) {
  70. send_failure(5, "Invalid checksum on upload chunk 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. if(!ENABLE_PPRF) {
  78. send_failure(1, "PPRF has been disabled on this server");
  79. }
  80. $handle = fopen("php://input", "rb");
  81. if(fread($handle, 5) != "PPRF\x00") {
  82. send_failure(0, "Stream did not begin with PPRF magic number.");
  83. }
  84. $message_info = unpack("Psize/Ctype", fread($handle, 9));
  85. // Get collection
  86. if($message_info["type"] == 6) {
  87. $ppcl = get_ppcl();
  88. verify_collection_message($handle, $ppcl);
  89. $data = file_get_contents(PUBLICATION_DIR . "/collection.ppcl");
  90. $message = pack("V", strlen($data));
  91. $message .= $data;
  92. send_message(135, $message);
  93. }
  94. // Register name
  95. if($message_info["type"] == 32) {
  96. $ppcl = get_ppcl();
  97. verify_collection_message($handle, $ppcl);
  98. $member_name_size = unpack("C", fread($handle, 1))[1];
  99. $member_name = fread($handle, $member_name_size);
  100. $member = get_member($ppcl, $member_name);
  101. $signed_name_size = unpack("v", fread($handle, 2))[1];
  102. $signed_name = fread($handle, $signed_name_size);
  103. $name = sodium_crypto_sign_open($signed_name, $member->signing_public_key);
  104. if($name == null) {
  105. send_failure(11, "Could not verify signed name");
  106. }
  107. if(strtolower(substr($name, strlen($name)-5, 5)) != ".ppub") {
  108. send_failure(7, "Name must end in \".ppub\"");
  109. }
  110. $path = PUBLICATION_DIR . "/" . $name;
  111. if(file_exists($path)) {
  112. send_failure(8, "Name already exists");
  113. }
  114. file_put_contents($path, "");
  115. send_confirmation();
  116. }
  117. // Begin upload
  118. if($message_info["type"] == 33) {
  119. $ppcl = get_ppcl();
  120. verify_collection_message($handle, $ppcl);
  121. $details = unpack("Pfile_size/Cname_size", fread($handle, 9));
  122. $member_name = fread($handle, $details["name_size"]);
  123. $member = get_member($ppcl, $member_name);
  124. // Create upload session
  125. $token = random_bytes(16);
  126. $key = sodium_crypto_box_keypair();
  127. $session_data = pack("P", $details["file_size"]);
  128. $session_data .= $key;
  129. $session_data .= pack("C", $details["name_size"]);
  130. $session_data .= $member_name;
  131. file_put_contents(PPRF_DATA_DIR . "/upload-session-info-" . bin2hex($token), $session_data);
  132. file_put_contents(PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($token), "");
  133. $session_auth = sodium_crypto_box_publickey($key);
  134. $session_auth .= $token;
  135. // Build and send reply
  136. $challenge = sodium_crypto_box_seal($session_auth, $member->sealing_public_key);
  137. $reply = pack("v", strlen($challenge));
  138. $reply .= $challenge;
  139. $reply .= pack("V", PPRF_MAX_UPLOAD_CHUNK_SIZE);
  140. send_message(160, $reply);
  141. }
  142. // Upload message
  143. if($message_info["type"] == 35) {
  144. $ppcl = get_ppcl();
  145. verify_collection_message($handle, $ppcl);
  146. $session_header = read_upload_session_message($handle);
  147. $session = get_upload_session($session_header);
  148. $path = PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($session_header["token"]);
  149. $offset = unpack("P", fread($handle, 8))[1];
  150. $member = get_member($ppcl, $session["member_name"]);
  151. if($offset != filesize($path)) {
  152. send_failure(6, "Invalid offset, got " . $offset . " expected " . filesize($path));
  153. }
  154. $data_size = $message_info["size"] - 9; // Message size minus headers
  155. $data = fread($handle, $data_size);
  156. // Verify data
  157. $checksum = hash("sha512", $data, true);
  158. verify_upload_auth($session_header, $session, $member, $checksum);
  159. file_put_contents($path, $data, FILE_APPEND);
  160. send_confirmation();
  161. }
  162. // Finalise upload message
  163. if($message_info["type"] == 34) {
  164. $ppcl = get_ppcl();
  165. verify_collection_message($handle, $ppcl);
  166. $session_header = read_upload_session_message($handle);
  167. $session = get_upload_session($session_header);
  168. $upload_path = PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($session_header["token"]);
  169. $member = get_member($ppcl, $session["member_name"]);
  170. $flags = unpack('C', fread($handle, 1))[1];
  171. $dest_len = unpack('v', fread($handle, 2))[1];
  172. $dest_name = fread($handle, $dest_len);
  173. $dest_path = PUBLICATION_DIR . "/" . $dest_name;
  174. $bsdiff_old_checksum = null;
  175. // If cancel flag set
  176. if(($flags & 1 << 0) != 0) {
  177. cleanup_upload_session($session_header["token"]);
  178. send_confirmation();
  179. }
  180. // If BSDIFF flag set
  181. if(($flags & 1 << 2) != 0) {
  182. $bsdiff_old_checksum = fread($handle, 64);
  183. }
  184. if(!file_exists($dest_path)) {
  185. cleanup_upload_session($session_header["token"]);
  186. send_failure(9, "The name \"" . $dest_name . "\" is not registered");
  187. }
  188. // If overwrite flag NOT set, and file is not empty
  189. if(($flags & 1 << 1) == 0 && filesize($dest_path) != 0) {
  190. cleanup_upload_session($session_header["token"]);
  191. send_failure(10, "The destination \"" . $dest_name . "\" is not empty");
  192. }
  193. // If destination is already published
  194. foreach($ppcl->publications as $pub) {
  195. if($pub->name == $dest_name) {
  196. send_failure(15, "The destination \"" . $dest_name . "\" cannot be overwritten as it is currently published");
  197. }
  198. }
  199. // Verify data
  200. $checksum = hash_file("sha512", $upload_path, true);
  201. verify_upload_auth($session_header, $session, $member, $checksum);
  202. // Copy to destination
  203. if($bsdiff_old_checksum == null) {
  204. rename($upload_path, $dest_path);
  205. }
  206. else if(!ENABLE_PPRF_BSPATCH) {
  207. send_failure(1, "BSPATCH is not supported by this server");
  208. }
  209. else {
  210. $old_hash = hash_file("sha512", $dest_path, true);
  211. if($bsdiff_old_checksum != $old_hash) {
  212. cleanup_upload_session($session_header["token"]);
  213. send_failure(12, "Destination file does not match provided checksum");
  214. }
  215. $patched_path = PPRF_DATA_DIR . "/upload-session-patch-" . bin2hex($session_header["token"]);
  216. $output=null;
  217. $retval=null;
  218. exec(BSPATCH_PATH . " " . escapeshellarg($dest_path) . " " . escapeshellarg($patched_path) . " " . escapeshellarg($upload_path), $output, $retval);
  219. if($retval != 0) {
  220. cleanup_upload_session($session_header["token"]);
  221. send_failure(13, "bspatch failed with exit code " . $retval);
  222. }
  223. rename($patched_path, $dest_path);
  224. }
  225. cleanup_upload_session($session_header["token"]);
  226. send_confirmation();
  227. }
  228. // Publish message
  229. if($message_info["type"] == 36) {
  230. $ppcl = get_ppcl();
  231. verify_collection_message($handle, $ppcl);
  232. $str_len = unpack("v", fread($handle, 2))[1];
  233. $pub_str = fread($handle, $str_len);
  234. $auth_len = unpack("v", fread($handle, 2))[1];
  235. $authentication = fread($handle, $auth_len);
  236. try {
  237. $pub_entry = new CollectionPublication($pub_str, $ppcl->members);
  238. $member = get_member($ppcl, $pub_entry->member_name);
  239. $signature = sodium_crypto_sign_open($authentication, $member->signing_public_key);
  240. if($signature == null) {
  241. send_failure(5, "Could not verify member signature");
  242. }
  243. $expected_auth = hash("sha512", $pub_str, true);
  244. $expected_auth .= $ppcl->current_state_token;
  245. if($expected_auth != $signature) {
  246. send_failure(5, "Invalid authorisation token");
  247. }
  248. array_push($ppcl->publications, $pub_entry);
  249. file_put_contents(PUBLICATION_DIR . "/collection.ppcl", $ppcl->to_string());
  250. send_confirmation();
  251. }
  252. catch(Exception $e) {
  253. send_failure(14, $e->getMessage());
  254. }
  255. }
  256. // // Get identity message
  257. // if($message_info["type"] == 41) {
  258. // $ppcl = get_ppcl();
  259. // verify_collection_message($handle, $ppcl);
  260. // $member_name_size = unpack("C", fread($handle, 1))[1];
  261. // $member_name = fread($handle, $member_name_size);
  262. // $member = get_member($ppcl, $member_name);
  263. // $message = pack("v", strlen($ppcl->shared_signature_key));
  264. // $message .= $ppcl->shared_signature_key;
  265. // $message = pack("v", strlen($ppcl->current_state_token));
  266. // $message .= $ppcl->current_state_token;
  267. // $message .= pack("v", strlen($member->collection_secret));
  268. // $message .= $member->collection_secret;
  269. // send_message(161, $message);
  270. // }
  271. send_failure(1, "Messages of type " . $message_info["type"] . " are not supported by this server");
  272. ?>