Linux: Einfaches Backup-Script

Heute möchte ich mein einfaches „Backup-Script“ für Linux-Rechner vorstellen. In meinem Szenario muss ich eine MySQL-Datenbank und ein paar Verzeichnisse sichern. Die Sicherungskopie möchte ich dann verschlüsseln und auf einen anderen Rechner laden.

Zunächst logge ich mich auf dem Rechner ein, auf dem ich Daten sichern will. Wenn ich noch keinen öffentlichen SSH-Schlüssel habe, lege ich einen neuen an (kein Passwort für den Schlüssel vergeben!):
ssh-keygen -t rsa

Diesen kopiere ich dann auf den Rechner, der die Sicherungskopien speichern soll:
ssh-copy-id benutzer@backup.agrafix.net

Nun lege ich eine neue Datei an, die den Verschlüsselungsschlüssel enthält:
openssl rand -base64 512 > key.txt

Jetzt zum eigentlichen Script:

#!/bin/bash

today=$(date +"%Y_%m_%d")

# CONFIG
mail="mail@agrafix.net"
keyFile="$HOME/key.txt"
backtarget="benutzer@backup.agrafix.net:a01/${today}.tar.gz"
backup=( "$HOME" "/var/www" )
mysqlUser="mysqluser"
mysqlPass="mysqlpass"
mysqlDb=( "mysqldb" )
# END CONFIG

dir=`mktemp -d`
target="/tmp/${today}_back.tar.gz"

for db in "${mysqlDb[@]}"
do
    echo "Backing up mysql to ${dir}/${db}.sql to ${db}.sql"
    mysqldump -u $mysqlUser -p"$mysqlPass" $db > "${dir}/${db}.sql"
done

for d in "${backup[@]}"
do
    backupName=${d////_}
    echo "Backing up ${d} to ${backupName}.tar.gz"
    tar --ignore-failed-read -f "${dir}/${backupName}.tar.gz" -cz $d
done

echo "Compressing everything to ${target}"
tar cfz - $dir | openssl enc -aes-256-cbc -kfile $keyFile -e > $target

echo "Done. Now uploading ${target} to backup machine ${backtarget}"
scp "${target}" "${backuptarget}"

echo "Deleting local backup"
rm -f $target
rm -rf $dir

echo "Sending email"
echo "Backup complete! ${today}" | mail -s 'Backup completed' $mail

echo "Okay! :-)"

Um ein Backup wieder einzuspielen, muss die entsprechende .tar.gz Datei zurück kopiert werden, und kann dann mit

openssl enc -in $1 -kfile key.txt -aes-256-cbc -d | tar -zxvf -

entpackt werden.

 

Einfache Datenbank-Klasse für PHP

Dieser Beitrag ist Teil meiner Sourecode a Day-Aktion.

Ich möchte eine Klasse vorstellen, die ich mir mal geschrieben habe um einfach auf mySQL-Datenbanken zuzugreifen. Ich verwende sie selbst allerdings nicht mehr, da CodeIgniter eine eigene Datenbank-Abstraktion liefert und die meisten PHP-Hosts jetzt auch mysqli unterstützen!

<?php
/**
 * mySQL Database Class
 *
 * @author Alexander Thiemann 
 */
class RealSQL {
	/**
	 * db connector
	 *
	 * @var resource
	 */
	protected $conn;
	
	/**
	 * save last query
	 *
	 * @var string
	 */
	protected $last_query = "";
	
	/**
	 * save query results
	 *
	 * @var array
	 */
	protected $results = array();
	
	/**
	 * Store variables, eg. sql replacements
	 *
	 * @var array
	 */
	private $stored_variables = array();
	
	/**
	 * constructor -> connect
	 *
	 * @param string $host
	 * @param string $user
	 * @param string $pass
	 * @param string $db
	 */
	public function __construct($host, $user, $pass, $db) {
		$this->conn = @mysql_connect($host, $user, $pass);
		
		if (!$this->conn) {
			$this->error();
		}
		
		if (!@mysql_select_db($db, $this->conn)) {
			$this->error();
		}
	}
	
	/**
	 * execute query
	 *
	 * @param string $query
	 * @param int $id
	 */
	public function query($query, $id=0) {
		if (strpos($query, "INSERT INTO") !== false) {
			if (!@mysql_unbuffered_query($query, $this->conn)) {
				$this->last_query = $query;
				$this->error();
			}
		}
		else {
			$this->results[$id] = @mysql_query($query, $this->conn);
			
			if (!$this->results[$id]) {
				$this->last_query = $query;
				$this->error();
			}
		}
	}
	
	/**
	 * fetch as array/numrows/etc
	 *
	 * @param int $id
	 * @param string $fetchtype
	 */
	public function fetch($id=0, $fetchtype="") {
		switch(@$fetchtype) {
			case "num_rows":
				return mysql_num_rows($this->results[$id]);
				break;
				
			default:
				$ass = mysql_fetch_assoc($this->results[$id]);
				
				return $ass;
				break;
		}
	}
	
	/**
	 * fetch all in one array
	 *
	 * @param int $id
	 */
	public function tpl_fetch($id=0) {
		$array = array();
		
		while ($r = $this->fetch($id)) {
			$array[] = $r;
		}
		
		return $array;
	}
	
	/**
	 * insert stuff into db
	 *
	 * @param array $data_array
	 * @param string $table
	 */
	public function insert($data_array, $table) {
		$fields = array_keys($data_array);
		
		foreach ($data_array As $k => $v) {
			if (is_numeric($v)) {
				$data_array[$k] = (double)$v;
			}
			else {
				$data_array[$k] = "'".urlencode($v)."'";
			}
		}
		
		$query = "INSERT INTO `".$table."` (`".implode("`,`", $fields)."`) VALUES (".implode(", ", $data_array).")";
		
		$this->query($query);
	}
	
	/**
	 * update fieldset
	 *
	 * @param array $data_array
	 * @param string $table
	 * @param string $where
	 */
	public function update($data_array, $table, $where) {
		
		$update = array();
		
		foreach ($data_array As $k => $v) {
			if (is_numeric($v)) {
				$data_array[$k] = (int)$v;
			}
			else {
				$data_array[$k] = "'".urlencode($v)."'";
			}
			
			$update[] = "`$k` = ".$data_array[$k];
		}
		
		$query = "UPDATE `$table` SET ".implode(", ", $update)." WHERE $where";
		
		$this->query($query);
	}
	
	/**
	 * set param (automatic urlencode+stripslashes+')
	 *
	 * @param string $key
	 * @param string $value
	 */
	public function setParam($key, $value) {
		if (is_string($value)) {
			$i = "'".urlencode(stripslashes($value))."'";
		}
		elseif (is_numeric($value)) {
			$i = $value;
		}
		else {
			$i = "'".urlencode(stripslashes($value))."'";
		}

		$this->stored_variables[$key] = $i;
	}

	/**
	 * delete all params
	 *
	 */
	public function clearParams() {
		$this->stored_variables = array();
	}

	/**
	 * execute paramed query
	 *
	 * @param string $sql
	 * @param int $id
	 */
	public function exec($sql, $id=0) {
		foreach($this->stored_variables AS $key => $val) {
			$sql = str_replace("[$key]", $val, $sql);
		}

		$this->query($sql, $id);
	}
	
	/**
	 * returns last ai-id
	 *
	 * @return int
	 */
	public function lastId() {
		return mysql_insert_id($this->conn);
	}	
	
	/**
	 * error handler
	 *
	 */
	protected function error() {
		// uncomment this line 
		// if you have deployed the app
		#die("Internal Server Error."); 
		
		echo "
"; echo "

RealSQL Error Occured

"; if ($this->last_query != "") { echo "

Query

".htmlspecialchars($this->last_query).""; } echo "

Error

".htmlspecialchars(mysql_error($this->conn)); echo ""; print_r(debug_backtrace()); echo ""; echo "
"; exit; } } ?>

Die Verwendung:

$db = new RealSQL("localhost", "root", "", "test_db");

// einfach eine Query ausführen
$db->query("UPDATE user SET username = 'asdf' WHERE id = 1");

// query ausführen und Ergebnisse holen
$db->query("SELECT * FROM user", 1);

while ($row = $db->fetch(1))
{
	echo "Benutzer: " . $row['username'];
}

// neuen Eintrag in die DB
$db->insert(array('username' => 'test'), 'user');

// Eintrag ändern
$db->update(array('username' => 'banana'), 'user', 'id=2');

// Query mit Parametern
$db->setParam("user", $_POST['user']);
$db->setParam("pass", md5($_POST['pass']));

$db->exec("SELECT * FROM user WHERE username = [user] AND password = [pass]");

$db->clearParams();

Die aller erste Manager’s Life Version verwendete diese Klasse.

 

C++ CGI und mySQL

Ein CGI-Programm in C++ zu schreiben ist – eigentlich – ziemlich einfach:

#include 

using namespace std;

int main()
{
	cout << "Content-Type: text/html" << endl << endl;
	
	cout << "Hello CGI" << endl;
	cout << "

C++ says Hi to the CGI API

" << endl; cout << "" << endl; return 0; }

Das ganze kann man jetzt durch einen C++ Compiler jagen, in das cgi-bin Verzeichniss des Webservers verschrieben, chmod +x, und dann zB unter http://webserver/cgi-bin/test.cgi ansehen. Nicht sehr spannend, da man diese statische Seite ja auch direkt in HTML schreiben hätte können und sich den Umweg über CGI sparen hätte können.

Interessant wird es also, wenn man mit C++ über CGI dynamische Webseiten (dynamisch nicht im Sinne von JavaScript, sondern eher ähnlich wie PHP) generieren möchte. Ein erster Schritt wäre hier, die GET-Variablen auszulesen. Die GET-Variablen werden von der CGI in die Environment-Variable QUERY_STRING geschrieben, und zwar in folgendem Format:
[key1]=[value1]&[key2]=[value2]&....&[keyN]=[valueN]
Wir müssen uns also eine Funktion zum Parsen dieses Strings schreiben:

void getVariables(std::unordered_map &variables)
{
	// fetch the query string
	char* raw_query_string = getenv("QUERY_STRING");

	// query string empty?
	if (raw_query_string == NULL) 
	{
		variables.clear();
		return variables;
	}
	
	// key and value into a stringstream, init with an empty stream
	// to make sure get-queries such as ?step=1&confirm are parsed 
	// correctly
	std::stringstream key, value;

	key.str(std::string(""));
	value.str(std::string(""));

	// are we currently parsing a key or a value?
	bool isKey = true;

	// loop until we read all raw data
	while (*raw_query_string != '')
	{
		// if we read a &, we'll change or type to key and store 
		// are previous key and value variables into out map
		// and clear them
		if (*raw_query_string == '&') 
		{ 
			variables[key.str()] = value.str();
			isKey = true; 
			key.str(std::string(""));
			value.str(std::string(""));
		}
		// if we read a =, we'll change or type to value
		else if (*raw_query_string == '=') 
		{ 
			isKey = false; 
		}
		// anything else goes into the currently active stream
		else 
		{
			if (isKey) { key << *raw_query_string; }
			else { value << *raw_query_string; }
		}
		// move pointer to next char
		raw_query_string++;
	}
	
	// store last key value pair, because query-strings 
	// normaly don't end on &
	variables[key.str()] = value.str();
	
	return variables;
}

Das Problem ist, dass die keys und values URL-codiert sind, wenn man also Text verarbeiten möchte muss man sie noch entsprechend dekodieren. Das ist aber an dieser Stelle erstmal Aufgabe des Lesers 😉

Natürlich möchte man jetzt nicht nur GET-Variablen lesen, sondern auch Daten empfangen können, die per POST geschickt werden. Diese schickt der WebServer an stdin des CGI-Scripts. Und leider nicht -terminiert, aber das ist kein Problem, da die Umgebungsvariable CONTENT_LENGTH die genaue Länge der POST-Daten enthält. Die POST Daten sind in gleichen Format wie die GET-Daten. Das Parsen funktioniert also entsprechend ähnlich, allerdings muss man erst beispielsweise per fread die Daten aus dem stdin in ein char-buffer lesen und -terminieren bevor man sie durch entsprechende parsing Funktion schicken kann.

Mit diesen zwei Funktionalitäten kann man schon eine Menge anstellen (wie zB ein Datei-basiertes Gästebuch), allerdings muss man beim Ausgeben von Benutzereingaben daran denken diese Entsprechend zu Maskieren, damit man keine XSS-Attacken und Ähnliches erlaubt. Dafür habe ich mir folgende kleine Hilfsfunktion geschrieben:

std::string htmlspecialchars(std::string input)
{
	replaceAll("&", "&", input);

	replaceAll("", ">", input);

	replaceAll("'", "'", input);
	replaceAll(""", """, input);

	return input;
}

void replaceAll(std::string search, std::string replace, std::string &str)
{
	size_t currPos = 0;
	size_t findPos;
	while((findPos = str.find(search, currPos)) != std::string::npos) 
	{
		str.replace(findPos, search.size(), replace);
		currPos = findPos + replace.size();
	}
}

Jetzt wird’s entlich interessant: Wir möchten in unserer C++ CGI-Anwendung natürlich auch (wie gewohnt aus PHP) eine mySQL-Datenbank ansprechen. Ich habe zuerst ziemlich lange mit dem MySQL Connector/C++ rumprobiert, hab das ganze allerdings nicht wirklich vernünftig zum Laufen bekommen (wenn Details gewünscht sind, kann ich gerne noch einen weiteren Blogpost dazu schreiben :-D). Viele Probleme die bei mir aufgetreten sind waren wohl nicht unbekannt und sind (teilweise) sogar ohne Lösung auf StackOverflow zu finden. Dort bin ich dann auch auf die Idee gekommen einfach die MySQL C-API zu nehmen. Gesagt getan, ich hab mir einen kleinen Wrapper dafür geschrieben und ab dort konnte ich ohne Probleme über C++ mit meiner mySQL Datenbank reden. Um die C-API zu benutzen, muss man einfach von der MySQL Homepage den „C-Connector“ runterladen, das include-Directory im Compiler mit einbinden, das lib-Directory dem Linker geben und mit der libmysql linken. Dann kann man die Funktionen per #include „mysql.h“ laden und verwenden. Je nach Projekt kann es nötig sein noch die SOCKET-Header (unter windows winsock2.h, unter linux sys/socket.h und arpa/inet.h) einzubinden.

Kleines Beispiel-Programm:

#include 

#include "mysql.h"

using namespace std;

int main()
{
	cout << "Content-Type: text/html" << endl << endl;
	
	cout << "nmySQL TEST" << endl;
	
	MYSQL *conn = mysql_init(NULL);
	
	if (mysql_real_connect(conn, "localhost", "root", "", "testdatabase", NULL, 0, NULL) == NULL)
	{
		printf("mySQL Error #%u: %s 
n", mysql_errno(conn), mysql_error(conn)); return 0; } if (mysql_real_query(conn, "SELECT name, age FROM clients") != 0) { printf("mySQL Error #%u: %s
n", mysql_errno(conn), mysql_error(conn)); return 0; } cout << "

Client age table:

" << endl; MYSQL_RES *result = mysql_store_result(conn); MYSQL_ROW row; while (row = mysql_fetch_row(result)) { printf("n", row[0], row[1]); } mysql_free_result(result); mysql_close(conn); cout << "
Name age
%s%d
" << endl; return 0; }

Da ich ja am TWLan-Projekt mitarbeite (www.twlan.org) habe ich als Test mal die Startseite und die Registration in C++ implementiert. Dafür habe ich noch eine kleine C++ HTML Template Klasse geschrieben, ist allerdings nichts besonderes. Nun habe ich die PHP-Startseite und die C++ CGI-Startseite mal (ohne Caching) gegeneinander im Punkto Geschwindigkeit gegenübergestellt:

PHP-TWLan: 0.2501s 0.0743s 0.0771s 0.0693s 0.0672s 0.0635s 0.0775s
AVG:	   0.097s

CGI-TWLan: 0.0220s 0.0321s 0.0142s 0.0354s 0.0199s 0.0194s 0.0195s
AVG:	   0.023s

Die CGI-Version ist, selbst bei nur 2-Datenbank-Abfragen, 4x schneller als der PHP-Code. Der sehr hohe Wert bei der ersten PHP-Zeitmessung erklärt sich übrigens dadurch, dass die Zend-Engine beim ersten durchlauf den PHP-Opcode cached. Ein solches Ergebnis war zu erwarten – und wenn man es dann mal selbst erlebt ist es doch sehr motivierend weitere Web-Anwendungen in C++ zu schreiben. Wenn man dann noch FastCGI unterstützt, und zB lighttpd als Webserver verwendet, dann hat man wirklich die ultimativ-schnelle Web-Anwendung 🙂

Das „TWLanCGI“ Projekt gibts im TWLan-Forum übrigens für Windows zum Download (hier).