Commits

JanKanis committed 8a49e42 Draft

Some refactorings and bugfixes:

- do (nearly) all subprocess interaction from one point through exec_command
- correctly escape the filename in http headers on file download
- don't replace %0D%0A in editor output
- handle errors when saving editor result

Comments (0)

Files changed (1)

     return "cd ".escapeshellarg($dir)."\n".$cmd;
 }
 
-/* executes a command in the given working directory and returns the output */
+/* executes a command in the given working directory and returns output */
 function exec_cwd($cmd, $directory) {   
-    $io = array();
-    $p = proc_open(add_dir($cmd, $directory), array(1 => array('pipe', 'w')), $io);
-    $return = '';
-    while(!feof($io[1])) {
-        $return .= fread($io[1], 8192);
-    }
-    fclose($io[1]);
-    proc_close($p);
-    return $return;
+    list($status, $stdout, $stderr) = exec_command($cmd, $directory);
+    return $stdout;
 }
 
 /* return exit code of command */
 function exec_test_cwd($cmd, $directory) {
-    $io = null;
-    $p = proc_open(add_dir($cmd, $directory), array(), $io);
-    return proc_close($p);
+    list($status, $stderr, $stderr) = exec_command($cmd, $directory);
+    return $status;
 }
 
+/* 
+ * Where the real magic happens
+ *
+ * $mergeoutputs says if the command's stdout and stderr should be separated 
+ * or merged into a single string.
+ * $fd9 adds an extra pipe on file descriptor 9 to the process, used for out 
+ * of band communication.
+ */
+function exec_command($cmd, $dir, $mergeoutput=False, $fd9=False) {
+
+    $io = array();
+    $pipes = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w'));
+    if($fd9) $pipes[9] = array('pipe', 'w');
+    $p = proc_open(add_dir($command, $_SESSION['cwd']), $pipes, $io);
+
+    /* 
+     * Read output using stream_select. Reading the pipes sequentially could
+     * potentially cause a deadlock if the subshell would write a large 
+     * ammount of data to pipe 2 (stderr), while we are reading pipe 1. The
+     * subshell would then block waiting for us to read pipe 2, and we would
+     * block waiting for the subshell to write to pipe 1, resulting in a 
+     * deadlock.
+     */
+
+    // set all streams to nonblocking mode, so we can read them all at once 
+    // below
+    foreach($io as $pipe) {
+        stream_set_blocking($pipe, 0);
+    }
+
+    $out = $err = $out9 = '';
+
+    while (True) {
+        // we need to recreate $read each time, because it gets modified in
+        // stream_select. Also, we just want to select on those pipes that are
+        // not closed yet. 
+        $read = array();
+        foreach($io as $pipe){
+            if(!feof($pipe))
+                $read[] = $pipe;
+        }
+
+        // break out if nothing more to read
+        if(count($read) == 0) 
+            break;
+
+        // define these because we must pass something by reference
+        $write = null;
+        $except = null;
+
+        // wait for the subshell to write to any of the pipes
+        stream_select($read, $write, $except, 10000);
+
+        // and read them. We don't bother to see which one is ready, we just 
+        // try them all. That's why we put them in nonblocking mode. 
+        $out .= fgets($io[1]);
+        if ($mergeoutput) {
+            $out .= fgets($io[2]);
+        } else {
+            $err .= fgets($io[2]);
+        }
+        if ($fd9) {
+            $out9 .= fgets($io[9]);
+        }
+    }
+
+    fclose($io[1]);
+    fclose($io[2]);
+    fclose($io[9]);
+    $status = proc_close($p);
+    $ret = array($status, $out);
+    if (!$mergeoutputs) $ret[] = $err;
+    if ($fd9) $ret[] = $out9;    
+    return $ret;
+}
+
+
 
 /* initialize everything */
 
         $cmd."\n".
         "pwd >&9\n";
 
-    $io = array();
-    $p = proc_open(add_dir($command, $_SESSION['cwd']),
-                   array(1 => array('pipe', 'w'),
-                         2 => array('pipe', 'w'),
-                         9 => array('pipe', 'w')),
-                   $io);
-
-    $newcwd = '';
-
-    /* 
-     * Read output using stream_select. Reading the pipes sequentially could
-     * potentially cause a deadlock if the subshell would write a large 
-     * ammount of data to pipe 2 (stderr), while we are reading pipe 1. The
-     * subshell would then block waiting for us to read pipe 2, and we would
-     * block waiting for the subshell to write to pipe 1, resulting in a 
-     * deadlock.
-     */
-
-    // set all streams to nonblocking mode, so we can read them all at once 
-    // below
-    foreach($io as $pipe) {
-        stream_set_blocking($pipe, 0);
-    }
-
-    while (True) {
-        
-        // we need to recreate $read each time, because it gets modified in
-        // stream_select. Also, we just want to select on those pipes that are
-        // not closed yet. 
-        $read = array();
-        foreach($io as $pipe){
-            if(!feof($pipe))
-                $read[] = $pipe;
-        }
-
-        // break out if nothing more to read
-        if(count($read) == 0) 
-            break;
-
-        // define these because we must pass something by reference
-        $write = null;
-        $except = null;
-
-        // wait for the subshell to write to any of the pipes
-        stream_select($read, $write, $except, 10000);
-
-        // and read them. We don't bother to see which one is ready, we just 
-        // try them all. That's why we put them in nonblocking mode. 
-        $_SESSION['output'] .= htmlescape(fgets($io[1]));
-        $_SESSION['output'] .= htmlescape(fgets($io[2]));
-        $newcwd .= fgets($io[9]);
-
-    }
+    list($status, $out, $newcwd) = exec_command($command, $_SESSION['cwd'], True, True);
 
     // trim because 'pwd' adds a newline
     if(strlen($newcwd) > 0 && $newcwd{0} == '/')
         $_SESSION['cwd'] = trim($newcwd);
-    
-    fclose($io[1]);
-    fclose($io[2]);
-    fclose($io[9]);
-    proc_close($p);
-}
+
+    return htmlescape($out);
+}    
 
 
 function builtin_download($arg) {
 
     $filesize = trim(exec_cwd("stat -c%s ".escapeshellarg($arg), $_SESSION['cwd']));
 
+    // We can't use exec_command because we need access to the pipe
     $io = array();
     $p = proc_open(add_dir('cat '.escapeshellarg($arg), $_SESSION['cwd']), 
                    array(1 => array('pipe', 'w')), $io);
+
+    /* Passing a filename correctly in a content disposition header is nigh 
+     * impossible. If the filename is unsafe, we just pass nothing and let the
+     * user choose himself. 
+     * The 'rules' are at http://tools.ietf.org/html/rfc6266#appendix-D
+     * If problematic characters are encountered we use the filename*= form, 
+     * user agents that don't support that don't get a filename hint. 
+     */
+    $basename = basename($arg);
+    // match non-ascii, non printable, and '%', '\', '"'. 
+    if (preg_match('/[\x00-\x1F\x80-\xFF\x7F%\\\\"]/', $basename)) {
+        // Assume UTF-8 on the file system, since there's no way to check
+        $filename_hdr = "filename*=UTF-8''".rawurlencode($basename).';';
+    } else {
+        $filename_hdr = 'filename="'.$basename.'";';
+    }
+
     header('Content-Description: File Transfer');
     header('Content-Type: application/octet-stream');
-    header('Content-Disposition: attachment; filename='.basename($arg));
+    header('Content-Disposition: attachment; '.$filename_hdr);
     header('Content-Transfer-Encoding: binary');
     header('Expires: 0');
     header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0');
-    header('Content-Length: '.$filesize);
+    if($filesize) header('Content-Length: '.$filesize);
 
     /* Read output from cat. */
     fpassthru($io[1]);
             $writeaccesswarning = true;
         }
 
-        $editorcontent = htmlspecialchars(str_replace(
-            "%0D%0D%0A", "%0D%0A", exec_cwd("cat $escarg", $_SESSION['cwd'])));		 
-        $showeditor = true;
+        list($status, $output, $error) = exec_command("cat $escarg", $_SESSION['cwd']);
+        if($status != 0) {
+            $_SESSION['output'] .= "editor: error: ".htmlescape($error)."\n";
+        } else {
+            $editorcontent = htmlspecialchars($output);
+            $showeditor = true;
+        }
     }
 
     return;
          * THE KERNELS BUFFER OF THE SUBSHELLS STDOUT.
          */
 
-        $errmsg = '';
-        fwrite($io[0], str_replace("%0D%0D%0A", "%0D%0A", $_POST["filecontent"]));
+        /* The docs are not entirely clear whether fwrite can write only part
+         * of the string to a pipe, but testing shows that php internally 
+         * splits up large writes into smaller ones, so everything gets 
+         * written.
+         */
+        if(fwrite($io[0], $content) === FALSE) {
+            $_SESSION['output'] .= "editor: Error saving editor content to ".htmlescape($_POST['filetoedit'])."\n";
+        }
         // close immediately to let the shell know we are done. 
         fclose($io[0]);
+        // also read any error messages
         while (!feof($io[2])) {
             $errmsg .= fread($io[2], 8192);
         }
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.