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.

Dziwne rozumowanie UPC

Rozumowanie pracowników UPC musi prowadzić dziwnymi ścieżkami. Weźmy przykład z życia mojego osobistego. Przy telefonicznym uzgadnianiu warunków przedłużenia umowy wyraźnie zaznaczyłem, że chcę otrzymać zwykły modem kablowy bez wi-fi, bo zależy mi na tym, żeby działał w trybie mostkowania. Instalator z umową i modemem przyszedł kiedy byłem za granicą, więc umowę podpisywała Karina. Oczywiście w umowie znalazła się usługa wi-fi, modem też dostaliśmy z wi-fi, a uzyskanie przez niego publicznego adresu IP na moim komputerze okazało się niemożliwie. Napisałem reklamację, że usługa jest inna niż uzgadnialiśmy (w końcu mają nagrane rozmowy, powinni to łatwo zweryfikować) i oczekuję wymiany modemu na taki jakiego potrzebuję. Po dłuższym czasie otrzymałem negatywną odpowiedź. Zirytowałem się, ale stwierdziłem, że szkoda nerwów na kłótnie i zamówiłem płatną usługę wymiany modemu. UPC trawiło moje zamówienie jeszcze dłużej niż reklamację, aż w końcu odparło, że wyrazili zgodę na bezpłatną wymianę modemu. Nie można było tak od razu? Zapewne przyczyna tkwi w humorze czynnika ludzkiego procesującego reklamacje i zamówienia, ale zastanawiałem się czy aby UPC nie odrzuciło reklamacji by zamiast przyznać się do błędu postawić się w roli łaskawcy.

Połączenia HTTP z PHP za serwerem proxy

Dzisiaj zmierzyłem się z problemem wywoływania usługi SOAP z poziomu PHP działającego na serwerze schowanym za proxy. Ostatecznie okazało się to prostsze niż myślałem, wystarczy przekazać dodatkowe parametry do konstruktora obiektu SoapClient.

$soap = new SoapClient('http://domena.pl/usluga?wsdl', array(
  'proxy_host' => '10.107.10.69',
  'proxy_port' => 8080,
  'proxy_login' => 'nazwa.uzytkownika',
  'proxy_password' => 'haslo'
  ));

Zabrało mi to jednak trochę czasu bo zacząłem od szukania rozwiązania na niższym poziomie czyli modyfikacji parametrów używanych przez PHP do wykonywania połączeń HTTP. Metodą guglania, prób i błędów dotarłem do takiego rozwiązania, które również działa.

$default = stream_context_get_default(array(
  'http' => array(
    'proxy' => 'tcp://10.107.10.69:8080',
    'request_fulluri' => true,
    'header' => 'Proxy-Authorization: Basic ' . base64_encode('nazwa.uzytkownika:haslo'),
    )
  ));

Dzięki tej metodzie z proxy zaczynają współpracować wszystkie funkcje wykorzystujące protokół HTTP, np. readfile() z plikiem na zdalnym serwerze.

readfile('http://www.google.com/');

Wyłączanie akcji w generatorze Symfony

Generator modułów administracyjnych w Symfony nie umożliwia wybrania akcji, które dany moduł ma wykonywać, zawsze dostajemy pełen zestaw CRUD. Czasami zachodzi potrzeba zablokowania niektórych akcji, np. w wypadku logów bezpieczeństwa sens ma jedynie index (przeglądanie listy), a jakakolwiek modyfikacja powinna być zabroniona.

Odnośniki widoczne na liście można łatwo ukryć edytując plik generator.yml, wystarczy domyślne akcje zastąpić pustymi listami.

generator:
  param:
    config:
      list:
        title:             Permission changes log
        actions:           {}
        object_actions:    {}
        batch_actions:     {}
      filter:
        display:           [agent_user_name, subject_user_name, permission_name, created_at]

Akcje można jednak dalej wykonywać ręcznie wpisując URL, dlatego należy je zablokować w kontrolerze.

class permission_logActions extends autoPermission_logActions
{
  public function preExecute()
  {
    $this->forward404If($this->getActionName() !== 'index', 'Action disabled');
    parent::preExecute();
  }
}

Zapisywanie osadzonych formularzy w Symfony

Zapisywanie osadzonych formularzy w Symfony działa inaczej niż intuicyjnie zakładałem. Co ciekawsze, już raz się z tym kiedyś zetknąłem, ale było to na tyle dawno, że zapomniałem rozwiązania.

Załóżmy, że w zwykłym formularzu dziedziczącym z klasy sfForm osadzam formularz oparty na modelu z bazy danych czyli dziedziczący z sfFormDoctrine.

class OnePageCheckoutForm extends sfForm
{
  public function configure()
  {
    $this->widgetSchema['same_address'] = new sfWidgetFormInputCheckbox();
    $this->validatorSchema['same_address'] = new sfValidatorBoolean();

    $this->embedForm('customer', new CustomerForm());
    $this->embedForm('shipment_address', new AddressForm());
    $this->getWidgetSchema()->setNameFormat('checkout_form[%s]');
  }
}

W kontrolerze chcę zapisać dane z osadzonych formularzy, ale oczywiście klasa OnePageCheckoutForm nie ma metody save(). W pierwszym odruchu napisałem mniej więcej coś takiego:

public function executeCheckout(sfWebRequest $request)
{
  if (!$this->getUser()->hasCart())
  {
    return $this->redirect('@cart');
  }
  $this->cart = $this->getUser()->getCart();

  $this->form = new OnePageCheckoutForm();
  if ($request->getMethod() == 'POST')
  {
    $this->form->bind($request->getParameter($this->form->getName()), $request->getFiles($this->form->getName()));
    if ($this->form->isValid())
    {
      $values = $this->form->getValues();
      $this->form->getEmbeddedForm('customer')->save();
      $this->form->getEmbeddedForm('shipment_address')->save();

      $this->cart->setCustomerId($this->form->getEmbeddedForm('customer')->getObject()->getId());
      $this->cart->save();
      $this->redirect('@cart_payment_process');
    }
  }
}

Ku memu zaskoczeniu zobaczyłem wyjątek, z którego wynikało, że zapis formularza customer się nie powiódł, bo formularz nie zawiera danych. Dla pewności wrzuciłem w kod kilka var_dumpów:

// wynik: dane dla całego formularza łącznie z osadzonymi
var_dump($this->form->getValues());
// wynik: false
var_dump($this->form->getEmbeddedForm('customer')->isBound());
// wynik: pusta tablica
var_dump($this->form->getEmbeddedForm('customer')->getObject()->toArray());

Jak się okazało, formularz klasy sfForm nie aktualizuje obiektów w osadzonych formularzach, trzeba to wykonać samemu:

$this->form->getEmbeddedForm('customer')
  ->updateObject($this->form->getValue('customer'));

Można to zrobić w kontrolerze, ja ze względów konwencjonalno-estetycznych dopisałem do klasy OnePageCheckoutForm metody updateEmbeddedForms() i saveEmbeddedForms(), które są z grubsza kopią tak samo nazwanych metod z klasy sfFormObject.

Na poły wirtualne środowisko developerskie

Wymiana dysku systemowego na SSD była dla mnie okazją do wprowadzenia zmian w domowym Linuksie. Jedną z nich było postawienie zupełnie nowego środowiska developerskiego. Z powodzeniem używam go już od niemal dwóch miesięcy i jestem z niego zadowolony, więc żywię skromną nadzieję, że opisując je może komuś pomogę.

Przez długie lata jeden system służył mi do Internetu, programowania, rozrywki i wszelkich innych zastosowań. Miałem na nim zainstalowany komplet przeglądarek, wybór baz danych (m.in. PostgreSQL, MySQL, MongoDB), trochę serwerów (Lighttpd, ProFTPd, IceCast), interpretery i kompilatory różnych języków programowania (PHP, Python, g++…) plus sporo różnych narzędzi. Takie rozwiązanie ma kilka wad:

  • Na desktopie mam przeważnie nowsze wersje programów niż na serwerze produkcyjnym, więc czasami pojawiają się problemy z kompatybilnością.
  • Bazy, serwery i inne bajery pożerają zasoby nawet jeśli ich w danej chwili nie potrzebuję, bo nie programuję.
  • Kompilowanie programów ze źródeł, instalowanie dziwnych paczek, grzebanie w konfiguracji programów nawet przy zachowaniu staranności prędzej czy później tworzy w systemie bajzel, w efekcie którego rzeczy przestają działać lub działają w osobliwy sposób.
  • Mam tylko jedno środowisko, żeby spróbować kod z inną wersją PHP albo innym serwerem www muszę uciekać się do karkołomnych/upierdliwych konfiguracji.

Czytaj dalej „Na poły wirtualne środowisko developerskie”

Sortowanie wyników z użyciem funkcji w Solarium

To będzie zwięzły wpis dokumentujący małe zwycięstwo w pojedynku z Solarium. Zadanie polegało na dodaniu nowego sposobu sortowania produktów: w pierwszej kolejności według dostępności produktu (bez względu na ilość, tylko binarne tak/nie), w drugiej według ceny. W bazie Solr mamy tylko ilość sztuk danego produktu, należało ją więc jakoś zrzutować na wartość binarną. Na szczęście Solr umożliwia użycie funkcji do porządkowania znalezionych dokumentów, Solarium od drugiej wersji także to obsługuje. Końcowy efekt moich starań jest następujący:

$query->addSort($query->getHelper()->functionCall('min', array('stock', 1)), Solarium_Query_Select::SORT_DESC);

Dla celów dydaktycznych dodam, że najpierw próbowałem zrobić to w ten sposób:

$query->addSort($query->getHelper()->functionCall('if', array('stock', 1, 0)), Solarium_Query_Select::SORT_DESC);

jednak kończyło się to błędem:

Solr HTTP error: sort param could not be parsed as a query, and is not a field that exists in the index: if(stock,1,0) (400)

Przyczyna okazała się prozaiczna, ale odkrycie jej zajęło mi dłuższą chwilę. Otóż funkcja if dostępna jest dopiero od wersji 4.0 Solr, a my używamy 3.6.