10

I need to execute an external program from Java (to convert a fodt file to pdf using libreoffice, it so happens) I know the precise command-line I need for the program:

/usr/bin/libreoffice --headless --convert-to pdf:'writer_pdf_Export' --outdir /home/develop/tomcat/mf/ROOT/private/docs/0/ /home/develop/tomcat/mf/ROOT/private/docs/0/35_invoice.fodt

and that works perfectly from the command line. But it does not work in Java using a ProcessBuilder:

java.io.IOException: Cannot run program "/usr/bin/libreoffice --headless --convert-to pdf:'writer_pdf_Export' --outdir /home/develop/tomcat/mf/ROOT/private/docs/0 /home/develop/tomcat/mf/ROOT/private/docs/0/35_invoice.fodt": java.io.IOException: error=2, No such file or directory

I tried some different approaches without success. Here is a sample of the last test

        List<String> command = new ArrayList<String>();
        command.add("/usr/bin/libreoffice");
        command.add("--headless");
        command.add("--convert-to pdf:'writer_pdf_Export' --outdir " + getDestinationDirectory(order) + " " + getInvoiceFilename() + ".fodt");
  
        ProcessBuilder builder = new ProcessBuilder(command);

        Process process = null;
        try {
            process = builder.start();
        } catch (IOException ex) {
            Logger.getLogger(Documents.class.getName()).log(Level.SEVERE, null, ex);
        }
        InputStream is = process.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);
        String line;
        try {
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException ex) {
            Logger.getLogger(Documents.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.out.println("Program terminated!");
Raedwald
  • 46,613
  • 43
  • 151
  • 237
Azathoth
  • 582
  • 1
  • 7
  • 29
  • 1
    This error is typical when file or the directory you try to move ou do something doesn't exists. Your command, problably is correct, but, your file or path didn't arrive the other side (path, for example). – Vinicius Lima May 10 '13 at 12:51
  • @ViniciusLima that is not the cause of the problem here. – Raedwald Oct 12 '20 at 13:53

4 Answers4

6

The ProcessBuilder constructors require each argument of the external program to be separate (in the form of an array or List of Strings). The first exception message you got,

Cannot run program "/usr/bin/libreoffice --headless --convert-to pdf:'writer_pdf_Export' --outdir /home/develop/tomcat/mf/ROOT/private/docs/0 /home/develop/tomcat/mf/ROOT/private/docs/0/35_invoice.fodt"

is not complaining that it can find a program named /usr/bin/libreoffice. It is complaining that it can not find a program with the very long and peculiar name "/usr/bin/libreoffice --headless --convert-to pdf:'writer_pdf_Export' --outdir /home/develop/tomcat/mf/ROOT/private/docs/0 /home/develop/tomcat/mf/ROOT/private/docs/0/35_invoice.fodt", because you concatenated the arguments into one String.

Instead of

command.add("--convert-to pdf:'writer_pdf_Export' --outdir " + getDestinationDirectory(order) + " " + getInvoiceFilename() + ".fodt")

and such like, split each of the arguments into its own call to List.add

command.add("--convert-to");
command.add("pdf:writer_pdf_Export");

command.add("--outdir");
command.add(getDestinationDirectory(order).toString());

command.add(getInvoiceFilename() + ".fodt");

Note that there are no apostrophes around "writer_pdf_Export" since those are shell meta-characters and are not required when you're constructing an array to pass to exec without an intermediating shell.

Raedwald
  • 46,613
  • 43
  • 151
  • 237
Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
  • I do not have any **filters/writer_pdf_Export.xcu** on my system but if I type thath command directly into a shell it works – Azathoth May 10 '13 at 13:30
  • Splitting in two (**command.add("--convert-to"); command.add("pdf:writer_pdf_Export");**) results in *Unknown option: --outdir * – Azathoth May 10 '13 at 13:34
  • @Azathoth, It sounds like we're making progress since its complaining about a later parameter. Have you tried splitting every argument into its own `add` as I suggested above? Maybe updating your question with your current code would make diagnosis easier. – Mike Samuel May 10 '13 at 16:03
1

Try this (keep it simple) ...

Process p = Runtime.getRuntime().exec("/usr/bin/libreoffice --headless --convert-to pdf:'writer_pdf_Export' --outdir "+ getDestinationDirectory(order)+" "+getInvoiceFilename()+".fodt");

Fully ...

    Process process = null;
    try {
            process = Runtime.getRuntime().exec("/usr/bin/libreoffice --headless --convert-to pdf:'writer_pdf_Export' --outdir "+ getDestinationDirectory(order)+" "+getInvoiceFilename()+".fodt");
    } catch (IOException ex) {
        Logger.getLogger(Documents.class.getName()).log(Level.SEVERE, null, ex);
    }
    BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
    String line;
    try {
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException ex) {
        Logger.getLogger(Documents.class.getName()).log(Level.SEVERE, null, ex);
    }
    br.close();
    System.out.println("Program terminated!");
xagyg
  • 9,562
  • 2
  • 32
  • 29
  • This is one of the first test made: *no error, no outpu and most of all ... no pdf* – Azathoth May 10 '13 at 12:58
  • It works from the command line? I found this https://bugs.freedesktop.org/show_bug.cgi?id=44486 – xagyg May 10 '13 at 13:02
  • Do you get the same error when you read both the process `InputStream` and the process `ErrorStream`? See `getInputStream` and `getErrorStream`. – xagyg May 10 '13 at 13:05
  • Can you `cd` to here `/home/develop/tomcat/mf/ROOT/private/docs/0`? What are the permission on the directory? ... and the output file? – xagyg May 10 '13 at 13:12
  • Yes, via shell works like a charm. Removed the underscores but nothing changes. My user has r/w permissions, yes. Actually this project runs locally with netbeans+tomcat and those belongs to the same user – Azathoth May 10 '13 at 13:16
  • @Azathoth, and your tomcat is not chroot jailed? – Mike Samuel May 10 '13 at 13:19
  • no chroot. If I use the version with a single String I cannot see an stream because *process = builder.start();* triggers a *NullpointerException*. Splitting the command into an array I see in the *InputStreamReader* Libreoffice complaining about the parameters (*Unknown option: --convert-to pdf:writer_pdf_Export*) – Azathoth May 10 '13 at 13:25
  • With my approach, you don't use the ProcessBuilder at all, so don't start anything. The `exec` does all the work. – xagyg May 10 '13 at 13:28
  • I've pasted the full code in my answer now. Is it any different to what you tried? Notice ProcessBuilder isn't used etc. – xagyg May 10 '13 at 13:35
  • As I said, if I run this way I cannot see a thing: no errors and no pdf too – Azathoth May 10 '13 at 13:38
  • Something were overlapping! now, without quotes the file is appeared! But ... can't believe. It works for the invoice, but the same command does'nt for the order confirmation!! Thank you (all)! – Azathoth May 10 '13 at 13:40
  • This is probably not too susceptible to [shell injection](http://en.wikipedia.org/wiki/Code_injection#Shell_injection), since `exec` does not spawn a full shell under the hood, but you might still run into problems with a servlet that interpolates request inputs into a string. An attacker who controls invoice filename could probably cause libreoffice to be called with any arguments, and if libreoffice opens a shell with one of those, then that attacker can escalate privileges. – Mike Samuel May 10 '13 at 16:02
  • @Mike Samuel yep, but in this case there is non interaction. The user places the order, i generate the documents (with fixed name), then I convert them to pdf and send them back to the user. – Azathoth May 11 '13 at 07:28
  • @Azathoth, I don't see how that changes things. A call to download a trojan onto your system works whether or not the attacker gets their documents while that download is running. – Mike Samuel May 11 '13 at 12:30
  • Perhaps I was not very clear (or I didn't understood what you are tring to tell me). There is no interaction between user (the customer) and server (speaking about files). A customer places an order (regular e-commerce); when the order is authorized the system generates the documents. Those documents are sended via email. *_invoice* and *_confirm* for the administrator in opendocument format and in pdf for the final customer. There are no upload, no download, no trojans, no injection in any form – Azathoth May 11 '13 at 13:31
  • @Azathoth, Agreed, if the customer has no control over either `getDestinationDirectory(order)` or `getInvoiceFilename()` then you're safe against shell injection. If they do or might in the future, and any command substring can reach a shell then you're at risk. – Mike Samuel May 11 '13 at 14:02
  • Yep, no one will ever have the opportunity to dictate an (unescaped) filename or worse a system path on my server, that's for sure! ^^ – Azathoth May 11 '13 at 15:21
1

I have tried every solution proposed in this thread and it does not work.

In my app (java webapp using TOMCAT in linux) it only works to create a shell script and execute the script. But you have to put the absolute path in the script, if not, it does not work ($HOME does not work). Besides, you can pass it arguments.

Example:

Runtime.getRuntime().exec("/home/user/myscript.sh param1");
demongolem
  • 9,474
  • 36
  • 90
  • 105
er_benji
  • 305
  • 4
  • 9
0

ProcessBuilder can be erratic sometimes with long command (executable path with lot of arguments.). What you can do is,

Assume your command string is String cmdString = "executableFilePath -param1 -param2"

Convert this to map

List cmdMap = Arrays.asList(cmdString.split(" "));

Pass this cmdMap to ProcessBuilder.

navaltiger
  • 884
  • 12
  • 27