pprf.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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, "Provided 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. // Register name
  86. if($message_info["type"] == 32) {
  87. $ppcl = get_ppcl();
  88. verify_collection_message($handle, $ppcl);
  89. $member_name_size = unpack("C", fread($handle, 1))[1];
  90. $member_name = fread($handle, $member_name_size);
  91. $member = get_member($ppcl, $member_name);
  92. $signed_name_size = unpack("v", fread($handle, 2))[1];
  93. $signed_name = fread($handle, $signed_name_size);
  94. $name = sodium_crypto_sign_open($signed_name, $member->signing_public_key);
  95. if($name == null) {
  96. send_failure(11, "Could not verify signed name");
  97. }
  98. if(strtolower(substr($name, strlen($name)-5, 5)) != ".ppub") {
  99. send_failure(7, "Name must end in \".ppub\"");
  100. }
  101. $path = PUBLICATION_DIR . "/" . $name;
  102. if(file_exists($path)) {
  103. send_failure(8, "Name already exists");
  104. }
  105. file_put_contents($path, "");
  106. send_confirmation();
  107. }
  108. // Begin upload
  109. if($message_info["type"] == 33) {
  110. $ppcl = get_ppcl();
  111. verify_collection_message($handle, $ppcl);
  112. $details = unpack("Pfile_size/Cname_size", fread($handle, 9));
  113. $member_name = fread($handle, $details["name_size"]);
  114. $member = get_member($ppcl, $member_name);
  115. // Create upload session
  116. $token = random_bytes(16);
  117. $key = sodium_crypto_box_keypair();
  118. $session_data = pack("P", $details["file_size"]);
  119. $session_data .= $key;
  120. $session_data .= pack("C", $details["name_size"]);
  121. $session_data .= $member_name;
  122. file_put_contents(PPRF_DATA_DIR . "/upload-session-info-" . bin2hex($token), $session_data);
  123. file_put_contents(PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($token), "");
  124. $session_auth = sodium_crypto_box_publickey($key);
  125. $session_auth .= $token;
  126. // Build and send reply
  127. $challenge = sodium_crypto_box_seal($session_auth, $member->sealing_public_key);
  128. $reply = pack("v", strlen($challenge));
  129. $reply .= $challenge;
  130. $reply .= pack("V", PPRF_MAX_UPLOAD_CHUNK_SIZE);
  131. send_message(160, $reply);
  132. }
  133. // Upload message
  134. if($message_info["type"] == 35) {
  135. $ppcl = get_ppcl();
  136. verify_collection_message($handle, $ppcl);
  137. $session_header = read_upload_session_message($handle);
  138. $session = get_upload_session($session_header);
  139. $path = PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($session_header["token"]);
  140. $offset = unpack("P", fread($handle, 8))[1];
  141. $member = get_member($ppcl, $session["member_name"]);
  142. if($offset != filesize($path)) {
  143. send_failure(6, "Invalid offset, got " . $offset . " expected " . filesize($path));
  144. }
  145. $data_size = $message_info["size"] - 9; // Message size minus headers
  146. $data = fread($handle, $data_size);
  147. // Verify data
  148. $checksum = hash("sha512", $data, true);
  149. verify_upload_auth($session_header, $session, $member, $checksum);
  150. file_put_contents($path, $data, FILE_APPEND);
  151. send_confirmation();
  152. }
  153. // Finalise upload message
  154. if($message_info["type"] == 34) {
  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. $upload_path = PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($session_header["token"]);
  160. $member = get_member($ppcl, $session["member_name"]);
  161. $flags = unpack('C', fread($handle, 1))[1];
  162. $dest_len = unpack('v', fread($handle, 2))[1];
  163. $dest_name = fread($handle, $dest_len);
  164. $dest_path = PUBLICATION_DIR . "/" . $dest_name;
  165. $bsdiff_old_checksum = null;
  166. // If cancel flag set
  167. if(($flags & 1 << 0) != 0) {
  168. cleanup_upload_session($session_header["token"]);
  169. send_confirmation();
  170. }
  171. // If BSDIFF flag set
  172. if(($flags & 1 << 2) != 0) {
  173. $bsdiff_old_checksum = fread($handle, 64);
  174. }
  175. if(!file_exists($dest_path)) {
  176. cleanup_upload_session($session_header["token"]);
  177. send_failure(9, "The name \"" . $dest_name . "\" is not registered");
  178. }
  179. // If overwrite flag NOT set, and file is not empty
  180. if(($flags & 1 << 1) == 0 && filesize($dest_path) != 0) {
  181. cleanup_upload_session($session_header["token"]);
  182. send_failure(10, "The destination \"" . $dest_name . "\" is not empty");
  183. }
  184. // Verify data
  185. $checksum = hash_file("sha512", $upload_path, true);
  186. verify_upload_auth($session_header, $session, $member, $checksum);
  187. // Copy to destination
  188. if($bsdiff_old_checksum == null) {
  189. rename($upload_path, $dest_path);
  190. }
  191. else if(!ENABLE_PPRF_BSPATCH) {
  192. send_failure(1, "BSPATCH is not supported by this server");
  193. }
  194. else {
  195. $old_hash = hash_file("sha512", $dest_path, true);
  196. if($bsdiff_old_checksum != $old_hash) {
  197. cleanup_upload_session($session_header["token"]);
  198. send_failure(12, "Destination file does not match provided checksum");
  199. }
  200. $patched_path = PPRF_DATA_DIR . "/upload-session-patch-" . bin2hex($session_header["token"]);
  201. $output=null;
  202. $retval=null;
  203. exec(BSPATCH_PATH . " " . escapeshellarg($dest_path) . " " . escapeshellarg($patched_path) . " " . escapeshellarg($upload_path), $output, $retval);
  204. if($retval != 0) {
  205. cleanup_upload_session($session_header["token"]);
  206. send_failure(13, "bspatch failed with exit code " . $retval);
  207. }
  208. rename($patched_path, $dest_path);
  209. }
  210. cleanup_upload_session($session_header["token"]);
  211. send_confirmation();
  212. }
  213. send_failure(1, "Messages of type " . $message_info["type"] . " are not supported by this server");
  214. ?>