Attaching invoices to email and SSL with local PhantomJS

I have PhantomJS installed locally and am having an issue where I get no PDF attached to emails with HTTPS enabled. Switch back to HTTP and everything works.

I’m using a self signed certificate

Laravel log shows Error 0 with HTTPS enabled. Is there something simple I’m not doing? (Debian Jessie, Invoice Ninja 3.2, PhantomJS 2.11 installed from the website not the repos)

As mentioned, everything works fine with HTTP

Just to confirm, when enabling HTTPS have you also updated the APP_URL to start with https?

The 0 error usually means the app can’t find PhantomJS in which case running which phantomjs can help to check the setting, but if it works with http this is less likely to be the problem.

I have, yep.

As a test I deliberately set the wrong path and have the exact same error (so correct path or not, I get Error 0)

I’m not sure. Here’s the PhantomJS script generated by the app, you can use it to try loading the invoice link from the command line. You’ll need to replace SET_LINK_HERE with your link.

/** * Set up page and script parameters */ var page = require('webpage').create(), system = require('system'), response = {}, debug = [], logs = [], procedure = {}; /** * Global variables */ /** * Define width & height of capture */ /** * Define paper size. */ /** * Define viewport size. */ var viewportWidth = 0, viewportHeight = 0; if (viewportWidth && viewportHeight) { debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Set viewport size ~ width: ' + viewportWidth + ' height: ' + viewportHeight); page.viewportSize = {width: viewportWidth, height: viewportHeight}; } /** * Define custom headers. */ page.customHeaders = {}; /** * Page settings */ page.settings.resourceTimeout = 5000; /** * On resource timeout */ page.onResourceTimeout = function (error) { response = error; response.status = error.errorCode; }; /** * On resource requested */ page.onResourceRequested = function (resource) { }; /** * On resource received */ page.onResourceReceived = function (resource) { if (!response.status) { response = resource; } }; /** * Handle page errors */ page.onError = function (msg, trace) { var error = {message: msg, trace: []}; trace.forEach(function (t) { error.trace.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : '')); }); logs.push(error); }; /** * Handle global errors */ phantom.onError = function (msg, trace) { var stack = []; trace.forEach(function (t) { stack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : '')); }); response.status = 500; response.content = msg; response.console = stack; system.stdout.write(JSON.stringify(response, undefined, 4)); phantom.exit(1); }; /** * Open page */ page.open('__SET_LINK_HERE__', 'GET', '', function (status) { page.evaluate(function () { debugger; var styles = {}; for (var property in styles) { document.body.style[property] = styles[property]; } }); var delay = 0; if (!delay) { return procedure.execute(status); } debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Delaying page render for ' + delay + ' second(s)'); window.setTimeout(function () { debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Rendering page after delaying for ' + delay + ' second(s)'); procedure.execute(status); }, (delay * 1000)); }); /** * Execute procedure */ procedure.execute = function (status) { if (status === 'success') { try { response.content = page.evaluate(function () { return document.getElementsByTagName('html')[0].innerHTML }); } catch (e) { response.status = 500; response.content = e.message; } } response.console = logs; system.stderr.write(debug.join('\\n') + '\\n'); system.stdout.write(JSON.stringify(response, undefined, 4)); phantom.exit(); };

So I created “phantom.js” using your script and set the URL (hopefully correctly) and this is what I get in the terminal with HTTPS:

\n{
“contentType”: null,
“headers”: [],
“id”: 1,
“redirectURL”: null,
“stage”: “end”,
“status”: null,
“statusText”: null,
“time”: “2017-04-12T13:51:07.278Z”,
“url”: “”,
“console”: []

If I switch to HTTP, I get very different output:

You need to use a URL for the client portal, you should have examples in your error log from the PhantomJS failures. The links include /view/…

Ah! OK done:

{
“contentType”: null,
“headers”: [],
“id”: 1,
“redirectURL”: null,
“stage”: “end”,
“status”: null,
“statusText”: null,
“time”: “2017-04-12T14:40:25.226Z”,
“url”: “”,
“console”: []
}

Same output as before with HTTPS

I’m not sure, it could be related to using a self signed certificate but it should work.

Is there a way I can pass “ignore-ssl-errors=true” to PhantomJS when it’s called?

Not that I’m aware of.

Ok. Of course, using PhantomJS Cloud works fine. Guess I’ll have to do that (or stop using SSL)

Thanks for looking at it Hillel. Much appreciated!

Out of interest, if anybody reads this and has SSL enabled and is using a local PhantomJS install, does it work fine?

Well, bit of an update!

If I run your script from the terminal with --ignore-ssl-errors=true, I get the same terminal output as I do when using HTTP.

So it looks like if there was a way to pass “–ignore-ssl-errors=true” when IN calls phantomjs, my problem would disappear.

HI Hiker,

Did you ever solve this. I’m about to try the same. I’ve read some with similar issues (not using Invoice Ninja) have tried the following:
–ssl-protocol=tlsv1
-ignore-ssl-errors=true
–ssl-protocol=any

but as you say, where to pass this in terms of invoice Ninja.

I’d say this was critical to all those who self host - i.e. the need to use SSL and PDF attachments locally.

From the example here it looks like you may be able to set it as an option.

$client->addOption('–ignore-ssl-errors=true');

https://github.com/jonnnnyw/php-phantomjs/issues/74

The relevant code in our app is here:

https://github.com/invoiceninja/invoiceninja/blob/master/app/Libraries/CurlUtils.php#L52

Thank you. I will give this a try when I install SSL. Not sure if this is affecting everyone who needs to attach PDF using local PhantomJS?

For reference, I believe you are suggesting:

Changing:

$client = Client::getInstance();
$client->isLazy();
$client->getEngine()->setPath($path);

To:

$client = Client::getInstance();
$client->addOption('--ignore-ssl-errors=true');
$client->isLazy();
$client->getEngine()->setPath($path);

That’s correct, you may also want to try adding:

$client->addOption('–ssl-protocol=tlsv1');
$client->addOption('–web-security=false');