Roundcube przez HTTPS czyli nginx i self-signed certificate

Powinienem to zrobić dawno temu, ale dopiero teraz zabrałem się za zabezpieczenie SSL-em mojego webmaila opartego na Roundcube. Ponieważ poza mną nikt z niego nie korzysta, a sam sobie zazwyczaj ufam, to zdecydowałem się nie korzystać z żadnego centrum certyfikacji i wygenerować certyfikat własnoręcznie.

W sieci nie brakuje instrukcji generowania certyfikatów, ja staram się robić wszystko po debianowemu, więc kierowałem się opisem z debianowej wiki. Dlatego certyfikat wylądował w katalogu /etc/ssl/localcerts, który musiałem stworzyć:

mkdir -p /etc/ssl/localcerts

Polecenie generowania klucza i certyfikatu:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/localcerts/mail.komitywa.net.key -out /etc/ssl/localcerts/mail.komitywa.net.crt

Przed wygenerowaniem certyfikatu zostaniemy zapytani o dane certyfikowanego podmiotu:

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:PL
State or Province Name (full name) [Some-State]:Mazowieckie
Locality Name (eg, city) []:Warsaw
Organization Name (eg, company) [Internet Widgits Pty Ltd]:komitywa.net
Organizational Unit Name (eg, section) []:.
Common Name (eg, YOUR name) []:mail.komitywa.net
Email Address []:moj-adres-email@komitywa.net

Na koniec warto ograniczyć dostęp do wygenerowanych plików:

chmod 600 /etc/ssl/localcerts/mail.komitywa.net.*

Teraz można już SSL do konfiguracji nginx’a. Do działającego wirtualnego hosta dodałem:

listen 443 ssl;
ssl_certificate /etc/ssl/localcerts/mail.komitywa.net.crt;
ssl_certificate_key /etc/ssl/localcerts/mail.komitywa.net.key;

Po przeładowaniu nginx’a i przetestowaniu, że połaczenie po HTTPS działa postanowiłem jeszcze dodać przekierowanie z HTTP na HTTPS:

server {
       listen 80;
       server_name mail.komitywa.net;
       return 301 https://mail.komitywa.net$request_uri;
}

Wygenerowany certyfikat jest ważny przez rok, więc w kwietniu 2016 będę musiał wygenerować nowy certyfikat.

Weekend w Brukseli

Miniony weekend spędziłem z Kariną w Brukseli. Wycieczkę uważam za więcej niż udaną, a bez fałszywej skromności dodam, że duża w tym zasługa dobrego planowania. Poniżej garść luźnych uwag i sugestii, które mogą się przydać komuś, kto chciałby sobie urządzić podobną wycieczkę.

Tanie latanie

Mój pierwszy lot Ryanairem okazał się niespodziewanie… zwyczajny. Po lekturze opinii w internecie obawiałem się, że ludzie będą wskakiwać do samolotu oknami, wpychać się na nie swoje miejsca, grillować między fotelami, słowem Sodomy i Gomory ze skrzydłami albo latającego odpowiednika „Słonecznego” Koleii Mazowieckich. Tymczasem okazało się, że choć samolot był wypełniony chyba do ostatniego miejsca, a pasażerowie uformowali pokaźną kolejkę na długo przed ogłoszeniem boardingu to wejście na pokład odbyło się bez ekscesów. Wprawdzie na moim miejscu usiadła jakaś miła starsza pani, ale tylko dlatego, że nie wiedziała, że na karcie pokładowej ma wydrukowane miejsce. Grzecznie jej to wytłumaczyłem i problem był rozwiązany. Fotele, a i owszem, nie odchylają się, miejsca jest co najwyżej akuratnie, za bułę i napój trzeba zapłacić, a film sobie zorganizować we własnym zakresie, ale przez dwie godziny można znieść takie niedogodności, tym bardziej, że bilet był dwukrotnie tańszy niż na pendziolino do Krakowa. Po stronie plusów należy zapisać też dogodne godziny wylotów na weekendowy wyjazd: wylot w piątek o 9:20, powrót w niedzielę o 18:25.

Dojazd

Ryainair ląduje lotnisku Charleroi, takim belgiskim Modlinie (choć wielkością bliżej mu do Okęcia). Wygodne połączenie z Brukselą zapewnia linia autokarowa. Bilety najlepiej kupić przez internet, będą wtedy kosztowały przynajmniej o 3 € od łebka taniej, co przy dwóch osobach w obie strony da ok 50 zł oszczędności. Oszczędza się też czas, bo wprost z samolotu można wsiadać do autokaru.

Nocleg

Poszukiwania noclegu rozpocząłem od Airbnb, ale okazało się, że w obrębie starego miasta taniej można spać w hotelu. Wybrałem hotel Saint Nicolas, dwie minuty spacerkiem od Wielkiego Placu i rzut beretem od stacji metra Bourse. Miejscówka jest świetna jako baza wypadowa do korzystania z brukselskich atrakcji, zarówno za dnia jak i w nocy. Trzeba mieć jednak świadomość, że położenie może być też wadą, hotel stoi przy ruchliwym deptaku, a centrum Brukseli żyje do późnych godzin nocnych.

Zwiedzanie

Na zwiedzanie Brukseli warto uzbroić się w Brussels Card czyli kartkę (bo dostajemy ją jako wydruk A4) dającą wstęp do 30 muzeów (w tym tych MSZ najciekawszych) i zniżki w innych miejscach (np. Atomium). Biorąc pod uwagę, że wstęp do muzeum kosztuje przeważnie od 6 do 10 € to zakup karty się opłaca. Ja kupiłem takową na 48 godzin, aktywowałem w piątek po południu o korzystałem aktywnie aż do wyjazdu w niedzielę.

Brussels Card ma też opcję korzystania z dwóch autobusowych linii turystycznych, ale ja wybrałem komunikację miejską. Bruksela ma znakomity zbiorkom, do większości interesujących mnie miejsc dojechałem prawie pod drzwi. Opłaca się kupić bilety na 5 lub 10 przejazdów ew. na 48 godzin. Niestety trzeba się przyzwyczaić do aromatu uryny na stacjach metra.

Google Maps dobrze sprawdza się w nawigowaniu po Brukselii z buta, ale sugestie dotyczące używania zbiorkomu bywają nietrafione. Mapę Brukselii zapisałem w telefonie, ale z myślą o Google Maps wykupiłem pakiet 100 MB transferu w roamingu. Pod koniec weekendu okazało się, że wykorzystałem go nieco ponad 30 MB, z czego tylko mała część przypadła na Mapy.

Manneken pis to oczywiście punkt obowiązkowy, choć tak po prawdzie nie ma co oglądać. Ciekawsze jest to, że w ramach równouprawnienia od 1987 Bruksela ma też siusiającą dziewczynkę czyli Jeanneke pis. Jest jeszcze trzecia siusiająca rzeźba – Zinneke pis czyli pies. Do kompletu brakuje jeszcze kota.

Zinneke pis

Z kolei Atomium na żywo robi większe wrażenie niż się spodziewałem. Niestety swoje trzeba odstać w kolejkach, w deszczowy niedzielny poranek czekaliśmy blisko godzinę.

Atomium

Belgia stoi komiksem, nic więc dziwnego, że muzeum komiksu jest na wypasie. Stała ekspozycja pokazuje historię komiksu, proces jego powstawania od pomysłu do gotowego albumu, różne style i gatunki i całą masę ciekawych eksponatów. W czasie mojej wizyty ekspozycję czasową stanowiła wystawa poświęcona Thorgalowi oraz osobna poświęcona Grzegorzowi Rosińskiemu. Moim zdaniem to najciekawsze muzeum, które odwiedziłem w Brukseli.

Podobne wrażenia oferuje MOOF czyli muzeum figurek kolekcjonerskich. Figurki to nie mój konik, ale miło się je oglądało. Oprócz figurek sporo miejsca poświęcono komiksom i filmom animowanym powstałym w Belgii. Bardzo fajnie bawiłem się przy starych konsolach z grami na podstawie komiksów o Asteriksie i Obeliksie.

MOOF

MOOF

Muzeum piwa brzmi ciekawie, ale jego oferta ogranicza się do projekcji umiarkowanie interesującego filmu i degustacji piwa.

Ciekawsze jest muzeum czekolady, a zwłaszcza pokaz robienia pralinek belgijskich. Oczywiście jest też degustacja.

Muzeum zabawek wygląda i pachnie jak mieszkanie patologicznego zbieracza. W sumie nic ciekawego, ale fajne jest to, że dużą częścią eksponatów można się bawić, a dzieciaki, które widziałem w muzeum miały z tego wielką frajdę.

Bardzo podobało mi się w Auto World. Mają tam dużą kolekcję zabytkowych samochodów, choć najdłużej przyglądałem się youngtimerom takim jak Honda NSX i specjalistycznie przygotowanym samochodom wyścigowym. Karinie, która jest umiarkowaną fanką motoryzacji, też się podobało, więc warto tam się wybrać.

Auto World - japońskie supersamochody

Auto World

Jedzenie

W Brukselii należy zjeść frytki i gofry. W pobliżu hotelu jest kanciapa z frytkami, która, jeśli wierzyć nalepce przy drzwiach, jest przez kogoś tam polecana. Potwierdzam, że fryty z sosem andaluzyjskim mają tam dobre. Po sąsiedzku jest kanciapa z goframi i churrosami, takoż smacznymi. W okolicy jest jednak pierdyliard podobnych kanciap, możliwe, że lepszych lub bardziej znanych. Z drugiej strony nie polecam belgiskiego McDonald’sa czyli Quicka. Co by nie mówić o Maku to paszę mają tam smaczną, a lokale czyste. W Quicku dostałem kanapkę, która wyglądała jakby w celu podgrzania ktoś na niej usiadł, do podłogi prawie się przykleiłem, a na koniec musiałem szukać kosza na śmieci, który nie był przepełniony.

WHERE warunek vs LEFT JOIN warunek

Być może nie powinienem publicznie przyznawać się, że popełniłem tak podstawowy błąd w konstruowaniu zapytania SQL, ale po jego odkryciu szczerze się uśmiałem, a skoro śmiech to zdrowie to się nim podzielę.

W uproszczeniu: mam tabelkę z danymi klientów (client) i tabelkę produktów (product), pomiędzy którymi istnieje relacja jeden do wielu. Wyciąganie danych do pokazania jest zrealizowane w Doctrine w następujący sposób:

$qb
    ->select('customer, product')
    ->from($this->getEntityName(), 'customer')
    ->leftJoin('customer.products', 'product')
    ->where('customer.identifier = :identifier')
    ->setParameter('identifier', $identifier);

Dostałem za zadanie zmienić wyświetlanie danych w ten sposób by pokazywać jedynie aktywne produkty. Zrobiłem to tak:

$qb
    ->select('customer, product')
    ->from($this->getEntityName(), 'customer')
    ->leftJoin('customer.products', 'product')
    ->where('customer.identifier = :identifier')
    ->andWhere('product.isActive = 1')
    ->setParameter('identifier', $identifier);

Ku memu zdziwieniu w wypadku klienta, który ma tylko jeden nieaktywny produkt wynik zapytania był pusty. No jakże to?! Przecież klient jak najbardziej istnieje, a ponieważ nie ma produktów spełniających kryteria wyszukiwania (isActive = 1) to w miejscu danych produktu powinny być NULLe.

Swój błąd zrozumiałem dopiero, kiedy spojrzałem na generowane przez Doctrine zapytanie SQL. W uproszczeniu wygląda ono tak:

SELECT 
  c.name,
  p.product_type
FROM 
  customer c 
  LEFT JOIN product p ON c.id = p.customer_id 
WHERE 
  c.identifier = '65092900214'
  AND p.is_active = 1

Ponieważ warunek p.is_active = 1 sprawdzamy w części WHERE zapytania to eliminujemy z zapytania wszystkie wiersze, w których on nie jest spełniony. Zatem jeśli klient ma tylko jeden nieaktywny produkt to wynik zapytania będzie pusty.

Oczekiwany efekt (wiersz z NULLami w miejscu danych produktu) uzyskamy przenosząc warunek do części LEFT JOIN zapytania:

SELECT 
  c.name,
  p.product_type
FROM customer c
  LEFT JOIN product p ON c.id = p.customer_id 
  AND p.is_active = 1
WHERE 
  c.identifier = '65092900214'

Ponieważ warunek dotyczy złączenia to jeśli nie zostanie on spełniony to złączenie nie zostanie wykonane, ale dane klienta zostaną zwrócone. Dokładnie tak jak chciałem.

W Doctrine poprawne zapytanie należy zbudować tak:

$qb
    ->select('customer, product')
    ->from($this->getEntityName(), 'customer')
    ->leftJoin('customer.products', 'product', 'WITH', 'product.isActive = 1')
    ->where('customer.identifier = :identifier')
    ->setParameter('identifier', $identifier);

Twig Extensions w aplikacji Symfony2

Twig sam z siebie ma sporo wygodnych funkcji, ale kilka bardzo przydatnych udogodnień nie trafiło do głównej biblioteki lecz do osobnej paczki Twig Extensions. W wypadku standardowej aplikacji Symfony2 te rozszerzenia już są uwzględnione w composer.json, ale jeśli nie to można jest zainstalować poleceniem:

composer require twig/extensions ~1.1.0

Trzeba je także włączyć w konfiguracji aplikacji, tak jak każde inne rozszerzenie Twiga:

services:
    twig.extension.intl:
        class: Twig_Extensions_Extension_Intl
        tags:
            - { name: twig.extension }

Powyższy przykład dodaje rozszerzenie Intl, które podobnie jak biblioteka Intl ułatwia formatowanie danych zgodnie z ustawieniami lokalizacji. Obecnie w Twig Extensions dostępne są następujące klasy:

  • Twig_Extensions_Extension_Array
  • Twig_Extensions_Extension_Date
  • Twig_Extensions_Extension_I18n
  • Twig_Extensions_Extension_Intl
  • Twig_Extensions_Extension_Text

Przy założeniu, że aplikacja ma ustawione polską lokalizację to wyświetlenie daty w postaci „13 listopada 2014” wygląda tak:

{{ meeting.date|localizeddate('long', 'none') }}

Wyświetlenie liczby z częścią dziesiętną:

{{ player.power|round(2)|localizednumber }}

Przy korzystaniu z filtra localizednumber tylko liczby z częścią ułamkową będą wyświetlane z przecinkiem, a liczby całkowite są wyświetlane bez przecinka.

Klucz kluczem do sukcesu

Mechanizm planowania zapytań w MySQL jest mniej sprytny niż sądziłem. Chciałem wykonać z pozoru łatwe zapytanie: z bazy użytkowników liczącej ok 130 tys. wierszy chciałem wyłuskać osoby mieszkające pod określonymi kodami pocztowymi (ok. 20 tys.). Dodatkowym warunkiem było podanie numeru telefonu, który znajduje się w innej tabelce oraz wyrażenie zgody na marketing, ale to nie jest istotne dla głównego problemu. Napisałem takie zapytanie:

SELECT
	k.imie,
	k.nazwisko,
	k.kod_pocztowy,
	u.numer_telefonu
FROM klient k
INNER JOIN ustawienia u ON k.id = u.klient_id
INNER JOIN tmp_kody t ON k.kod_pocztowy = t.kod_pocztowy
WHERE k.zgoda_na_marketing = 1
ORDER BY k.id

Szukane kody pocztowe wrzuciłem sobie do tabelki, bo przy próbie umieszczenia ich w treści zapytania phpMyAdmin odmówił współpracy, a wklejanie zapytania na zdalnej konsoli trwało ponad 10 minut.

Pierwszą próbę wykonania zapytania przerwałem po kilku minutach. EXPLAIN pokazał, że MySQL nie bardzo chce korzystać z indeksów i najchętniej przeskanowałby całą tabelkę tmp_kody i ustawienia. Próbowałem przepisać zapytanie na kilka sposobów by skłonić MySQL do ułożenia innego planu, ale bezskutecznie. Skutecznym rozwiązaniem okazało się tymczasowe dodanie indeksu na kolumnę kod_pocztowy. Oryginalne zapytanie wykonało się w mgnieniu oka, a EXPLAIN pokazał, że wszystkie etapy planu wykonania zaczęły korzystać z indeksów.

Wniosek na przyszłość: jeśli MySQL sobie nie radzi to można mu pomóc dodając tymczasowy indeks. To może okazać się szybsze niż czekanie na wynik nieoptymalnego zapytania.

Import dużych liczb z pliku CSV

Od czasu do czasu jestem proszony o wyciąganie z bazy różnych raportów. Prosta sprawa: zapytanie w phpMyAdminie, eksport wyniku do pliku CSV i wysyłamy… o ile w pliku nie ma kolumny zawierającej dużą liczbę. W takim wypadku zarówno LibreOffice Calc jak i Excel mają nieprzyjemny zwyczaj zaokrąglania jej. Przykładowo w 16 cyfrowym numerze karty po którym identyfikujemy klienta i w którym ostatnia cyfra jest sumą kontrolną dostajemy na końcu zawsze 0, co oczywiście skutecznie niweczy zarówno sprawdzanie sumy kontrolnej jak i identyfikację klienta.

Na szczęście w Calcu można to naprawić. W Excelu zapewne też, ale odbiorcy moich raportów nie wiedzą jak, a ja nie mam Excela, żeby eksperymentować. Dlatego przed wysłaniem raportu do działu marketingu importuję plik do Calca. W okienku z opcjami importu należy zaznaczyć kolumnę zawierającą dużą liczbę i zmienić jej typ na Tekst.

Import pliku CSV

Potem należy zapisać arkusz w formacie Excela. Taki plik można dostarczyć do marketingu.

Podpisywanie dokumentów profilem zaufanym w PUE ZUS

Ilekroć próbuję cokolwiek załatwić przez Platformę Usług Elektronicznych Zakładu Ubezpieczeń Społecznych czuję się jakbym próbował wbić gwoździa starą skarpetą – może i da się to zrobić, ale na pewno nie jest to łatwe ani wygodne. Zupełnie jakby ZUS chciał odstraszyć ludzi od załatwiania spraw online.

Teraz próbowałem złożyć deklaracje ZUS ZWUA i ZUS ZUA. O ile wypełnienie deklaracji przez kreatora było w miarę łatwe (oczywiście przez Firefoksa, bo wbudowana w Chrome’a wtyczka Flash jest nieobsługiwana), o tyle próba wysłania deklaracji kończyła się fiaskiem. Na ekranie pojawiał się kręcioł, Java odpalała jakiś SZAFIR SDK instalator i mogiła. W akcie desperacji spróbowałem całą operację przeprowadzić z komputera z Windowsem. Tutaj sytuacja była o tyle lepsza, że wyskoczyło okno podpisywania dokumentów podpisem elektronicznym. To naprowadziło mnie na właściwy trop: mimo, że do PUE loguję się przez profil zaufany ePUAP i mimo, że jako ubezpieczony podpisuję dokumenty rzeczonym profilem zaufanym to w ePłatniku domyślnie włączone jest podpisywanie dokumentów podpisem elektronicznym. Żeby to zmienić należy w zakładce Płatnik wybrać w menu po lewej Ustawienia, a następnie Ustawienia konta. Tam należy wybrać „Sposób podpisywania dokumentów w aplikacji ePłatnik” jako „Podpis profilem zaufanym ePuap”.

ZUS PUE podpis

Tworzenie pliku CSV w pamięci

Nie tak dawno pisałem o funkcjach fgetcsv() i fputcsv() w kontekście nakładania filtrów na strumienie PHP. Również z pomocą strumieni można rozwiązać problem tworzenia plików CSV w pamięci. Wystarczy otworzyć taki niby-plik w pamięci:

$fh = fopen('php://temp', 'rw');

i już można do niego zapisywać dane przez fputcsv():

fputcsv($fh, ["Imię", "Nazwisko", "Login", "PESEL"]);

Na koniec wystarczy tylko przewinąć wskaźnik pozycji w pliku na początek i pobrać jego zawartość:

rewind($fh);
$csv = stream_get_contents($fh);
fclose($fh);

Katalogi przed plikami w Gnome Shell

Jestem otwarty na nowe rozwiązania w dziedzinie interfejsu graficznego aplikacji, ale wyświetlanie katalogów przed plikami w managerze plików jest dla mnie aksjomatem. Tymczasem po którejś aktualizacji Debiana Nautilus zaczął pokazywać pliki wymieszane z katalogami (w kolejności alfabetycznej). Co gorsza, nie dało się tego zmienić przez wyklikanie bezpośrednio w Nautilusie. Musiałem uciec się do googlania, który podpowiedział w użycie dconf-editor i kluczu org.gnome.nautilus.preferences zaznaczenie opcji sort-directories-first.

dconf

Checkbox kontra NOT NULL

W encji mamy pole legal1 typu boolean, które nie może być NULL-em.

    /**
     * @var boolean
     *
     * @ORM\Column(name="legal1", type="boolean", columnDefinition="TINYINT(1)")
     */
    private $legal1;

W formularzu pole jest prezentowane jako checkbox. Niby wszystko powinno działać, a jednak jeśli wyślemy formularz bez zaznaczania checkboxa to na encji nie jest wywoływana metoda setLegal1(), a w rezultacie Doctrine próbuje zapisać NULL jako wartość pola legal1, co oczywiście kończy się wyjątkiem.

Można kombinować na samej encji (np. zwracać z getLegal1() 0 jeśli wartość to NULL), ale chyba najbardziej eleganckim rozwiązaniem będzie zastosowanie transformera, który będzie tłumaczył wartości pomiędzy encją a formularzem. Prościutki transformer realizujący takie zadanie może wyglądać tak.

namespace MDurys\CommonBundle\Lib\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

class BoolTransformer implements DataTransformerInterface
{
    public function transform($input)
    {
        return boolval($input);
    }

    public function reverseTransform($input)
    {
        return intval($input);
    }
}

W formularzu należy go zastosować jako model transfomer:

    $builder
        ->add(
            $builder->create('legal1', 'checkbox')->addModelTransformer(new BoolTransformer())
            )

Jego działanie polega na tym, że podczas tworzenia formularza zostanie wywołana metoda transform(), która wartość z bazy danych (0 lub 1) rzutuje na wartość boolowską. Z kolei podczas zapisywania formularza zostanie wywołana metoda reverseTransform(), która rzutuje wartość z formularza na liczbę całkowitą. Tym sposobem z NULL-a zrobi się 0.