Maërlyn

Go és solr

2016. 10. 09.

Időnként azóta is előveszem a Got, most épp az egyik tervezett melóhelyi átállás adta az ötletet a következő játékhoz.

Van egy solr core-unk és egy nodejs microservice-ünk, amik ugyanazokat az adatokat tárolják, csak picit máshogy, és másutt is használjuk őket. Ezt akarjuk kiváltani egyetlen kibővített core-ral, ami mindkét célra alkalmas.

A configot az ini csomaggal olvasom:

cfg, err = ini.LoadSources(ini.LoadOptions{}, 
    "/etc/icom/settings_locationserver.ini")
if err != nil {
	panic(err)
}

A solr csomag felett meg van egy saját wrapperem, ami összeállít és megfuttat egy queryt:

func (sc SolrClient) simpleQuery(q string, rows int)
    *solr.SelectResponse {
	query := solr.Query{
		Params: solr.URLParamMap{
			"q": []string{q},
		},
		Rows: rows,
	}

	res, err := sc.connection.Select(&query)

	if err != nil {
		log.Printf("solr error: %s\n", err)
	}

	return res
}

Ezzel az ID szerinti lekérdezés ilyen egyszerű:

func (sc SolrClient) FindById(id int)
    (map[string]interface{}, bool) {
	res := sc.simpleQuery("location_id:"+strconv.Itoa(id), 1)

	if res.Results.Len() == 0 {
		return nil, false
	}

	return res.Results.Get(0).Fields, true
}

mert az ID saját forrásból jön (mysql auto_inc mező).

Ezekkel a http requestet megoldjuk pár sorral:

func returnJson(w http.ResponseWriter, data interface{}) {
	w.Header().Set("Content-Type", "application/json")
	err := json.NewEncoder(w).Encode(data)

	if err != nil {
		log.Fatal(err)
	}
}

func getLocationById(w http.ResponseWriter, r *http.Request,
    ps httprouter.Params) {
	id, _ := strconv.Atoi(ps.ByName("id"))

	location, found := solrClient.FindById(id)

	if !found {
		w.WriteHeader(404)
		return
	}

	returnJson(w, location)
}

Kocsi számolgatás

2016. 03. 26.

Ma egy éve, hogy autótulajdonos lettem. Ezalatt beleraktam 14.475km-t, és most kicsit számolgatni fogok, mennyibe került nekem ez az egész, és hogy vajon megérte-e.

A kocsi egy idén 14 éves, benzines, 1.4-es Corolla, most már bőven 245 ezer km felett van benne összesen. A vételár, átírás, első szervíz együtt majdnem 1.1M Ft volt.

32 alkalommal tankoltam az egy év alatt, a maival együtt összesen 365.432Ft-ért 1064 litert. Összesítve azért durvának tűnik, de mentem is vele rendesen. Eleinte Lukoilhoz jártam, aztán átálltam az OMV Super 95-ére, mert bár drágább, érezhetően erősebb vele az autó, és csendesebb is. Ezzel az átlagfogyasztása 7.3 l/100km.

Komoly baja nem volt. A hátsó lambda-szondát kellett cserélni, ez volt az egyetlen nagy és váratlan kiadás. A nem tankolás jellegű kiadások összesen 395.299 forintot tettek ki, ebben benne van az első szervíz (70k), egy téligumi szett (71k), a lambda-szonda (40k), két gumiszerelés futómű-beállítással (egyenként 18k) valamint egy kormány- és váltóbőrözés (30k). A többi ezekhez képest apróság, mint az izzócsere egy ezresért, vagy a fagyálló 400-ért, de ilyesmikből sok van. Felírtam még azt az ecsetet is, amivel a festéket kentük az olyan helyekre, ahol már elkezdett rozsdállni, és nem látványos helyen van. Meglepődtem azért, hogy ekkora lett az összeg.

Az első évi biztosításom 35k volt, a második évi 37k, baleseti adóval. Gépjárműadó évi 13k.

És akkor most nézzük a mérleg másik oldalát: összejön-e annyi, hogy azt mondhassam, megérte?

Az biztos, hogy bár egyik oldalról nagy kötöttség, a másik oldalt nagy szabadságot ad. Kirándulni van kedvem, vagy vasárnap délután át kell mennem a város másik végére, ami BKV-val három óra? Beülök és megyek. Számtalan egynapos, és számos (hosszú)hétvégés kirándulást csináltam vele, általában nem egyedül, bár ilyen is volt, mint például múlt kedden, mikor láttam, hogy Dobogókőn havazik, csak bevágtam magam és mentem. Tervezett utak voltak Veszprémbe, Egerbe, Debrecenbe, Tokajba, Nyíregyházára, Szombathelyre – tényleg sokfelé. Meg persze haza Dunaújvárosba. Akkor is jól jött, amikor valakit hirtelen kórházba kellett vinni.

Bő egy hónapja ezzel járok munkába is, mikor 300Ft volt a benzin literje úgy döntöttem, kipróbálom. Elég kényelmes egyébként, bár persze tudom, hogy drága, és hiányzik az a napi bő óra olvasás. De így könnyebben megtehetem, hogy hétfőnként a Városligetben és az Andrássny úton sétálok egy nagyot, illetve megtehetem, hogy pénteken melóból hamarabb lelépve felmegyek valamelyik környéki hegyre élvezni a napsütést és a kilátást. Azóta jobban ismerem, és több részén jártam a városnak, mint előtte összesen.

Háromszor segítettem költözni, lehajtott hátsó üléssorral hatalmas helyem van, egyik alkalommal egy aránylag nagy szekrény tűnt el a gép gyomrában, és még pakoltunk mellé.

Szóval rengeteg olyan élményt adott, ami kocsi nélkül nem lett volna meg, gondolok itt főleg a kirándulásokra, de Ingresses eseménynél is nemegyszer voltam sofőr. Nyúlott az arcom, amikor az összegeket megláttam, de ezen kalandokra visszagondolva mégis azt mondom: megérte megvenni és fenntartani az autót.

Go + twitter + libnotify

2015. 10. 10.

A Go egy viszonylag új nyelv a Google-től. Típusos, egy darab binárisba fordított (azaz piszok gyors) és nyelvi szinten támogatja a párhuzamosságot. Szemeztem vele egy ideje, aztán nemrég kipróbáltam. Az első program, amit írtam benne egy twitter értesítő volt, mert miért ne? Két elég egyszerű dolog kombinációja, vannak rájuk kész libek, és életszagú.

Választott twitter libem az anaconda, libnotify-ra pedig a go-notify. A projekt mappa létrehozása után gyorsan go geteltem is őket; a twitter fejlesztői oldalán pedig regisztráltam egy új appot, szerezve négy tokent. Ezeket env változóban passzolom át a kódnak (mert sehogy nem sikerült json-ból beolvasnom, azóta leesett, hogy azért, mert kisbetűs névvel voltak a structomban), így:

consumerKey := os.Getenv("TWITTER_CONSUMER_KEY")
consumerSecret := os.Getenv("TWITTER_CONSUMER_SECRET")
accessToken := os.Getenv("TWITTER_ACCESS_TOKEN")
accessTokenSecret := os.Getenv("TWITTER_ACCESS_TOKEN_SECRET")

Amivel aztán initelhetjük az anacondát, és rögtön próbáljuk is ki, minden stimmel-e:

anaconda.SetConsumerKey(consumerKey)
anaconda.SetConsumerSecret(consumerSecret)
api := anaconda.NewTwitterApi(accessToken, accessTokenSecret)

user, err := api.GetSelf(nil)
if err != nil {
	panic(err)
}

fmt.Printf("Logged in as @%s\n", user.ScreenName)
go doNotify(
	"Twitter",
	"Logged in as @" + user.ScreenName,
	user.ProfileImageURL)

A notificationöket megjelenítő kód külön függvénybe került már csak azért is, mert egy bug miatt minden notif előtt initelni, utána pedig uninitelni kell a libraryt - a kettő között pár pásodperc késleltetéssel. Extraként bekerült a user képének letöltése, rengeteg hibakezeléssel :)

func doNotify(title, text, image string) {
	if "" != image {
		filename := "/tmp/twitter-" + getMd5(image)
		output, _ := os.Create(filename)
		response, _ := http.Get(image)
		io.Copy(output, response.Body)

		defer output.Close()
		defer response.Body.Close()
		defer os.Remove(filename)

		image = filename
	}

	notify.Init("twitter-stream-notify")
	defer notify.UnInit()

	notification := notify.NotificationNew(title, text, image)
	notify.NotificationShow(notification)

	time.Sleep(10 * time.Second)
	notify.NotificationClose(notification)
}

Mivel go prefixszel hívom mindenütt, külön szálon indul, a sleep véletlen sem fogja meg a fő threadet.

Szóval már bejelentkeztünk, megvan az első notifünk is, még hátravan a tweetekre várás és azokról értesítés összerakása. Ezzel a résszel szórakoztam a legtöbbet, az anaconda dokumentációja nem épp részletes a streamekkel kapcsolatban, úgyhogy a kódból szedtem össze az infot - nem volt egyszerű egy tökúj nyelven.

Először a twittertől kell elkérnünk a user streamjét - ez azokat a tweeteket jelenti valós időben, amiket az általa követettek írnak. A lib elfedi ennek a részleteit, és a végén egy go channelbe pakolja nekünk, ebből kell végtelenciklusban olvasnunk. Ide jöhet sokfajta üzenet, minket ezek közül csak a tweetek érdekelnek - a típus eldöntése a goban elsőre nem egyértelmű, de utólag nagyon kellemes és praktikus megoldás.

stream := api.UserStream(nil)

for {
	select {
	case data := <-stream.C:
		if tweet, ok := data.(anaconda.Tweet); ok {
			fmt.Printf("@%s: %s\n", tweet.User.ScreenName, tweet.Text)
			go doNotify("@" + tweet.User.ScreenName,
				tweet.Text,
				tweet.User.ProfileImageURL)
		}
	}
}

A teljes kód fenn van githubon. Az egészet kb. egy délután alatt raktam össze, és nagyon megtetszett a nyelv. Azóta nekiestem mégegy projektnek, az melóhoz kapcsolódik, egy nodejs microservice-t kezdtem újrairni goban, egy hétvége alatt összeálltak a fő funkciók, azonos körülmények között hatszor gyorsabb (ab-val mérve) és jelentősen kevesebb ramot eszik. Biztosan fogok még ezzel a nyelvvel játszani.

HHVM

2014. 03. 30.

A HHVM a Facebook alternatív PHP interpretere, ami azzal az ígérettel érkezik, hogy jelentősen gyorsabb a klasszikusnál. A héten tartottam erről előadást a budapest PHP meetupon, alant ennek összefoglalója jön.

Történeti összefoglaló

Az első változat, akkor még HPHP néven, 2008-ban jelent meg. Ez egy php -> c++ fordító volt, közel a teljes 5.2-es nyelvet ismerte, a két főbb kivétel az eval és a create_function. A saját szervereiken eddigre már ezt használtál élesben, drámai sebességnövekedést tapasztalva. Komoly hátránya volt az előállt, esetenként többgigás binárist rövid idő alatt kiszórni a szerverekre.

A HPHPc (compiler) nem volt alkalmas hibakeresésre, ennek megoldására jött létre a HPHPd (debugger) változat, ebben lehetett watch-okat és breakpointokat beállítani, mint ma egy modern IDE + xdebug kombinációban lehetséges.

2010-ben kezdődött a munka a következő változaton, ez már HHVM néven futott. Ahogy a neve is mondja, ez egy virtuális gép, ami a PHP kódot egy köztes bytekódra fordítja, majd ebből just-in-time készít natív x64 kódot, amit futtat végül. Hatalmas előnye a HPHPc-hez képest, hogy itt nincs hatalmas bináris, amit terjeszteni kéne; viszont a teljesítménybeli javulás nagyrészt megmaradt. Ráadásul az időigényes fordítást sem kell kivárni. A HHVM ismeri közel a teljes 5.4-es nyelvet, beleértve az eval és create_function függvényeket.

2013-ban a HPHPc deprecated lett. (Van erre értelmes magyar kifejezés?)

2014-ben bemutatták a Hack nyelvet, ami egy erősen bővített PHP, erről később írok bővebben.

Telepítés

A HHVM hivatalosan csak linuxon támogatott. OSX-re és FreeBSD-re vannak nemhivatalos fordítási leírások.

A github wikijén számos disztróra találunk forgatási útmutatót, mindenféle verzióra. Debianra (tehát Mintre és Ubuntura is) valamint Centosra vannak előre elkészített csomagok. Az én laptopomon ennyi volt az egész:

wget http://dl.hhvm.com/conf/hhvm.gpg.key
sudo apt-key add hhvm.gpg.key
echo deb http://dl.hhvm.com/ubuntu saucy main | 
    sudo tee /etc/apt/sources.list.d/hhvm.list
sudo apt-get update
sudo apt-get install hhvm

Az utolsó parancs végeztével rögtön elérhető a hhvm parancs:

$ hhvm -–version
HipHop VM 2.4.2 (rel)
Compiler: tags/HHVM-2.4.2-0-g432ecffa04b21c60953bb236a9db8278f4650537
Repo schema: 1be260b29a71097b5d1f78c6e4dcbb981ba03bde

$ hhvm -m d
Welcome to HipHop Debugger!
Type "help" or "?" for a complete list of commands.
hphpd> phpinfo()
HipHop

Aki a saját rendszerén nem szeretne turkálni, vagy nem támogatott oprendszert használ, létezik vagrant box, ami egy ubuntu guestre forrásból forgatja a legfrissebb HHVM-et: javier/hhvm-vagrant-vm

Aki pedig azon gondolkodik, hol tudja ezt saját szerver nélkül hostolni: van már Heroku buildpack , ami push után HHVM-el szolgál ki.

Extensionök

A legfontosabb core extensionök out-of-the-box jönnek a HHVM-el beépítve, és várhatóak újak. [Itt a teljes lista][extensions], a szerintem legfontosabbak pedig:

És persze adott a lehetőség sajátok írására.

Konfiguráció

Az előadásban erről túl sok szót nem ejtettem, és okkal: annyi beállítási lehetőség van, hogy azok megérnének több saját előadást. Itt is csak a szerintem legfontosabbakról írok. A teljes és részletes leírást Runtime options címen githubon találjátok.

Log {
    NoSilencer = false
    AlwaysLogUnhandledExceptions = true
    InjectedStackTrace = true
    NativeStackTrace = true
}

A NoSilencer kapcsolóval lehet logoltatni a @ operátorral elnyomott hibákat is. Az AlwaysLogUnhandledExceptions akkor is logolja a fatal errorokat, ha arra van user-defined error handler. Az utolsó kettő pedig a logba írt stack tracke-ek formátumát szabályozza, illetve hogy a php vagy a c++ kódé legyen-e.

Eval.Debugger {
  EnableDebugger = true                                          
  EnableDebuggerServer = true
  Port = 8089
  DefaultSandboxPath = path to source files
}

Alapból a debugger (hphpd) nem figyel, így lehet bekapcsolni. Indítás után a logból is látszik, hogy várja a kapcsolatokat.

HHVM mint FCGI

Amikor az előadást tartottam akkor még lehett önálló szerverként futtatni a HHVM-et, nem kellett elé külön http szerver. Ezzel járt az, hogy rengeteg konfigurációs paraméter az egyéb file-okkal foglalkozott, és nem a php értelmezővel. 2013 decemberében bemutatták az új, FastCGI felületét, amivel ezt a problémát megoldották, és a HHVM már tényleg csak a php kód kiszolgálásáért volt felelős.

Időközben kijött a 3.0-ás verzió, amiben ez a működési mód már nem létezik. Jóideje ez volt az ajánlás, és production/staging környezetben valószínűleg nem okoz meglepetést, debug során viszont olyan hasznos volt, mint a php beépített szervere.

A FCGI mód bekapcsolásához ezt kell beleírnunk a konfigunk Server blokkjába:

Server {
  // ...
  Type = fastcgi
  Post = 9000
  // vagy
  FileSocket = /var/run/hhvm.sock
}

Nginx példa:

root /var/www;
fastcgi_pass  unix:/var/run/hhvm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME$document_root$fastcgi_script_name;
include       fastcgi_params;

És hogy milyen teljesítménybeli javulást hoz ez? A gyári tesztek szerint egy stock wordpress telepítéssel ennyit (ab-val tesztelve):

PHP-FPM

Requests per second: 23.17 [#/sec] (mean)
Time per request:    2157.579 [ms] (mean)
Time per request:    43.152 [ms] (mean, across all concurrent requests)
Transfer rate:       275.42 [Kbytes/sec] received

HHVM + FastCGI

Requests per second: 184.71 [#/sec] (mean)
Time per request:    270.689 [ms] (mean)
Time per request:    5.414 [ms] (mean, across all concurrent requests)
Transfer rate:       2194.38 [Kbytes/sec] received

Már itt hatalmas a javulás, de aztán amikor rendesen beindul a JIT, még látványosabb:

Requests per second: 949.21 [#/sec] (mean)
Time per request:    52.676 [ms] (mean)
Time per request:    1.054 [ms] (mean, across all concurrent requests)
Transfer rate:       11276.46 [Kbytes/sec] received

Bár teljesen mesterséges teszt, a Fibonacci-számolás szépen kihozza az interpretált és a natív (JIT-fordított) kód teljesítménye közti különbséget:

PHP-FPM

Requests per second: 13789.24 [#/sec] (mean) Fib(5)
Requests per second: 3202.31 [#/sec] (mean)  Fib(15)
Requests per second: 118.94 [#/sec] (mean)   Fib(25)
Requests per second: 8.40 [#/sec] (mean)     Fib(30)

HHVM + FastCGI

Requests per second: 8842.70 [#/sec] (mean) Fib(5)
Requests per second: 8892.66 [#/sec] (mean) Fib(15)
Requests per second: 5581.37 [#/sec] (mean) Fib(25)
Requests per second: 737.56 [#/sec] (mean)  Fib(30)

Hack

Kicsit több, mint egy hete jelentették be a HHVM-re épülő Hack nyelvet. Ez egy jelentősen kibővített PHP, ezekkel a fontos újításokkal:

Skaláris típusok

Objektumokra, interfészekre, tömbökre most is lehet typehintelni, de skalárokra nem. A Hack ezt lehetővé teszi osztályváltozókra, konstansokra, függvényparaméterekre és -visszatérési értékekre:

<?hh

class MyClass {
  const int MyConst = 0;
  
  private string $x = '';
  
  public function increment(int $x): int {
    $y = $x + 1;
    return $y;
  }
}

Vegyük még észre, hogy a megszokott <?php helyett <?hh a nyitótag.

Genericek

Részben a típusosságból következik, hogy a C++ template-es osztályaival analóg módon tudunk általánosan, típusfüggetlenül saját osztályokat leírni, aztán JIT-fordítás időben létrejön belőle a konkrét típushoz való változat:

<?hh

class Box<T> {
    protected T $data;
    
    public function __construct(T $data) {
        $this->data = $data;
    }

    public function getData(): T {
        return $this->data;
    }
}

Collection-ök

A php tömbjei nagyon sokoldalúak, de pont emiatt bizonyos célokra nem ideálisak. A Hack behozta a C++ STL-jéből ismert Vector, Map, Set és Pair osztályokat, melyek egy része bár elérhető SPL-ből, ezek az új változatok mégis jóval többet tudnak - a típuskényszerítésen kívül is. Részletes bemutatás a HHVM dokumentációjában.

Új lamda syntax

Ez csak syntax sugar, de jóval egyszerűbben lehet névtelen függvényeket definiálni, mint php-ban:

<?hh

function foo(): (function(string): string) {
  $x = 'bar';
  return $y ==> $x . $y;
}
function test(): void {
  $fn = foo();
  echo $fn('baz'); // barbaz
}

Shape-ek

PHP-ban nincs struct típus. Ennek megoldására jön a shape, ami fix kulcsos tömböket jelent - ha valamely hiányzik vagy plusz van, akkor az hiba:

<?hh

type my_shape = shape(
    "field1" => int,
    "field2" => bool,
);

function first_shape(): my_shape {
    return shape("field1" => 42, "field2" => false);
}

Async függvényhívások

Ezt még nem sikerült teljesen felfognom. A lényege annyi, hogy aszinkron függvényhívást tesz lehetővé, amely a futása után meghív egy Callbacket. A node.js-re hasonlít, de további kísérletezést igényel. Részletes leírás.

A Hackkel további ismerkedéshez ajánlom az interaktív tutorialjukat.

Mire jó ez ma?

Célként tűzték ki, hogy minnél több frameworkkel legyenek kompatibilisek, az aktuális állás megtekinthető itt: frameworks. Eszerint például laravelt vagy codeignitert ma már nyugodtan futtathatunk rajta, nem fogunk hibába futni. Van azonban valami, ami a mindennapjainkat nagyban javíthatja:

Composer HHVM-el

$ alias hhvm-composer="hhvm -v ResourceLimit.SocketDefaultTimeout=30 
    -v Http.SlowQueryTreshold=30000 
    -v Eval.Jit=false /usr/local/bin/composer`

Az intl-függő csomagokkal probléma lehet, de ezen túllépve négy-ötszörös sebességnövekedést tapasztaltam. A beállítások a lassú hálózaból fakadó problémák elkerülésére szolgálnak, your mileage may vary.

PHPUnit HHVM-el

$ composer global require "phpunit/phpunit=4.0.*"

$ alias hhvm-phpunit="hhvm -v Eval.Jit=false ~/.composer/vendor/bin/phpunit"

Nagyon csábító dolog, de valamire nagyon figyelni kell: ekkor a HHVM stack ellen teszteljük a kódunkat.

Vagrant

2013. 10. 28.

A napi munka során gyakran előkerül az a probléma, hogy mindhárom nagy operációs rendszer használatban van a cégnél. Hosszadalmas munka mindegyiken egy ugyanolyan fejlesztőkörnyezetet beállítani, és még így sem ideális a helyzet: a Windows kis/nagybetűre érzéketlen filerendszerén történő fejlesztés után könnyen problémákba ütközhetünk egy Linuxos case-sensitive rendszerre történő deploy során. És ekkor még nem esett szó az olyan, egyes alkalmazások működéséhez szükséges külső komponensekről, mint egy queue szerver, MTA vagy adatbázis.

És itt lép színre a Vagrant.

Mi ez?

A Vagrant egy olyan program, melynek célja az, hogy könnyen és gyorsan tudjunk több gépen (és több operációs rendszeren) azonos virtuális gépeket előállítani. Így például a repository-nk klónozása és egy vagrant up után máris lehet dolgozni anélkül, hogy akár órákat kellene eltöltenünk egy virtuális gép telepítésével és beállításával. Alapértelmezetten VirtualBoxot használ a háttérben (mert minden platformon fut és ingyenes), de van lehetőség VMWare használatára is.

Ha ez a gép elkészült, onnantól az futtatja a kódunk, de a saját operációs rendszerünk böngészőjével, fejlesztői eszközeivel tudunk továbbra is dolgozni.

Hogyan működik?

A rendszer lelke a repositoryban levő Vagrantfile, amiben a gépünk főbb paramétereit adhatjuk meg, például hogy milyen alaprendszerre épül, legyen-e hálózati elérése és hogyan, illetve hogy milyen módszerrel történjen a provisioning (a telepítés utáni konfiguráció). Ez egy egyszerű, ruby szintaxist követő file, például valami hasonló:

Vagrant.configure("2") do |config|
  config.vm.box = "precise64"
  config.vm.box_url = "http://files.vagrantup.com/precise64.box"

  config.vm.network :private_network, ip: "192.168.33.33"
    config.vm.network :forwarded_port, guest: 80, host: 8080
    config.ssh.forward_agent = true

  config.vm.provider :virtualbox do |v|
    v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
    v.customize ["modifyvm", :id, "--memory", 512]
  end

  nfs_setting = RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /linux/
  config.vm.synced_folder "./", "/vagrant", id: "vagrant-root" , :nfs => nfs_setting

  config.vm.provision :shell, :inline => 'echo -e "mysql_root_password=root
controluser_password=root" > /etc/phpmyadmin.facts;'

  config.vm.provision :puppet do |puppet|
    puppet.manifests_path = "vagrant"
    puppet.module_path = "vagrant/modules"
    puppet.options = ['--verbose']
  end
end

Ez egy Ubuntu Precise-ra (12.04 LTS) épülő gépet ír le, aminek van egy belső hálózati címe, a host gép 8080-as portja át van irányítva a guest 80-as portjára, van internet-elérése, 512MB RAM-ja, egy szinkronizált mappája (a Vagrantfile mappája a guest gép /vagrant útvonalán), valamint puppettel történik a konfigurációja.

Használata

Az alapvető műveletekhez csak néhány parancsot kell megtanulnunk:

Puppet

A használata önmagában is megérne egy bejegyzést, ezért itt csak a fő configfile-on megyek végig, ami jelen esetben a vagrant/default.pp. Akit érdekel a teljes beállítás, githubon megtekintheti.

group { 'puppet': ensure => present }
Exec { path => [ '/bin/', '/sbin/', '/usr/bin/', '/usr/sbin/' ] }
File { owner => 0, group => 0, mode => 0644 }

Alapbeállítások. Legyen egy puppet nevű csoport a gépen, ez legyen a PATH, valamint az új file-ok alapértelmezett tulaja a root, 644-es engedélyekkel.

class {'apt':
  always_apt_update => true,
}

Class['::apt::update'] -> Package <|
    title != 'python-software-properties'
and title != 'software-properties-common'
|>

Futtasson egy apt-get update-et minden csomagra, ami nincs a felsoroltak közt.

apt::key { '4F4EA0AAE5267A6C': }

apt::ppa { 'ppa:ondrej/php5-oldstable':
  require => Apt::Key['4F4EA0AAE5267A6C']
}

Adjon hozzá egy új APT kulcsot, majd egy PPA-t az előbbi kulccsal. Ez az 5.4-es php-hoz szükséges, mert a Precise-ban csak 5.3 elérhető.

package { [
    'build-essential',
    'vim',
    'curl',
    'git-core',
    'mc',
  ]:
  ensure  => 'installed',
}

Ezek a csomagok legyenek telepítve.

class { 'apache': }

apache::dotconf { 'custom':
  content => 'EnableSendfile Off',
}

apache::module { 'rewrite': }

file { '/etc/apache2/sites-enabled/000-default':
  ensure => absent,
}

apache::vhost { 'localhost':
  server_name   => false, # hogy ez legyen a default
  serveraliases => [
],
  docroot       => '/vagrant/',
  port          => '80',
  env_variables => [
],
  priority      => '1',
}

Legyen apache-unk, amiben tiltva van a sendfile, engedélyezve a rewrite modul. Szintén tiltva legyen az alapértelmezetten létrehozott virtualhost, mert sajátot akarunk csinálni localhost néve, és ezt szeretnénk a defaultnak; a DocumentRoot pedig legyen a korábban beállított /vagrant, ami a host gépen azzal a mappával van szinkronizálva, amelyben a Vagrantfile található.

class { 'php':
  service             => 'apache',
  service_autorestart => false,
  module_prefix       => '',
}

php::module { 'php5-mysql': }
php::module { 'php5-cli': }
php::module { 'php5-curl': }
php::module { 'php5-gd': }
php::module { 'php5-intl': }
php::module { 'php5-mcrypt': }

class { 'php::devel':
  require => Class['php'],
}

class { 'php::pear':
  require => Class['php'],
}

class { 'xdebug':
  service => 'apache',
}

Legyen telepítve PHP az adott modulokkal, fejlesztésre előkészítve, xdebuggal.

class { 'composer':
  require => Package['php5', 'curl'],
}

Legyen rendszerszinten elérhető a Composer.

puphpet::ini { 'xdebug':
  value   => [
    'xdebug.default_enable = 1',
    'xdebug.remote_autostart = 0',
    'xdebug.remote_connect_back = 1',
    'xdebug.remote_enable = 1',
    'xdebug.remote_handler = "dbgp"',
    'xdebug.remote_port = 9000'
  ],
  ini     => '/etc/php5/conf.d/zzz_xdebug.ini',
  notify  => Service['apache'],
  require => Class['php'],
}

Az xdebug legyen engedélyezve, és lehessen távolról kapcsolódni hozzá. Ez a gyakorlatban a helyi hálózatot jelenti, így lehet a host gépről, vagy akár egy másik gépről is debuggolni.

puphpet::ini { 'php':
  value   => [
    'date.timezone = "Europe/Budapest"'
  ],
  ini     => '/etc/php5/conf.d/zzz_php.ini',
  notify  => Service['apache'],
  require => Class['php'],
}

puphpet::ini { 'custom':
  value   => [
    'display_errors = On',
    'error_reporting = -1',
    'allow_url_fopen = 1'
  ],
  ini     => '/etc/php5/conf.d/zzz_custom.ini',
  notify  => Service['apache'],
  require => Class['php'],
}

Legyen a PHP-nk Budapesti időzónára beállítva, mutassa a hibákat (az összeset), és legyen engedélyezve a http protocol wrapper.

class { 'mysql::server':
  config_hash   => { 'root_password' => 'root' }
}

mysql::db { 'silex-alap':
  grant    => [
    'ALL'
  ],
  user     => 'silex-alap',
  password => 'silex-alap',
  host     => 'localhost',
  sql      => '/vagrant/db.sql',
  charset  => 'utf8',
  require  => Class['mysql::server'],
}

Legyen MySQL a gépen, a root jelszó legyen root, ezen felül jöjjön létre egy silex-alap adatbázis ugyanilyen nevű és jelszavú felhasználóval, akinek mindenre van benne joga, és rögtön importálja is ide a db.sql file-t.

class { 'phpmyadmin':
  require => [Class['mysql::server'], Class['mysql::config'], Class['php']],
}

file { '/etc/apache2/conf.d/phpmyadmin':
    ensure => 'link',
    target => '/etc/phpmyadmin/apache.conf',
    require => Class["phpmyadmin"],
}

Legyen telepítve phpmyadmin, és annak confja legyen engedélyezve az apache-ban, így lehessen elérni a /phpmyadmin útvonalon.

Végeredmény

A silex-alap repository klónozása után a gyökerében egyetlen vagrant up parancs (és egy kávényi várakozás) után van:

Enum mező Doctrine2-ben

2013. 09. 16.

A Doctrine2 nagyon szép és jó, viszont a platformfüggetlenség jegyében (helyesen) nem támogatja beépítve a csak MySQL-ben létező enum mezőt. Számunkra viszont ez elég hasznos volna, lássuk hát, hogyan tudjuk belevarázsolni Symfony2 keretek közt.

Az osztályok felépítését úgy oldottam meg, hogy van egy ős, amiben le van írva az összes művelet, valamint igény szerinti számban ennek a gyerekei egyedi névvel és a hozzájuk tartozó értékekkel.

Új típust úgy tudunk létrehozni, hogy a Doctrine\DBAL\Types\Type osztályból származtatunk. Íme az ősünk:

<?php

namespace Foo\BarBundle\DBAL;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;

abstract class EnumType extends Type
{
    protected $name;
    protected static $values;

    public function getSqlDeclaration(array $fieldDeclaration,
        AbstractPlatform $platform)
    {
        return
           $platform->getVarcharTypeDeclarationSQL($fieldDeclaration);
    }

    public function convertToPHPValue($value,
        AbstractPlatform $platform)
    {
        return $value;
    }

    public function convertToDatabaseValue($value,
        AbstractPlatform $platform)
    {
        if (!in_array($value, static::$values)) {
            throw new \InvalidArgumentException(
                "Invalid '".$this->name."' value."
            );
        }

        return $value;
    }

    public function getName()
    {
        return $this->name;
    }

    public static function getValues()
    {
        return static::$values;
    }

    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}

(A kódba plusz töréseket raktam, hogy kiférjen.)

Részletesen, hogy mi mit csinál:

Az utolsó függvényről picit bővebben szeretnék írni. Amikor migrálást csinálunk, akkor a Doctrine az adatbázis állapota alapján és a kódunk alapján készít egy-egy sémát, aztán ezeket hasonlítja össze. Az enum mezőnk az elsőben varchar, a másodikban saját típus lesz, ezért minden alkalommal módosítaná, mert nem tudja megkülönböztetni. E függvényünk igaz visszatérési értéke miatt viszont a mezőhöz hozzácsap egy “(DC2Type:valamilyenenum)” kommentet, ami alapján később pontosan tudja, milyen típusúnak is kell lennie.

Konkrét típust két lépésben tudunk létrehozni: származtatunk kell az előző osztályból, valamint regisztrálni kell azt. A gyerekre egy példa:

<?php

namespace Foo\BarBundle\DBAL;

class FejlecLablecEnumType extends EnumType
{
    protected $name = "fejleclablecenum";
    protected static $values = array(
        "fejléc"    =>  "fejléc",
        "lábléc"    =>  "lábléc",
    );
}

A regisztráció pedig csak ennyi:

# app/config/config.yml
doctrine:
    dbal:
        types:
            fejleclablecenum: Foo\BarBundle\DBAL\FejlecLablecEnumType

És máris tudjuk használni az entity-jeink leírásához:

/**
 * @ORM\Column(type="fejleclablecenum")
 */
protected $tipus;

Illetve a formjainkban:

$tipusok = FejlecLablecEnumType::getValues();
$builder->add("tipus", "choice", array(
    "label"         =>  "Típus",
    "choices"       =>  $tipusok,
    "constraints"   =>  array(
        new Assert\NotBlank(),
        new Assert\Choice(array("choices" => array_keys($tipusok))),
    ),
));

Ez utóbbi megoldás nem a legszebb, de még nem találtam rá jobbat.

A PHP 5.5 új jelszókezelő függvényei

2013. 06. 03.

A PHP 5.5-ös verziójától kezdve elérhető lesz néhány új függvény, amelyek a jelszavak kezelését segítik. Ezekről fogok írni most, hogy miért volt rájuk szükség, miért kerültek be a PHP magjába, és hogyan lehet őket használni.

Háttér

Hiába ismert az MD5-ben egy hiba 1996, a SHA1-ben pedig 2005 óta, a mai napig ezek azok a függvények, amelyeket a PHP-ban programozók többsége használ a jelszavak egyirányú kódolására (hash-selérére). Rossz esetben salt nélkül, kicsit jobb esetben egy jelszavanként különböző salttal.

Akárhogy is, ezekben egy hiba közös: sem az MD5, sem az SHA1 nem erre lett megalkotva. Mindkettőnek eredeti célja az, hogy üzenetek gyors integritás-ellenőrzését tegyék lehetővé – tehát fontos szempont volt tervezésükkor, hogy gyorsak legyenek, és egyszerűen meg lehessen őket valósítani áramkörök szintjén. Pontosan ez a sebesség teszi őket alkalmatlanná arra, hogy jelszavakat biztonságosan tároljunk benne.

2012 májusában Anthony Ferrara hozta létre a problémát megoldani célzott RFC-t, amit érdemes végigolvasni. Ebben részletesen leírja, mi a probléma a jelenlegi helyzettel. Mindössze négy új függvényt ír le azzal a céllal, hogy olyan egyszerű legyen a használata: balgaság legyen bárhogy máshogy csinálni.

A függvények

password_hash

Ezzel tudod hashelni a jelszót új felhasználó létrehozásakor.

string password_hash(string $password, int $algo, array $options = array())

Az első paraméter maga a kódolandó jelszó, a második a használandó algoritmus. Kettő konstans van erre definiálva, a PASSWORD_BCRYPT és a PASSWORD_DEFAULT. Jelenleg mindkettőnek ugyanaz az értéke, de ha később egy erősebb algoritmus is beépítésre kerül, a default arra fog módosulni.

Az opciók közt két értéket adhatunk meg: az egyik a salt, amit ha kihagyunk, random generál egyet; a másik a cost, ami az algoritmus műveletigényére van hatással. Ezt érdemes úgy belőni, hogy egy másodperc körüli idő alatt számoljon egy hasht. Érvényes bármely 4 és 30 közti egész, az alapértelmezett a 10.

Visszatérési értéke bcrypt esetén egy fix 60 karakteres string, ami tartalmaz mindent, amire ellenőrzéshez szükségünk van, azaz elég ezt mentenünk.

password_verify

Ezzel lehet ellenőrizni bejelentkezéskor, helyes jelszót adott-e meg a felhasználó.

bool password_verify($password, $hash)

Két paramétert vár, az első a kapott jelszó, a második a korábban mentett hash. Azt adja vissza, stimmel-e.

Érdekesség, hogy a háttérben egy olyan string-összehasonlító függvényt használ, ami ellenálló az időzítéses támadásokkal szemben: nem tér vissza hamissal az első eltérésnél, hanem mindenképp végignézi mindkét stringet.

password_needs_rehash

Ezzel azt tudod eldönteni, szükséges-e a jelszó újrakódolása azért, mert a rendszer közben átállt új algoritmusra, vagy nagyobb költségre. A bejelentkezési folyamat részeként lehet használni, amikor megvan a kódolatlan jelszó is.

bool password_needs_rehash(string $hash, int $algo, array $options = array())

Paraméterei annyiban térnek el a password_hash-étől, hogy ez egy már kódolt jelszót vár, nem egy eredetit. Azt adja vissza, gyengébb paraméterekkel (algoritmus, költség) lett-e hash-elve, mint amit kapott.

password_get_info

Ezzel tudod egy hash kódoláskor megadott paramétereit.

array password_get_info(string $hash)

A visszatérési értéke egy tömb algo és options kulcsokkal, az első a használt algoritmust adja meg, a második az annak átadott paramétereket, mint például a költség.

Használati példa

Az alábbi példa a fentebb linkelt RFC-ből származik. Megmutatja, hogy kell egy kapott jelszót hash-elni adott paraméterekkel, azt ellenőrizni, illetve megnézni, szükséges-e az újbóli kódolás.

<?php
$password = "rasmuslerdorf";
$hash = password_hash($password, PASSWORD_BCRYPT, array(
    "cost" => 7,
    "salt" => "usesomesillystringfor",
)));

if (password_verify($password, $hash)) {
    if (password_needs_rehash($hash, PASSWORD_BCRYPT, array(
            'cost' => 8,
    ))) {
        update_password_in_db($password);
    }
    log_user_in();
} else {
    error_wrong_password();
}

Az jól látható, hogy innentől felesleges egyéb megoldásokat használni, ennél tényleg nem lehet egyszerűbb.

És 5.5 előtt?

Nem kell félni attól, hogy ezek csak évek múltán lennének használhatók, amikor már a szolgáltatók többsége átállt 5.5-re. Az eredeti, C nyelvű patch szerzője megvalósította ugyanazt PHP-ban is password_compat néven, ezt már 5.3.7-től kezdve lehet használni. Működése pontosan ugyanaz, mint a majdani beépített függvényeké.

Szakdolgozat

2013. 05. 22.

Két héttel ezelőtt, május 6-án, leadtam a szakdolgozatom.

A témám egy olyan rendszer készítése volt, mely egy adott szervezetnek tud segíteni az ülései szervezésében, illetve azok jegyzőkönyveinek elkészítésében. Mindezt webalkalmazásként, a Symfony2 keretrendszer használatával műveltem.

A kód még nem publikus, de a dokumentáció igen - jó kockaként LaTeX-ben írtam, hogy lehessen verziókövetni. A teljes forrás, valamint a pdf kimenet elérhető a githubon.

Meetup előadás

2012. 12. 11.

Nemrég a Veszprémi Technology Meetupon előadás tartottam az NXCről. A diasor itt letölthető, vagy elérhető a slideshare-en.

FTP-only backup

2012. 08. 30.

A közelmúltban szükségem volt egy olyan backup megoldásra, ami akkor is működik, ha a tárhelyre csak ftp elérésem van - a phpmyadmin is csak a szolgáltató saját adminfelületén keresztül érhető el.

A háttér az, hogy jóatyám céges joomlás oldalát felnyomták. Mivel nem saját kód, nem volt verziózva. A szolgáltató backupolt, de ez nem letölthető, hogy összehasonlítsd korábbi változattal, hanem vissza lehet állni rá.

Páróra játék eredménye ez a két script.

A megoldás egy php és egy bash scriptből áll. A php maga mellé egy file-ba menti a teljes DB dumpot, a felhasznált függvény David Walsh blogjáról való. Ez a mappa .htaccess-el védve van, csak jelszó ismeretében érhető el (tudom, hogy nem az igazi, de nem találtam jobb megoldást). Cirka egymegás dumpot csinál egy másodpercen belül.

A lényegi munkát a shellscript végzi. A környezet: a mappa, amiben dolgozik, egyaránt egy checkoutolt git repo, valamint a teljes ftp tartalmat letöltő wget parancs kimenete. A lépések:

  1. a .git-en kívül minden törlése
  2. adatbázis backup triggerelése
  3. teljes ftp tartalom mentése
  4. ha az előbbi meghiúsult, értesítő levél és kilépés
  5. ha nem változott semmi file, kilépés
  6. minden változás commitolása, pusholása
  7. értesítő levél

A központi szerver egy saját hostolású GitLab, így weben keresztül is vissza lehet nézni, mikor mi változott. Gittel természetesen az adott időpontra visszaállás is könnyen megoldható.