Antyspam w O2.pl lubi linki

Tworzenie i wysyłanie maili z aplikacji tak, by nie wpadały do spamu to spory kawałek wiedzy. Być może kiedyś opiszę moje doświadczenia w tym temacie, dzisiaj skupię się na jednej ciekawostce związanej z omijaniem filtra antyspamowego w poczcie O2.pl.

Problem na pierwszy rzut oka nietypowy: niektóre z maili wysyłanych na konta O2.pl dochodzą, inne zaś wpadają do spamu. Oczywiście maile wysyłane w z jednej aplikacji, wysyłane w ten sam sposób i z grubsza w tym samym czasie, jedyną różnicą jest treść. Niestety nagłówki w O2.pl nie zawierają informacji co dokładnie powoduje zakwalifikowanie wiadomości jako spam. Na wyczucie próbowałem parafrazować i przeredagować treść problematycznych maili, ale to nic nie dało. W końcu zauważyłem, że odrzucane maile nie zawierają żadnych linków podczas gdy te poprawnie dostarczane przynajmniej jeden. Na próbę dodałem byle jaki link i jak za dotknięciem magicznej różdżki maile zaczęły lądować w skrzynce „Odebrane”.

Na moje wyczucie powinno być odwrotnie, to maile z linkami są bardziej podejrzane. Zgaduję, że może filtr antyspamowy O2.pl zakłąda, że wiadomość powinna zawierać link do wypisania się z listy, ale to byłoby dość dziwne założenie. Tak czy owak dodanie linka pomaga.

Przydatne gotowce ffmpeg

Mój aparat nagrywa filmy korzystając z kodeka MJPEG dla obrazu i PCM dla dźwięku, co gwarantuje dobrą jakość, ale kosztem niskiego stopnia kompresji. W większości wypadków nie potrzebuję takiej jakości, więc by zaoszczędzić miejsce na dysku konwertuję filmy do formatu MP4 z obrazem w H264 i dźwiękiem AAC. By nie musieć konwertować każdego pliku z osobna używam polecenia find w połączeniu z ffmpeg i taki duet konwertuje wszystkie filmy we wskazanym katalogu:

find /media/foto/wakacje_2013/ -type f -iname *.AVI -exec ffmpeg -i {} -c:v libx264 -preset slow -crf 22 -acodec libfaac -ac 2 -ar 44100 -ab 128k {}.mp4 \;

Jeśli dodatkowo zachodzi potrzeba obrotu obrazu można posłużyć się filtrem transpose z wartością 1 powodującym obrót o 90 stopni:

find /media/foto/chorwacja_2008/ -type f -iname *.AVI -exec ffmpeg -i {} -vf "transpose=1" -c:v libx264 -preset slow -crf 22 -acodec libfaac -ac 2 -ar 44100 -ab 128k {}.mp4 \;

Jak zwykle w wypadku polecenia find tylny ukośnik i średnik na końcu linii są niezbędne.

Squid zmienia WSDL?

Z używaniem SOAP-a przez proxy w PHP bywają problemy, o czym już pisałem. Dzisiaj w podobnej sytuacji natknąłem się na inną zagwostkę. Podczas pisania integracji z usługą eraty.pl prosty kod z instrukcji:

$client = new \SoapClient('https://www.eraty.pl:8443/eRatyWSService/eRatyWS?wsdl');
$identyfikacja['login'] = 'ERatyTest';
$identyfikacja['haslo'] = 'ERatyTest!1';
$identyfikacja['numerSklepu'] = 13010005;
$identyfikacja['typProduktu'] = 0;
$identyfikacja['wariantSklepu'] = 1;
try
{
	$result = $client->pobierzLinieKredytowe($identyfikacja);
}
catch (\Exception $e)
{
	echo $e->faultstring;
}

rzucał mi wyjątkiem „Could not connect to host”. Oczywiście zarówno firewall i proxy zezwałały na dostęp do eraty.pl, a ten sam kod uruchomiony na innym serwerze działał bez zająknięcia. Nad znalezieniem rozwiązania trochę musiałem posiedzieć, aż w końcu w stack trace’ie zauważyłem, że metoda SoapClient::__doRequest() jako drugi parametr dostaje http://www.eraty.pl:8080/eRatyWSService/eRatyWS (hint: numer portu). Nie mam pojęcia skąd to się bierze, zakładam, że to coś linii PHP-Squid, ale z tą wiedzą rozwiązanie było proste. Wystarczyło wymusić poprawną lokalizację usługi przy tworzeniu obiektu SoapClient:

$client = new \SoapClient('https://www.eraty.pl:8443/eRatyWSService/eRatyWS?wsdl',
	array('location' => 'https://www.eraty.pl:8443/eRatyWSService/eRatyWS')
	);

Małe a cieszy: sp_executesql

Aplikacja, nad którą obecnie pracuję, dostaje od użytkowników zapytania SQL, które wykonuje na wielu bazach danych MS SQL. Zapytania mogą być najróżniejszego typu, od prostych SELECTów po złożone skrypty w Transact SQL-u. Wykonywanie takich złożonych zapytań przez PDO i odczytywanie wyników mogłoby być kłoptliwe, analiza zapytań i rozbijanie ich na pojedyncze kwerendy jeszcze bardziej. Bólu głowy mozna sobie oszczędzić w bardzo prosty sposób – z pomoca przychodzi sp_executesql. Nasze złożone zapytania możemy wykonać poprzez tę funkcję przerzucając rozwiązanie wspomnianych problemów na bazę danych.

$statement = $dbh->prepare("EXEC sp_executesql N" . $dbh->quote($query));
$statement->execute($params);
$rows = $statement->fetchAll();

Wywoływanie funkcji z nieznaną liczbą parametrów

Niektóre funkcje PHP mogą akceptować zmienną liczbę parametrów. To wygodne rozwiązanie, ale co zrobić jeśli nie mamy z góry ustalonej liczby parametrów, a funkcja nie ma swojego odpowiednika przyjmującego parametry w tablicy?

Załóżmy, że chcemy wyciągnąć wspólne wartości z kilku tablic. Można to zrobić używając funkcji array_intersect(). Jeżeli liczba tablic na wejściu nie jest znana możemy przetwarzanie je w pętli. Dużo prościej będzie jednak posłużyć się funkcją call_user_func_array().

$result = call_user_func_array('array_intersect', $arrayOfArrays);

W zmiennej $arrayOfArrays oczywiście znajdują się tablice, z których chcemy wyciągnąć wspólne wartości.

Wadą call_user_func_array() jest narzut czasowy, wywoływanie funkcji w ten sposób jest wyraźnie wolniejsze niż bezpośrednio. W krytycznej pętli chyba warto by się pokusić o switcha, który wywołuje funkcję z odpowiednią ilością argumentów.

PDO dblib a parametr charset

Dzisiaj zmagałem się z kodowaniem znaków w bazach danych Microsoft SQL. Sytuacja wyglądała następująco: PHP na Linuksie, aplikacja w UTF-8, na innym serwerze kilkadziesiąt baz danych Microsoft SQL, a żeby było zabawniej bazy używały kilku kodowań znaków.

Do baz łączyłem się przez PDO ze sterownikiem dblib. Pomysł z konwertowaniem kodowania w aplikacji szybko odrzuciłem, zamiast tego zainteresowałem się parametrem charset. W dokumentacji nie jest jasno opisany, ale łatwo można się domyślić, że informuje sterownik o oczekiwanym kodowaniu.

$dbh = new \PDO('dblib:charset=UTF-8;host=10.12.34.56;dbname=nazwa_bazy', $dbUser, $dbPassword);

Niestety to nie działało. Rozwiązaniem okazała się zmiana konfiguracji FreeTDS w pliku /etc/freetds/freetds.conf.

tds version = 7.0

Ta zmiana wymusza połaczenia z użyciem wersji 7.0 protokołu. Wedle dokumentacji dopiero w tej wersji działa parametr client charset.

Serwer na Debianie: nginx, PHP i bazy danych

Przedstawiam mój przepis szybkie postawienie serwera, na którym działa nginx, PHP i bazy danych. Ja z powodzeniem używam takiej konfiguracji na maszynie deweloperskiej, ale można jej też użyć jako punktu wyjścia do konfiguracji serwera produkcyjnego, jednak w tym wypadku radzę uważnie przeczytać zastrzeżenia na końcu.

W przepisie używam Debiana Squeeze, ale po dodaniu sudo przed większością poleceń będzie go można zastosować do Ubuntu. ;-)

Opisywana konfiguracja spełnia następujące założenia:

  • Na serwerze mają działać nginx, PHP, MySQL, PostgreSQL, MongoDB i memcached.
  • Oprogramowanie powinno być w możliwie aktualnych stabilnych wersjach.
  • Skrypty PHP mają działać z prawami użytkownika.
  • Serwer powinien mieć możliwość uruchamiania skryptów PHP zabezpieczonych ionCube’em.

Czytaj dalej „Serwer na Debianie: nginx, PHP i bazy danych”

Błąd w ADOdb CacheFlush()

Przy okazji przeprowadzki anime.com.pl na nowy serwer zaktualizowałem wykorzystywane biblioteki, w tym ADOdb. W trakcie testów odkryłem, że coś jest nie tak z obsługą cache, a konkretnie czyszczenie zapamiętanych wyników przez CacheFlush() nie działało. Po analizie kodu ADOdb odkryłem, że pierwszą rzeczą, którą robi CacheFlush() jest sprawdzenie czy $ADODB_CACHE jest ustawione:

global $ADODB_CACHE_DIR, $ADODB_CACHE;	
if (empty($ADODB_CACHE)) return false;

Problem polega na tym, że $ADODB_CACHE jest inicjalizowane tylko w metodzie CacheExecute() czyli innymi słowy, żeby skorzystać z CacheFlush() trzeba najpierw wywołać CacheExecute(). Moim zdaniem to błąd (wcześniej chyba tak nie było), zgłosiłem go i mam nadzieję, że zostanie poprawiony. W międzyczasie pozostaje poprawienie ADOdb na własną rękę lub wywoływanie CacheExecute() przed CacheFlush().

su: Uwierzytelnienie nie powiodło się

Na jednym VPS-ie z Debianem 6 nie mogłem przejść na konto roota korzystając z polecenia su. Próba jego wykonania kończyła się tak:

joe@dejiko:~$ su -
Hasło:
su: Uwierzytelnienie nie powiodło się

W /var/log/auth.log pojawiały się takie wpisy:

Feb 21 10:48:30 dejiko unix_chkpwd[4123]: check pass; user unknown
Feb 21 10:48:30 dejiko unix_chkpwd[4123]: password check failed for user (root)
Feb 21 10:48:30 dejiko su[4122]: pam_unix(su:auth): authentication failure; logname=joe uid=1000 euid=1000 tty=/dev/pts/1 ruser=joe rhost=  user=root

Początkowo myślałem, że problem tkwi w tym, że użytkownik joe nie należy do grupy sudoers, ale ten trop okazał się mylny. W wyniku śledztwa okazało się, że winne są uprawnienia pliku /bin/su. Na działającym systemie wyglądały one tak:

-rwsr-xr-x 1 root root 34024 2011-02-15  /bin/su

zaś na problematycznej maszynie tak:

-rwxr-xr-x 1 root root 34024 2011-02-15  /bin/su

Innymi słowy brakowało bitu „set UID”, więc problem rozwiązałem poleceniem:

chmod u+s /bin/su

Na inkryminowanym systemie ten sam problem dotyczył polecenia passwd, objawy były następujące:

joe@dejiko:~$ passwd
Zmienianie hasła dla joe.
(obecne) hasło UNIX:
passwd: Błąd podczas modyfikowania tokenu uwierzytelniania
passwd: password unchanged

Tu również pomógł chmod.

Lighttpd na kilku portach

Ostatnio przenosiłem serwer Icecast2 na osobną maszynkę i, co ważniejsze, na osobną domenę. Ponieważ trochę słuchaczy miało zapamiętane URL-e do radia chciałem ich bezboleśnie przekierować na nowy adres. W tym celu przygotowałem taką regułkę w Lighttpd:

$SERVER["socket"] == ":8000" {
	url.redirect = ("^/(.*)" => "http://stream.anime.com.pl:8000/$1")
}

Po zatrzymaniu Icecasta i zrestartowaniu Lighttpd wszystkie żądania HTTP wysyłane na port 8000 (na którm działał Icecast) są przekierowywane na nowy serwer. Dzięki temu słuchacze wciąż mogą używać starych URL-i.

Jednocześnie ten przykład pokazuje jak skłonić Lighty do nasłuchiwania na kilku portach jednocześnie. Można to wykorzystać np. po to, żeby uruchomić testową wersję serwisu na porcie 81 obok wersji produkcyjnej działającej na domyślnym porcie 80.

PS. Icecast2 ma funkcję przenoszenia słuchaczy, ale z tego co zrozumiałem działa ona tylko w obrębie jednego serwera. Czyli można ich przenieść na inny strumień na tym samym serwerze, ale na inny serwer już nie.