Seite wählen

NETWAYS Blog

Automatic PDF Generation with Google Chrome

Many developers get, sooner or later, the task to generate PDF documents automatically. Though, less developers put their experiences and insights then into a blog-post to save others some hassle. So let me do you a favor by explaining how we utilize Google Chrome in headless mode to generate pretty PDF files from HTML.

I won’t go into the details why Google Chrome. If you found this blog entry I suppose you already tried other options without success or satisfying results. We sure have tried several other options (e.g. wkhtmltopdf, dompdf, tcpdf) but only Google Chrome provided us with the results we wanted.

Another advantage of Google Chrome is the possibility to instrument a remotely running instance. And that’s exactly how we instruct it to generate a PDF file for us. Though, not with Puppeteer but with plain chrome devtools protocol (CDP) communication over a websocket.

I’ll avoid any programming language specific examples. You can take a look here at our implementation in PHP. So, let’s start with it.

The Process to Print HTML to PDF

First you’ll need to connect with the browser by use of a websocket connection at: ws://10.11.12.13:9222/devtools/browser/79744167-25f0-41a8-9226-382fa2eb4d66

This is printed on stderr right at launch of the process and also available on the JSON api: 10.11.12.13:9222/json/version

The important bit is the browser id (79744167-25f0-41a8-9226-382fa2eb4d66) at the end of the URI. This changes every time the process is launched and can’t be configured.

Communicating with the browser now takes place over this socket by transmitting and receiving JSON messages. They are divided into four types:

Calls

These primarily originate from ourselves. They contain an id, a method and parameters:

{ „id“: <mixed>, „method“: <string>, „params“: <object> }

The id is chosen by us and should be different for each call. It’s sent back with the response so it’s possible to later associate it with the call we made. Though this is mostly relevant if you are not communicating synchronously, which we do. So this may just as well be an integer incremented by 1 each time.

Results

These are sent by the browser in response to an API call.

{ „id“: <mixed>, „result“: <mixed> }

Errors

If it’s not a response, it’s an error.

{ „id“: <mixed>, „error“: { „code“: <int>, „message“: <string> } }

Events

These may be sent by the browser at any time.

{ „method“: <string>, „params“: <object> }

Some of these are of interest to us. Some of them are not and can be ignored.

Operating the Browser As Usual

In order to let the browser print us a web page (or HTML) to PDF we need to instrument it the same as when we do it manually on the desktop.

First we need to open a new tab:

Call: { „id“: 1, „method“: „Target.createTarget“, „params“: { „url“: „about:blank“ } }

Result: { „id“: 1, „result“: { „targetId“: „418546565-d4f9-4d9f-8569-9ad8f9db7f9de“ } }

Now we have to communicate with the tab, which requires a new websocket connection to: ws://10.11.12.13:9222/devtools/page/418546565-d4f9-4d9f-8569-9ad8f9db7f9de

Before we can print anything we have to tell the browser where to load the content to print from. This may either be a URI (which then needs to be accessible for the browser) or raw HTML. I’ll stick to raw HTML here, since it’s the most flexible option anyway:

Call: { „id“: 2, „method“: „Page.setDocumentContent“, „params“: { „frameId“: „418546565-d4f9-4d9f-8569-9ad8f9db7f9de“, „html“: <html> } }

Result: { „id“: 2, „result“: { } }

The next step is the instruction to print the page’s content to PDF:

Call: { „id“: 3, „method“: „Page.printToPDF“, „params“: <object> }

Result: { „id: 3, „result“: { „data“: <string> } }

What parameters you can use to influence the generation (e.g. the layout) are outlined in the official documentation.

The string you will get back is probably encoded in Base64, so decode it and store it where you want. The PDF file has been successfully generated.

If you are planning to use a single process to generate multiple PDFs with, remember to clean up afterwards. (i.e. closing the tab) Otherwise you will soon have a memory usage issue.

Call: { „id“: 4, „method“: „Target.closeTarget“, „params“: { „targetId“: „418546565-d4f9-4d9f-8569-9ad8f9db7f9de“ } }

Result: { „id“: 4, „result“: { „success“: <bool> } }

I hope this proves useful to you or convinces you to give Google Chrome a try to generate pretty PDFs. 🙂

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer für Icinga Web 2, Icinga DB Web sowie alle möglichen anderen Module und Bibliotheken im Web Bereich zuständig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gemütlich. Passt das Wetter, geht's auch mal auf eines seiner Zweiräder. Motorisiert oder nicht.

Icinga Web 2 – More Goodies for Developers

Tuesday version 2.7 of Icinga Web 2 has been released and introduced some interesting new functionality for module developers. Now I’d like to tell you, my fellow colleague, some more details about this.

 

jQuery v3 – Migration Required

First a friendly warning. We’ve upgraded the jQuery version we ship to v3.4.1. This has previously been v2.1.0 so now with this major upgrade some deprecated methods and interfaces are gone.

Though, don’t worry, you don’t need to hurry to avoid everyone complaining your module is incompatible with v2.7. We also ship jQuery migrate now which ensures that the usage of removed methods/interfaces still works. It also emits console warnings if it detects such a usage. The warnings are not active by default. They only appear when using the non-minimized javascript code. Put _dev in your address bar to instruct Icinga Web 2 to serve the non-minimized version. (e.g. /icingaweb2/dashboard?_dev)

Then start using the front-end as usual. Interact with all widgets you’ve written your own Javascript for and look for console entries starting with JQMIGRATE. Any of these messages will only appear once, repeated usages are not reported. If you’ve got a warning then, consult jQuery migrate’s warnings.md in order to get hints how to solve it.

jQuery migrate will be removed with Icinga Web 2 version 2.8.0. While this is still some time ahead, this (and the note in the upgrade documentation) is probably the only warning.

 

Persistence and Collapsible Containers

While we’re at it, let’s stay with the topic of Javascript. If you don’t already know about the localStorage and sessionStorage, it’s now time to inform yourself. (That’s an entire blogpost if described thoroughly)

There’s now an abstraction layer for this shipped with Icinga Web 2. It hides all the handling of complex datatypes and conflicts with other apps using the storage from you. That’s the object Icinga.Storage which utilizes the localStorage by default but also supports the sessionStorage. Take a look here to see how this is used for Icinga Web 2’s sidebar.

Though, this is only the basic stuff. If you need to store more complex data and want to benefit from a storage’s event processing, take a look at the object Icinga.Storage.StorageAwareMap. This is a proxy for Map and allows to subscribe to change events of particular keys in the map. It also keeps track of a key’s age and removes it automatically if it hasn’t been accessed for 90 days.

Another new addition are collapsible HTML containers. This is provided by a behavior which makes use of the StorageAwareMap, a perfect example use-case.

Making a container collapsible is as easy as possible. Just apply the CSS class collapsible and you’re done. If you’re not satisfied with the default height, apply the data attribute data-visible-height and give it the desired height in pixels. (For table’s and ul’s or ol’s there is also data-visible-rows.) Then, if you fancy a custom control by which users expand or collapse the container you can pass a CSS selector to data-toggle-element which (if a direct descendant of the container) then acts as the toggle.

 

Custom XHR Without Dirty Hacks

Have you ever wanted/tried to process link clicks or form submissions by yourself? Well, I have and it was a nightmare every single time. Most of Icinga Web 2’s processing is fine. But of course there ever is this single behavior or side-effect which keeps getting in the way. This has now come to an end.

Meet data attribute data-no-icinga-ajax which does exactly what it’s name suggests. Applied to an element it causes Icinga Web 2 to ignore click and submit events triggered by the element itself and all descendants.

Couldn’t be more simple, can it?

 

Hooks For Everyone

Previously hooks were only processed for logged-in users with the permission to access the module providing the hook. This for example prevented the audit module to register logins from users without the permission module/audit and also didn’t allow to log failed logins.

When providing a hook it is now possible to have it run always. ($this->provideHook($name, $implementation = null, $alwaysRun = false);)

Another case of hooks not being processed was the issue that, unlike in the web front-end, enabled modules were not loaded automatically on the CLI. Thus also their hooks were not registered. Now this has changed and also on the CLI all enabled modules are automatically loaded. If you’ve previously loaded the modules explicitly this is not required anymore. If you don’t need any other modules and want to avoid the overhead of loading them, you can disable this of course.

If you still don’t have enough of this, there’s also an entirely new hook available: ConfigFormEventsHook This hook enables you to influence every configuration form of Icinga Web 2. Extending a form’s validation or doing additional work once submission succeeded is now on the table.

 

That’s it. I hope these things are as useful to you as they were to us. And remember, we don’t mind any suggestions to further improve the integration of modules. You are the developer, you’ll know best what’s… best.

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer für Icinga Web 2, Icinga DB Web sowie alle möglichen anderen Module und Bibliotheken im Web Bereich zuständig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gemütlich. Passt das Wetter, geht's auch mal auf eines seiner Zweiräder. Motorisiert oder nicht.

Verschachtelte Listen mit Sortable.js

Nachdem ich vor einiger Zeit die Ehre hatte Drag & Drop im Business Process Modul für Icinga Web 2 zu integrieren, dachte ich mir ich plaudere heute mal ein wenig aus dem Nähkästchen welche Stolpersteine ich dabei überwinden musste.

Aber erst einmal eine kleine Vorstellung der Bibliothek die eingesetzt wurde: Sortable.js 

Die Entscheidung hierfür fiel erst nicht leicht, da das Angebot jener Bibliotheken die sich Drag & Drop verschrieben haben, nicht besonders klein ist. Aber wo wir gerade von klein reden, ist das bereits der wichtigste Punkt der Sortable.js von anderen abhebt. Weil sie nur das nötigste an Funktionalität abdeckt und keine fancy Animationen nutzt die nicht nur Leistung fressen sondern auch Dateigröße ist diese Bibliothek mit 25kB (minimized) eines der Leichtgewichte unter ihren Vertretern. Außerdem existiert keine Abhängigkeit zu jQueryUI, das alleine hat bereits überzeugt.

Nun aber zum eigentlichen Thema. Während Sortable.js wunderbar mit einfachen Listen arbeiten kann, wird es etwas anspruchsvoller wenn es um verschachtelte Listen geht. Glücklicherweise wurde in diesem Segment in den letzen Wochen einiges getan und die Unterstützung erheblich verbessert. Dennoch gibt es etwas das immer noch existiert und mir einige ruhelose Nächte bereitet hat. Gut, so schlimm war es nun auch wieder nicht, dennoch, es könnte ja einigen genauso wie mir nicht sofort wie Schuppen von den Augen fallen.

Aber erst einmal die Demo die das Problem darstellt. Versucht einmal alles aus der roten Liste zu entfernen und dann wieder Elemente aus der blauen hineinzuschieben. Demo

Klappt nicht so ganz? Tja, das liegt daran, dass die rote Liste zu klein ist sobald keine Elemente mehr enthalten sind. Der findige Leser denkt jetzt eventuell daran der Liste eine Mindesthöhe zu geben. Gar nicht so falsch. Demo

Klappt dennoch nicht? Hehe, willkommen im Club. Ein Blick in die README.md von Sortable.js und man findet emptyInsertThreshold. Leider führt der Name dieser Option eher in die Irre, denn die Lösung ist sie nicht. Euer Blick sollte eher auf invertSwap fallen. Demo

Warum das nun funktioniert? Arr, das geht etwas über das Ziel des Beitrags hinaus. Ich kann euch aber folgendes ans Herz legen.

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer für Icinga Web 2, Icinga DB Web sowie alle möglichen anderen Module und Bibliotheken im Web Bereich zuständig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gemütlich. Passt das Wetter, geht's auch mal auf eines seiner Zweiräder. Motorisiert oder nicht.

Was mein ist, bleibt mein

Auch wenn mich das als Egoist darstellt, sollte das jeder in Betracht ziehen. Jedenfalls wenn es um potentiell sensible Daten geht.

Ein Beispiel

Jeder der häufig im Internet unterwegs ist, sei es nun privat oder geschäftlich, kennt das. Eine Anmelde-Maske.

Wer auf Sicherheit wert legt, nutzt hoffentlich überall ein anderes Passwort. Selbstverständlich sind die auch in einem Passwort-Manager wie Keepass oder Enpass hinterlegt und mit einem sicheren Master-Passwort gesichert.

Aber mal ganz ehrlich, wer klickt im Browser bei der Frage „Soll dieses Passwort gespeichert werden?“ nicht gerne auf Ja? Nun, ich hab es eine Zeit lang vermieden war aber jedes Mal traurig nicht auf Ja geklickt zu haben.

Warum? Weil ich eine üble Angewohnheit, wie viele andere wohl auch, habe.

Bequemlichkeit

Ich nutze ein Passwort niemals ein zweites Mal. Ich habe alle meine Passwörter in Keepass gespeichert. Diese Datenbank ist mit einem relativ komplexen jedoch noch leicht zu merkendem Master-Passwort versehen und mit meinem Yubikey gekoppelt. 2-Faktor Authentifizierung wie aus dem Buche.

Aber ich bin bequemlich. Jedes Mal Keepass aufzumachen und diese Prozedur durchzuführen, für jede Anmelde-Maske die ich im Laufe des Tages benutzen muss? Ein Graus. Selbstverständlich kann ich Keepass im Hintergrund offen lassen, aber das würde dem Gedanken der 2-Faktor Authenfizierung widersprechen. Sicher, Keepass schützt die Passwörter vor unerlaubten Speicherzugriffen und derlei Späßen, jedoch hab ich dennoch kein gutes Gefühl dabei.

Aber warum speichere ich dann nicht einfach die Passwörter mit Hilfe des Browsers? Werden die dort nicht auch sicher gespeichert? Na, selbstverständlich werden sie das. Doch das Problem ist ein ganz anderes.

Das schwache Glied

Die Frage ist nämlich nicht wie die Passwörter vom Browser gespeichert werden, sondern wie erneut auf sie zugegriffen wird.

Ich setze Ubuntu 18 ein. Hier werden derart gespeicherte Passwörter im GnuPG-Schlüsselbund hinterlegt. Dieser wird bei jedem Login auf dem System entsperrt. (Oder beim entsperren des Systems.)

Nun, der aufmerksame Leser wird sich nun denken können weshalb ich das als schwaches Glied in der Kette ansehe. Ich bin bequemlich, welch Überraschung. Wenn ich das System entsperren muss, möchte ich nicht erst ein super sicheres Passwort eintippen müssen. Erst recht nicht während jemand daneben steht/sitzt. Je komplexer das Passwort nämlich, desto langsamer tippe ich es. Je langsamer ich tippe, desto eher steigt die Gefahr mir schaut jemand dabei zu. (Kollegen vertraue ich selbstverständlich, aber man weiß ja nie wo man sonst ist.) Deshalb: Es ist ein einfaches Passwort das super schnell getippt ist.

Falls aber doch jemand, oder etwas, Kenntnis von diesem Passwort erlangt war alles für die Katz. Sofort sind alle Passwörter aus dem GnuPG-Schlüsselbund gefährdet. Da hilft nur eines.

Die Lücke schließen

Zum Glück hat meine Bequemlichkeit doch ihre Grenzen. Denn ich fahre mein DELL XPS 13 grundsätzlich immer herunter wenn ich es länger aus den Augen lasse.

Somit ist diese Lücke auf einen Schlag verschlossen sobald der gesamte Festplatten-Inhalt verschlüsselt ist. Und das ist er inzwischen. LUKS sei dank.

Auch hier kommt es auf die Qualität der gewählten Passwörter an, schließlich muss vor dem Start des Systems erst einmal alles entschlüsselt werden können. Aber Achtung: Ein zu schwaches Passwort ist erneut das schwache Glied.

Hier habe ich einen Kompromiss mit meiner Bequemlichkeit geschlossen. Ich habe zwei Möglichkeiten meine Festplatte zu entschlüsseln. Zum einen ein super sicheres Passwort (das nicht im Wörterbuch zu finden ist), zum anderen aber auch ein leicht zu merkendes. (Das aber auch nicht im Wörterbuch zu finden ist, jedenfalls nicht 1:1) Der Clou jedoch ist, das zweite (einfache) Passwort ist mit dem Yubikey gekoppelt.

Ein Hoch auf die 2-Faktor Authentifizerung

Man merkt es vielleicht. Ich bin ein Fan meines Yubikeys. Besser gesagt, meiner zwei Yubikeys. (Es könnte ja einer abhanden kommen) Auf die technischen Details gehe ich jetzt nicht mehr ein, das macht zum Teil bereits Marius. Doch kurz auflisten wofür ich ihn noch einsetze möchte ich:

  • GPG (Private subkeys auf dem Yubikey)
  • SSH (Dank GPG, Gunnar hatte hierzu bereits etwas geschrieben)
  • Github (FIDO U2F)

Außerdem ist mein zweiter (privater) Yubikey NFC fähig, ich kann ihn also super easy mit der Keepass App auf dem Smartphone nutzen.

Was mein ist, bleibt mein!

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer für Icinga Web 2, Icinga DB Web sowie alle möglichen anderen Module und Bibliotheken im Web Bereich zuständig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gemütlich. Passt das Wetter, geht's auch mal auf eines seiner Zweiräder. Motorisiert oder nicht.

DELL XPS13 – Ein anständiges Fliegengewicht mit kleinerer Klappe

Dies ist die Fortsetzung zum zickigen Leichtgewicht mit großer Klappe.
Nun war es mal wieder soweit. Nach Jahren als einiger der wenigen die bei Meetings kein Macbook vor sich stehen hatten, habe ich nun erneut ein DELL XPS13 erhalten. Diesmal ist es das Modell 9370 (vormals 9343) in der FHD Ausführung. Oh ja, was vorher ein QHD+ war ist nun kleiner. Aber dafür viel angenehmer. Seit Ubuntu 14.04 hat sich zwar einiges getan hinsichtlich HiDPI Unterstützung, jedoch scheitert es immer noch meist an einzelnen Applikationen. Aus diesem Grund habe ich seit langem schon nicht die native Auflösung von 3200×1600 Bildpunkten betrieben, sondern wie auch mein zweiter Bildschirm mit 1920×1080 Bildpunkten. Allerdings war eine gewisse Unschärfe nicht zu verhindern.
Nunja, das neue Modell hat nun FHD als native Auflösung und jegliche Probleme mit Unschärfe, zu kleiner Schrift oder schrägen Skalierungs-Artefakten sind nun Geschichte. Geschichte ist außerdem der Touchscreen, aber den hab ich eh nie gebraucht. Was hingegen vollkommen neu ist:

  • Es wirkt leichter. Ich habs nicht nachgewogen, aber es wirkt eindeutig leichter.
  • 3 (!) USB-C Ports (Das waren vorher 2 USB-A Ports)
  • 4 statt 2 CPU-Kerne. Power satt. (Aber auch Hitze, dazu später mehr)
  • Ganze 16 GB RAM. (Vorher mit 8GB kam ich schon hin und wieder an meine Grenzen)
  • Mit 512 GB SSD doppelter Speicherplatz als vorher. (Jetzt werd ich wohl weniger oft VMs löschen)
  • Eine Infrarot Kamera. (Ist wohl ein Überbleibsel aus der Windows Variante, könnte noch nützlich werden)
  • Oh, und das Tastatur Layout. Ich nutze gerne Home, End, PageUp und PageDown. Jetzt muss ich dafür keine akrobatischen Kunststücke mit dem Function-key mehr vollziehen!

Wieder einmal war auch Ubuntu vorinstalliert. Da ich allerdings diesmal FDE (Full Disk Encryption) einsetzen wollte musste das runter. Zuerst hatte ich versucht mit Dell Recovery neu zu installieren. Schließlich hat Dell einen eigenen Kernel mit Plattform spezifischen Verbesserungen entwickelt. Dummerweise jedoch ist scheinbar genau jener Kernel (oder irgendwas anderes in diesem Paket) inkompatibel mit LUKS (Quelle), denn egal welches Passwort ich gewählt hatte (zuletzt „test“), nach abgeschlossener Installation wurde keines von LUKS als richtig erkannt.
Gut, also hieß es nun das normale Ubuntu 18.04 mit dem generic Kernel zu installieren. Und siehe da, es lief perfekt. Und so läuft es auch jetzt noch. Kaum zu glauben, ist aber wahr. Okay, vielleicht nicht perfekt, aber immerhin gut genug für mich. Bisher sind mir keine Fehler aufgefallen. All die Probleme die ich initial mit dem vorherigen Modell (9343) hatte, traten nicht auf. Kein Tastatur-Lag. Kein Touchpad-Ghosting. Sound ging sofort. Nichts. Nicht einmal mit dmesg sind grobe Fehler oder Warnungen zu entdecken. Ja sogar der Philips Monitor mit USB-C Dock-Funktionalität wird mitsamt der an ihn angeschlossenen Peripherie anstandslos erkannt.
Der einzige Wermutstropfen, wie eingangs schon erwähnt, ist die Hitze-Entwicklung. Ich habe noch nicht nachgesehen ob ich im UEFI die Lüfter konfigurieren kann, aber im Werkszustand drehen die leider bereits bei knapp über 55° lautstark auf. Wie laut kann ich nicht messen, aber es übertönt die sonst üblichen Geräusche im Büro. (Tastatur Klackern, knarrende Stühle, etc) Und hab ich mal PhpStorm und eine Centos-7 VM mit Icinga 2 und Icinga Web 2 laufen, werden die 55° schon recht oft überschritten. Dann blasen die Lüfter erst einmal für einige Minuten, bis ~43° erreicht sind.
Zu guter letzt habe ich heute mal nachgesehen was ich mit dieser ominösen Infrarot Kamera machen kann. Dabei erfahre ich, hätte ich Windows könnte ich diese mit Windows Hello koppeln. Hm, hab ich aber nicht, ich habe Ubuntu. Gut, gibt es Windows Hello Alternativen für Linux? Ja! Howdy! Auch ich musste schmunzeln bei diesem Namen. Erste Versuche führten auch recht schnell zum Erfolg. Jetzt kann ich einfach in die Kamera grinsen wenn ich im Login-Screen oder Lock-Screen bin. Oder mit sudo Kommandos ausführe. Oder im Ubuntu Software-Center etwas installiere. Kurz, dank PAM geht das einfach überall.

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer für Icinga Web 2, Icinga DB Web sowie alle möglichen anderen Module und Bibliotheken im Web Bereich zuständig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gemütlich. Passt das Wetter, geht's auch mal auf eines seiner Zweiräder. Motorisiert oder nicht.