Browse Source

First Commit

Nathaniel van Diepen 7 years ago
commit
1e4924a715

+ 22 - 0
.gitattributes

@@ -0,0 +1,22 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Custom for Visual Studio
+*.cs     diff=csharp
+*.sln    merge=union
+*.csproj merge=union
+*.vbproj merge=union
+*.fsproj merge=union
+*.dbproj merge=union
+
+# Standard to msysgit
+*.doc	 diff=astextplain
+*.DOC	 diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot  diff=astextplain
+*.DOT  diff=astextplain
+*.pdf  diff=astextplain
+*.PDF	 diff=astextplain
+*.rtf	 diff=astextplain
+*.RTF	 diff=astextplain

+ 217 - 0
.gitignore

@@ -0,0 +1,217 @@
+#################
+## Eclipse
+#################
+
+*.pydevproject
+.project
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.classpath
+.settings/
+.loadpath
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# CDT-specific
+.cproject
+
+# PDT-specific
+.buildpath
+
+
+#################
+## Visual Studio
+#################
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+
+[Dd]ebug/
+[Rr]elease/
+x64/
+build/
+[Bb]in/
+[Oo]bj/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+*_i.c
+*_p.c
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.log
+*.scc
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+*.ncrunch*
+.*crunch*.local.xml
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.Publish.xml
+*.pubxml
+
+# NuGet Packages Directory
+## TODO: If you have NuGet Package Restore enabled, uncomment the next line
+#packages/
+
+# Windows Azure Build Output
+csx
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+sql/
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.[Pp]ublish.xml
+*.pfx
+*.publishsettings
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file to a newer
+# Visual Studio version. Backup files are not needed, because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+App_Data/*.mdf
+App_Data/*.ldf
+
+#############
+## Windows detritus
+#############
+
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Mac crap
+.DS_Store
+
+
+#############
+## Python
+#############
+
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist/
+build/
+eggs/
+parts/
+var/
+sdist/
+develop-eggs/
+.installed.cfg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg
+config.example.php
+site/favicon.ico

+ 7 - 0
config.php

@@ -0,0 +1,7 @@
+<?php
+	define('MYSQL_SERVER','localhost');
+	define('MYSQL_USER','ircd');
+	define('MYSQL_PASSWORD','ircd');
+	define('MYSQL_DATABASE','ircd');
+	define('HOSTNAME','//localhost/');
+?>

+ 46 - 0
header.php

@@ -0,0 +1,46 @@
+<?php
+	error_reporting(E_ALL);
+	ini_set("display_errors", 1);
+	session_start();
+	define('DIR',dirname(__FILE__));
+	require_once(DIR.'/config.php');
+	require_once(DIR."/lib/irc.php");
+	require_once(DIR."/lib/security.php");
+	require_once(DIR."/lib/users.php");
+	require_once(DIR."/lib/servers.php");
+	require_once(DIR."/lib/opers.php");
+	require_once(DIR."/lib/forms.php");
+	require_once(DIR."/lib/configuration.php");
+	if(!empty($_SERVER['HTTP_CLIENT_IP'])){
+		$ip = $_SERVER['HTTP_CLIENT_IP'];
+	}elseif(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+		$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
+	}elseif(!empty($_SERVER['REMOTE_ADDR'])){
+		$ip = $_SERVER['REMOTE_ADDR'];
+	}else{
+		$ip = '';
+	}
+	define('USER_IP',$ip);
+	function get_sql(){
+		static $sql;
+		if(!$sql){
+			$sql = new mysqli(MYSQL_SERVER,MYSQL_USER,MYSQL_PASSWORD,MYSQL_DATABASE);
+			if ($sql->connect_errno) {
+				echo "Failed to connect to MySQL: (" . $sql->connect_errno . ") " . $sql->connect_error;
+				die();
+			}
+		}
+		return $sql;
+	}
+	function query($query,$args=Array()){
+		$sql = get_sql();
+		for ($i=0;$i<count($args);$i++){
+			if(is_string($args[$i])){
+				$args[$i] = $sql->real_escape_string($args[$i]);
+			}elseif(!is_numeric($args[$i])){
+				return false;
+			}
+		}
+		return $sql->query(vsprintf($query,$args));
+	}
+?>

+ 343 - 0
index.php

@@ -0,0 +1,343 @@
+<?php
+	header('Content-type: text/plain');
+	require_once('header.php');
+	if(!isset($_GET['user']) || !isset($_GET['key']) || !isset($_GET['server'])){
+		$opts = getopt('u:k:s:',Array('user:','key:','server:'));
+		$_GET['user'] = isset($opts['user'])?$opts['user']:(isset($opts['u'])?$opts['u']:false);
+		$_GET['key'] = isset($opts['key'])?$opts['key']:(isset($opts['k'])?$opts['k']:false);
+		$_GET['server'] = isset($opts['server'])?$opts['server']:(isset($opts['s'])?$opts['s']:false);
+		if(!$_GET['user'] || !$_GET['key'] || !$_GET['server']){
+			die('# Please provide a user, api key and a server name.');
+		}
+	}
+	$user = get_current_user_obj('netadmin') or $user = get_current_user_obj('servermanager') or $user = get_current_user_obj('globaladmin') or die('# Invalid user/key pair.');
+	$server = get_current_server_obj() or die('# Invalid server name');;
+	$opers = get_opers_for_server_obj($server['id']);
+	$pass = mkpasswd(get_conf('server-pass'));
+?>
+#################################################
+##                   Classes                   ##
+#################################################
+class		clients
+{
+	pingfreq 120;
+	maxclients 500;
+	sendq 100000;
+	recvq 8000;
+};
+class		servers
+{
+	pingfreq 120;
+	maxclients 11;
+	sendq 1000000;
+	connfreq 100;
+};
+#################################################
+##                     Me                      ##
+#################################################
+me {
+	name "<?php echo $server['host'];?>";
+	info "<?php echo $server['description'];?>";
+	numeric <?php echo $server['id'];?>;
+};
+#################################################
+##                   Admin                     ##
+#################################################
+admin {
+	"<?php echo $user['real_name'];?>";
+	"<?php echo $user['nick'];?>";
+	"<?php echo $user['email'];?>";
+};
+#################################################
+##                 Listeners                   ##
+#################################################
+listen         *:6697
+{
+	options
+	{
+		ssl;
+		clientsonly;
+	};
+};
+listen         *:8067;
+listen         *:6667;
+listen         *:6666;
+listen         *:6665;
+listen         *:7150
+{
+	options
+	{
+		serversonly;
+	};
+};
+listen         *:7100
+{
+	options
+	{
+		ssl;
+		serversonly;
+	};
+};
+#################################################
+##                   Link                      ##
+#################################################
+<?php
+	$ulines = get_ulines_obj();
+	foreach($ulines as $k => $u){?>
+link        <?php echo $u['host'];?> {
+	username *;
+	hostname *;
+	bind-ip *;
+	hub *;
+	port 7150;
+	password-receive "<?php echo $pass ?>" { sha1; };
+	password-connect "<?php echo get_conf('server-pass'); ?>";
+	class servers;
+};
+<?php
+	}
+	if(!is_null($server['parent'])){?>
+link        <?php echo $server['parent']['host'];?> {
+	username *;
+	hostname <?php echo $server['parent']['ip'];?>;
+	bind-ip *;
+	hub *;
+	port 7100;
+	password-receive "<?php echo $pass ?>" { sha1; };
+	password-connect "<?php echo get_conf('server-pass'); ?>";
+	class       servers;
+	options
+	{
+		zip;
+		ssl;
+		autoconnect;
+		nodnscache;
+		nohostcheck;
+	};
+};
+<?php
+	}
+	if(isset($server['children'])){
+		foreach($server['children'] as $k => $c){?>
+link        <?php echo $c['host'];?> {
+	username *;
+	hostname <?php echo $c['ip'];?>;
+	bind-ip *;
+	hub *;
+	port 7100;
+	password-receive "<?php echo $pass ?>" { sha1; };
+	password-connect "<?php echo get_conf('server-pass'); ?>";
+	class       servers;
+	options
+	{
+		zip;
+		ssl;
+		autoconnect;
+		nodnscache;
+		nohostcheck;
+	};
+};
+<?php		}
+	}
+?>
+ulines {
+	<?php
+		$ulines = get_ulines();
+		foreach($ulines as $k => $uline){
+			echo $uline.";\n";
+			if($k < count($ulines)-1){
+				echo "\t";
+			}
+		}
+	?>
+};
+#################################################
+##                   Log                       ##
+#################################################
+log "ircd.log" {
+	flags {
+		oper;
+		kline;
+		connects;
+		server-connects;
+		kills;
+		errors;
+		sadmin-commands;
+		chg-commands;
+		oper-override;
+		spamfilter;
+	};
+};
+#################################################
+##                   Alias                     ##
+#################################################
+alias "glinebot" {
+	format ".+" {
+		command "gline";
+		type real;
+		parameters "%1 2d Bots are not allowed on this server, please read the faq at http://www.example.com/faq/123";
+	};
+	type command;
+};
+alias statserv { type stats; };
+alias ss { target statserv; type stats; };
+#################################################
+##                   DRPass                    ##
+#################################################
+drpass {
+	restart "<?php echo $pass ?>" { sha1; };
+	die "<?php echo $pass ?>" { sha1; };
+};
+#################################################
+##             Network Settings                ##
+#################################################
+set {
+	network-name 		"omnimaga.org";
+	default-server 		"irc.omnimaga.org";
+	services-server 	"<?php echo get_conf('services-server','string'); ?>";
+	stats-server		"<?php echo get_conf('stats-server','string'); ?>";
+	help-channel 		"#omnimaga";
+	hiddenhost-prefix	"omni";
+	cloak-keys {
+		"XFGasdgREWhgreTG43FDSfweqfew";
+		"FDSAyh5ghREFadhrGHrewGQEg324";
+		"ASGfdah4431fgdsagdsagASgrw32";
+	};
+	hosts {
+		local			"local.users.irc.omnimaga.org";
+		global			"global.users.irc.omnimaga.org";
+		coadmin			"coadmin.users.irc.omnimaga.org";
+		admin			"admin.users.irc.omnimaga.org";
+		servicesadmin	"servicesadmin.users.irc.omnimaga.org";
+		netadmin 		"netadmin.users.irc.omnimaga.org";
+		host-on-oper-up "yes";
+	};
+	modes-on-join		"+nt";
+	kline-address "[email protected]";
+	modes-on-connect "+G";
+	modes-on-oper	 "+wgs";
+	oper-auto-join "<?php echo get_conf('ops-channel','string'); ?>";
+	options {
+		hide-ulines;
+		show-connect-info;
+	};
+	maxchannelsperuser 50;
+	anti-spam-quit-message-time 10s;
+	oper-only-stats "okfGsMRUEelLCXzdD";
+	throttle {
+		connections 3;
+		period 60s;
+	};
+	anti-flood {
+		nick-flood 3:60;
+	};
+	spamfilter {
+		ban-time 1d;
+		ban-reason "Spam/Advertising";
+		virus-help-channel "#help";
+	};
+};
+#################################################
+##                Enable Mibbit                ##
+#################################################
+// Datacenter one:
+cgiirc {
+	type webirc;
+	hostname 64.62.228.82;
+	password <?php echo get_conf('mibbit-password','string'); ?>;
+};
+// Datacenter two:
+cgiirc {
+	type webirc;
+	hostname 207.192.75.252;
+	password <?php echo get_conf('mibbit-password','string'); ?>;
+};
+// Datacenter three:
+cgiirc {
+	type webirc;
+	hostname 78.129.202.38;
+	password <?php echo get_conf('mibbit-password','string'); ?>;
+};
+// Datacenter four:
+cgiirc {
+	type webirc;
+	hostname 109.169.29.95;
+	password <?php echo get_conf('mibbit-password','string'); ?>;
+};
+#################################################
+##                    Allow                    ##
+#################################################
+allow {
+	ip             *@*;
+	hostname       *@*;
+	class           clients;
+	maxperip	10;
+};
+#################################################
+##                     Deny                    ##
+#################################################
+deny dcc {
+	filename "*sub7*";
+	reason "Possible Sub7 Virus";
+};
+#################################################
+##                    Bans                     ##
+#################################################
+ban nick {
+	mask "*C*h*a*n*S*e*r*v*";
+	reason "Reserved for Services";
+};
+#################################################
+##                Localization                 ##
+#################################################
+files {
+	motd "motd/en.txt";
+	rules "rules/en.txt";
+};
+tld {
+	mask *@*.ca;
+	motd "motd/en_CA.txt";
+	rules "rules/en_CA.txt";
+};
+tld {
+	mask *@*.com;
+	motd "motd/en_US.txt";
+	rules "rules/en_US.txt";
+};
+tld {
+	mask *@*.fr;
+	motd "motd/fr.txt";
+	rules "rules/fr.txt";
+};
+#################################################
+##                    Opers                    ##
+#################################################
+oper RehashServ {
+	class		clients;
+	from {
+		userhost [email protected];
+		userhost [email protected];
+		userhost [email protected];
+	};
+	password "<?php echo mkpasswd(get_conf('rehash-pass')); ?>" { sha1; };
+	flags {
+		can_rehash;
+		netadmin;
+	};
+};
+
+<?php foreach($opers as $k => $oper){?>
+oper <?php echo $oper['nick'];?> {
+	class		clients;
+	from {
+		<?php foreach($oper['hosts'] as $k => $host){?>
+			userhost <?php echo $host;?>;
+		<?php } ?>
+	};
+	password "<?php echo $oper['password'];?>" { <?php echo $oper['password_type'];?>; };
+	flags {
+		<?php echo $oper['flags'];?>
+	};
+	swhois "<?php echo $oper['swhois'];?>";
+};
+<?php }?>

+ 525 - 0
ircd.sql

@@ -0,0 +1,525 @@
+-- phpMyAdmin SQL Dump
+-- version 4.0.4.1
+-- http://www.phpmyadmin.net
+--
+-- Host: 127.0.0.1
+-- Generation Time: Feb 20, 2014 at 02:10 AM
+-- Server version: 5.6.11
+-- PHP Version: 5.5.3
+
+SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
+SET AUTOCOMMIT = 0;
+START TRANSACTION;
+SET time_zone = "+00:00";
+
+
+/*!40101 SET @[email protected]@CHARACTER_SET_CLIENT */;
+/*!40101 SET @[email protected]@CHARACTER_SET_RESULTS */;
+/*!40101 SET @[email protected]@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8 */;
+
+--
+-- Database: `ircd`
+--
+CREATE DATABASE IF NOT EXISTS `ircd` DEFAULT CHARACTER SET latin1 COLLATE latin1_general_ci;
+USE `ircd`;
+
+-- --------------------------------------------------------
+
+--
+-- Stand-in structure for view `children_v`
+--
+DROP VIEW IF EXISTS `children_v`;
+CREATE TABLE IF NOT EXISTS `children_v` (
+`user_id` int(100)
+,`parent_id` int(100)
+,`child_id` int(100)
+);
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `configuration`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `configuration`;
+CREATE TABLE IF NOT EXISTS `configuration` (
+  `key` varchar(100) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
+  `description` varchar(100) NOT NULL,
+  `value` varchar(4000) DEFAULT NULL,
+  `type` varchar(100) NOT NULL,
+  PRIMARY KEY (`key`),
+  UNIQUE KEY `key` (`key`),
+  KEY `key_2` (`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+--
+-- Dumping data for table `configuration`
+--
+
+INSERT INTO `configuration` (`key`, `description`, `value`, `type`) VALUES
+('2-factor-method', '2-Factor Method', 'none', 'list'),
+('authy-api-key', 'Authy API Key', NULL, 'string'),
+('authy-endpoint', 'Authy Endpoint', 'http://sandbox-api.authy.com', 'list'),
+('irc-port', 'Main Server Port', '6667', 'number'),
+('irc-server', 'Main Server IP', NULL, 'lookup'),
+('mibbit-password', 'Mibbit Password', NULL, 'string'),
+('ops-channel', 'Opers Channel', '#opers', 'string'),
+('persona-audience', 'Persona Audience', NULL, 'string'),
+('persona-endpoint', 'Persona Endpoint', 'none', 'list'),
+('rehash-pass', 'RehashServ Password', NULL, 'string'),
+('server-pass', 'Server-to-Server Password', NULL, 'string'),
+('services-server', 'Services Server', NULL, 'lookup'),
+('stats-server', 'Stats Server', NULL, 'lookup'),
+('xmlrpc-path', 'XMLRPC Path', '/xmlrpc', 'string'),
+('xmlrpc-port', 'XMLRPC Port', '9900', 'number'),
+('xmlrpc-server', 'XMLRPC Server', NULL, 'lookup');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `configuration_lists`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `configuration_lists`;
+CREATE TABLE IF NOT EXISTS `configuration_lists` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `key` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  `value` varchar(4000) COLLATE latin1_general_ci NOT NULL,
+  `label` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `key` (`key`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci AUTO_INCREMENT=8 ;
+
+--
+-- RELATIONS FOR TABLE `configuration_lists`:
+--   `key`
+--       `configuration` -> `key`
+--
+
+--
+-- Dumping data for table `configuration_lists`
+--
+
+INSERT INTO `configuration_lists` (`id`, `key`, `value`, `label`) VALUES
+(1, '2-factor-method', 'none', '(none)'),
+(2, '2-factor-method', 'google-authenticator', 'Google Authenticator'),
+(3, '2-factor-method', 'authy', 'Authy'),
+(4, 'authy-endpoint', 'https://api.authy.com', 'Production'),
+(5, 'authy-endpoint', 'http://sandbox-api.authy.com', 'Sandbox'),
+(6, 'persona-endpoint', 'none', '(none)'),
+(7, 'persona-endpoint', 'https://verifier.login.persona.org/verify', 'Production');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `configuration_lookups`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `configuration_lookups`;
+CREATE TABLE IF NOT EXISTS `configuration_lookups` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `key` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  `table` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  `column` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  `label_column` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  `enabled_column` varchar(100) COLLATE latin1_general_ci DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `configuration_key` (`key`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci AUTO_INCREMENT=5 ;
+
+--
+-- RELATIONS FOR TABLE `configuration_lookups`:
+--   `key`
+--       `configuration` -> `key`
+--
+
+--
+-- Dumping data for table `configuration_lookups`
+--
+
+INSERT INTO `configuration_lookups` (`id`, `key`, `table`, `column`, `label_column`, `enabled_column`) VALUES
+(1, 'irc-server', 'servers', 'ip', 'name', '!uline'),
+(2, 'services-server', 'servers', 'host', 'name', 'uline'),
+(3, 'stats-server', 'servers', 'host', 'name', 'uline'),
+(4, 'xmlrpc-server', 'servers', 'ip', 'name', 'uline');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `emails`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `emails`;
+CREATE TABLE IF NOT EXISTS `emails` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `user_id` int(100) NOT NULL,
+  `email` varchar(100) COLLATE latin1_general_ci NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `email` (`email`),
+  KEY `user_id` (`user_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci AUTO_INCREMENT=16 ;
+
+--
+-- RELATIONS FOR TABLE `emails`:
+--   `user_id`
+--       `users` -> `id`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `hosts`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `hosts`;
+CREATE TABLE IF NOT EXISTS `hosts` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `oper_id` int(100) NOT NULL,
+  `host` varchar(100) NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `oper_id` (`oper_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=20 ;
+
+--
+-- RELATIONS FOR TABLE `hosts`:
+--   `oper_id`
+--       `opers` -> `id`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `opers`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `opers`;
+CREATE TABLE IF NOT EXISTS `opers` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `nick` varchar(20) NOT NULL,
+  `role_id` int(11) NOT NULL,
+  `user_id` int(100) NOT NULL,
+  `manager_id` int(100) NOT NULL,
+  `server_id` int(100) DEFAULT NULL,
+  `password` varchar(100) NOT NULL,
+  `password_type_id` int(100) NOT NULL,
+  `swhois` varchar(100) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `id` (`id`),
+  UNIQUE KEY `nick` (`nick`),
+  KEY `id_2` (`id`),
+  KEY `nick_2` (`nick`),
+  KEY `role_id` (`role_id`),
+  KEY `password_type_id` (`password_type_id`),
+  KEY `user_id` (`manager_id`),
+  KEY `server_id` (`server_id`),
+  KEY `user_id_2` (`user_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=9 ;
+
+--
+-- RELATIONS FOR TABLE `opers`:
+--   `password_type_id`
+--       `password_type` -> `id`
+--   `server_id`
+--       `servers` -> `id`
+--   `role_id`
+--       `oper_roles` -> `id`
+--   `manager_id`
+--       `users` -> `id`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Stand-in structure for view `opers_v`
+--
+DROP VIEW IF EXISTS `opers_v`;
+CREATE TABLE IF NOT EXISTS `opers_v` (
+`id` int(100)
+,`user_id` int(100)
+,`manager_id` int(100)
+,`server_id` int(100)
+,`nick` varchar(20)
+,`password` varchar(100)
+,`password_type` varchar(100)
+,`swhois` varchar(100)
+,`flags` varchar(4000)
+,`role` varchar(20)
+);
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `oper_roles`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `oper_roles`;
+CREATE TABLE IF NOT EXISTS `oper_roles` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `name` varchar(20) NOT NULL,
+  `flags` varchar(4000) NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;
+
+--
+-- Dumping data for table `oper_roles`
+--
+
+INSERT INTO `oper_roles` (`id`, `name`, `flags`) VALUES
+(1, 'netadmin', 'netadmin;\ncan_restart;\ncan_die;\ncan_gkline;\ncan_zline;\ncan_gzline;\ncan_override;\ncan_addline;\nget_host;'),
+(2, 'global', 'global;\ncan_override;\ncan_setq;\ncan_addline;\ncan_dccdeny;\nget_host;'),
+(3, 'servicesadmin', 'services-admin;\r\ncan_override;\r\ncan_setq;\r\ncan_addline;\r\ncan_dccdeny;\r\nget_host;');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `password_type`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `password_type`;
+CREATE TABLE IF NOT EXISTS `password_type` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `name` varchar(100) NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;
+
+--
+-- Dumping data for table `password_type`
+--
+
+INSERT INTO `password_type` (`id`, `name`) VALUES
+(1, 'md5'),
+(2, 'sha1');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `servers`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `servers`;
+CREATE TABLE IF NOT EXISTS `servers` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `name` varchar(100) NOT NULL,
+  `host` varchar(100) NOT NULL,
+  `description` varchar(4000) NOT NULL,
+  `parent_id` int(100) DEFAULT NULL,
+  `user_id` int(100) NOT NULL,
+  `ip` varchar(15) NOT NULL,
+  `uline` tinyint(1) NOT NULL DEFAULT '0',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`,`host`),
+  KEY `parent_id` (`parent_id`),
+  KEY `user_id` (`user_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=9 ;
+
+--
+-- RELATIONS FOR TABLE `servers`:
+--   `parent_id`
+--       `servers` -> `id`
+--   `user_id`
+--       `users` -> `id`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Stand-in structure for view `ulines_v`
+--
+DROP VIEW IF EXISTS `ulines_v`;
+CREATE TABLE IF NOT EXISTS `ulines_v` (
+`id` int(100)
+,`host` varchar(100)
+);
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `users`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE IF NOT EXISTS `users` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `api_key` varchar(24) NOT NULL,
+  `secret_key` varchar(100) DEFAULT NULL,
+  `password` varchar(40) NOT NULL,
+  `real_name` varchar(50) NOT NULL,
+  `nick` varchar(20) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
+  `email` varchar(100) NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `api_key` (`api_key`),
+  UNIQUE KEY `nick` (`nick`),
+  KEY `authy_id` (`secret_key`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=8 ;
+
+--
+-- Dumping data for table `users`
+--
+
+INSERT INTO `users` (`id`, `api_key`, `secret_key`, `password`, `real_name`, `nick`, `email`) VALUES
+(7, '1', NULL, '$Dj94pkis$Fs5kyCo4ocTT7zh8asWNJwIelP0=', 'root', 'root', '');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `user_roles`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `user_roles`;
+CREATE TABLE IF NOT EXISTS `user_roles` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `user_id` int(100) NOT NULL,
+  `user_role_id` int(100) NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `user_id` (`user_id`,`user_role_id`),
+  KEY `user_role_id` (`user_role_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=14 ;
+
+--
+-- RELATIONS FOR TABLE `user_roles`:
+--   `user_id`
+--       `users` -> `id`
+--   `user_role_id`
+--       `user_role_types` -> `id`
+--
+
+--
+-- Dumping data for table `user_roles`
+--
+
+INSERT INTO `user_roles` (`id`, `user_id`, `user_role_id`) VALUES
+(13, 7, 4);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `user_role_types`
+--
+-- Creation: Feb 19, 2014 at 10:35 PM
+--
+
+DROP TABLE IF EXISTS `user_role_types`;
+CREATE TABLE IF NOT EXISTS `user_role_types` (
+  `id` int(100) NOT NULL AUTO_INCREMENT,
+  `name` varchar(100) NOT NULL,
+  `description` varchar(100) NOT NULL,
+  `flags` varchar(3) NOT NULL DEFAULT 'o',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`),
+  UNIQUE KEY `description` (`description`)
+) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=5 ;
+
+--
+-- Dumping data for table `user_role_types`
+--
+
+INSERT INTO `user_role_types` (`id`, `name`, `description`, `flags`) VALUES
+(1, 'oper', 'Oper', 'o'),
+(2, 'admin', 'Server Manager', 'n'),
+(3, 'netadmin', 'Network Admin', 'on'),
+(4, 'globaladmin', 'Global Admin', 'nao');
+
+-- --------------------------------------------------------
+
+--
+-- Structure for view `children_v`
+--
+DROP TABLE IF EXISTS `children_v`;
+
+CREATE ALGORITHM=UNDEFINED DEFINER=`eeems`@`localhost` SQL SECURITY DEFINER VIEW `children_v` AS select `p`.`user_id` AS `user_id`,`p`.`id` AS `parent_id`,`c`.`id` AS `child_id` from (`servers` `c` left join `servers` `p` on((`p`.`id` = `c`.`parent_id`))) where ((`p`.`user_id` is not null) and (`c`.`user_id` is not null) and (`c`.`parent_id` is not null) and (`c`.`uline` = 0));
+
+-- --------------------------------------------------------
+
+--
+-- Structure for view `opers_v`
+--
+DROP TABLE IF EXISTS `opers_v`;
+
+CREATE ALGORITHM=UNDEFINED DEFINER=`eeems`@`localhost` SQL SECURITY DEFINER VIEW `opers_v` AS select `o`.`id` AS `id`,`o`.`user_id` AS `user_id`,`o`.`manager_id` AS `manager_id`,`o`.`server_id` AS `server_id`,`o`.`nick` AS `nick`,`o`.`password` AS `password`,`p`.`name` AS `password_type`,`o`.`swhois` AS `swhois`,`r`.`flags` AS `flags`,`r`.`name` AS `role` from ((`opers` `o` join `oper_roles` `r` on((`r`.`id` = `o`.`role_id`))) join `password_type` `p` on((`p`.`id` = `o`.`password_type_id`)));
+
+-- --------------------------------------------------------
+
+--
+-- Structure for view `ulines_v`
+--
+DROP TABLE IF EXISTS `ulines_v`;
+
+CREATE ALGORITHM=UNDEFINED DEFINER=`eeems`@`localhost` SQL SECURITY DEFINER VIEW `ulines_v` AS select `s`.`id` AS `id`,`s`.`host` AS `host` from `servers` `s` where (`s`.`uline` = 1);
+
+--
+-- Constraints for dumped tables
+--
+
+--
+-- Constraints for table `configuration_lists`
+--
+ALTER TABLE `configuration_lists`
+  ADD CONSTRAINT `configuration_lists_ibfk_1` FOREIGN KEY (`key`) REFERENCES `configuration` (`key`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `configuration_lookups`
+--
+ALTER TABLE `configuration_lookups`
+  ADD CONSTRAINT `configuration_lookups_ibfk_1` FOREIGN KEY (`key`) REFERENCES `configuration` (`key`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `emails`
+--
+ALTER TABLE `emails`
+  ADD CONSTRAINT `emails_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `hosts`
+--
+ALTER TABLE `hosts`
+  ADD CONSTRAINT `hosts_ibfk_1` FOREIGN KEY (`oper_id`) REFERENCES `opers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `opers`
+--
+ALTER TABLE `opers`
+  ADD CONSTRAINT `opers_ibfk_2` FOREIGN KEY (`password_type_id`) REFERENCES `password_type` (`id`),
+  ADD CONSTRAINT `opers_ibfk_4` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  ADD CONSTRAINT `opers_ibfk_5` FOREIGN KEY (`role_id`) REFERENCES `oper_roles` (`id`),
+  ADD CONSTRAINT `opers_ibfk_6` FOREIGN KEY (`manager_id`) REFERENCES `users` (`id`);
+
+--
+-- Constraints for table `servers`
+--
+ALTER TABLE `servers`
+  ADD CONSTRAINT `servers_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  ADD CONSTRAINT `servers_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `user_roles`
+--
+ALTER TABLE `user_roles`
+  ADD CONSTRAINT `user_roles_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+  ADD CONSTRAINT `user_roles_ibfk_2` FOREIGN KEY (`user_role_id`) REFERENCES `user_role_types` (`id`) ON UPDATE CASCADE;
+COMMIT;
+
+/*!40101 SET [email protected]_CHARACTER_SET_CLIENT */;
+/*!40101 SET [email protected]_CHARACTER_SET_RESULTS */;
+/*!40101 SET [email protected]_COLLATION_CONNECTION */;

+ 201 - 0
lib/GoogleAuthenticator.php

@@ -0,0 +1,201 @@
+<?php
+
+/**
+ * PHP Class for handling Google Authenticator 2-factor authentication
+ *
+ * @author Michael Kliewe
+ * @copyright 2012 Michael Kliewe
+ * @license http://www.opensource.org/licenses/bsd-license.php BSD License
+ * @link http://www.phpgangsta.de/
+ */
+
+class PHPGangsta_GoogleAuthenticator
+{
+    protected $_codeLength = 6;
+
+    /**
+     * Create new secret.
+     * 16 characters, randomly chosen from the allowed base32 characters.
+     *
+     * @param int $secretLength
+     * @return string
+     */
+    public function createSecret($secretLength = 16)
+    {
+        $validChars = $this->_getBase32LookupTable();
+        unset($validChars[32]);
+
+        $secret = '';
+        for ($i = 0; $i < $secretLength; $i++) {
+            $secret .= $validChars[array_rand($validChars)];
+        }
+        return $secret;
+    }
+
+    /**
+     * Calculate the code, with given secret and point in time
+     *
+     * @param string $secret
+     * @param int|null $timeSlice
+     * @return string
+     */
+    public function getCode($secret, $timeSlice = null)
+    {
+        if ($timeSlice === null) {
+            $timeSlice = floor(time() / 30);
+        }
+
+        $secretkey = $this->_base32Decode($secret);
+
+        // Pack time into binary string
+        $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
+        // Hash it with users secret key
+        $hm = hash_hmac('SHA1', $time, $secretkey, true);
+        // Use last nipple of result as index/offset
+        $offset = ord(substr($hm, -1)) & 0x0F;
+        // grab 4 bytes of the result
+        $hashpart = substr($hm, $offset, 4);
+
+        // Unpak binary value
+        $value = unpack('N', $hashpart);
+        $value = $value[1];
+        // Only 32 bits
+        $value = $value & 0x7FFFFFFF;
+
+        $modulo = pow(10, $this->_codeLength);
+        return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
+    }
+
+    /**
+     * Get QR-Code URL for image, from google charts
+     *
+     * @param string $name
+     * @param string $secret
+     * @return string
+     */
+    public function getQRCodeGoogleUrl($name, $secret) {
+        $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
+        return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.'';
+    }
+
+    /**
+     * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
+     *
+     * @param string $secret
+     * @param string $code
+     * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
+     * @return bool
+     */
+    public function verifyCode($secret, $code, $discrepancy = 1)
+    {
+        $currentTimeSlice = floor(time() / 30);
+
+        for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
+            $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
+            if ($calculatedCode == $code ) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Set the code length, should be >=6
+     *
+     * @param int $length
+     * @return PHPGangsta_GoogleAuthenticator
+     */
+    public function setCodeLength($length)
+    {
+        $this->_codeLength = $length;
+        return $this;
+    }
+
+    /**
+     * Helper class to decode base32
+     *
+     * @param $secret
+     * @return bool|string
+     */
+    protected function _base32Decode($secret)
+    {
+        if (empty($secret)) return '';
+
+        $base32chars = $this->_getBase32LookupTable();
+        $base32charsFlipped = array_flip($base32chars);
+
+        $paddingCharCount = substr_count($secret, $base32chars[32]);
+        $allowedValues = array(6, 4, 3, 1, 0);
+        if (!in_array($paddingCharCount, $allowedValues)) return false;
+        for ($i = 0; $i < 4; $i++){
+            if ($paddingCharCount == $allowedValues[$i] &&
+                substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false;
+        }
+        $secret = str_replace('=','', $secret);
+        $secret = str_split($secret);
+        $binaryString = "";
+        for ($i = 0; $i < count($secret); $i = $i+8) {
+            $x = "";
+            if (!in_array($secret[$i], $base32chars)) return false;
+            for ($j = 0; $j < 8; $j++) {
+                $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
+            }
+            $eightBits = str_split($x, 8);
+            for ($z = 0; $z < count($eightBits); $z++) {
+                $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:"";
+            }
+        }
+        return $binaryString;
+    }
+
+    /**
+     * Helper class to encode base32
+     *
+     * @param string $secret
+     * @param bool $padding
+     * @return string
+     */
+    protected function _base32Encode($secret, $padding = true)
+    {
+        if (empty($secret)) return '';
+
+        $base32chars = $this->_getBase32LookupTable();
+
+        $secret = str_split($secret);
+        $binaryString = "";
+        for ($i = 0; $i < count($secret); $i++) {
+            $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
+        }
+        $fiveBitBinaryArray = str_split($binaryString, 5);
+        $base32 = "";
+        $i = 0;
+        while ($i < count($fiveBitBinaryArray)) {
+            $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)];
+            $i++;
+        }
+        if ($padding && ($x = strlen($binaryString) % 40) != 0) {
+            if ($x == 8) $base32 .= str_repeat($base32chars[32], 6);
+            elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4);
+            elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3);
+            elseif ($x == 32) $base32 .= $base32chars[32];
+        }
+        return $base32;
+    }
+
+    /**
+     * Get array with all 32 characters for decoding from/encoding to base32
+     *
+     * @return array
+     */
+    protected function _getBase32LookupTable()
+    {
+        return array(
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', //  7
+            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
+            'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
+            'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
+            '='  // padding char
+        );
+    }
+}

+ 49 - 0
lib/authy-php/Authy.php

@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * Autoloader
+ *
+ * PHP version 5
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+
+/**
+ * Autoloads Authy API files
+ * Based on https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
+ *
+ * @param string $className class to load
+ *
+ * @return boolean true when the file was loaded
+ */
+function Authy_autoloader($className)
+{
+    $className = ltrim($className, '\\');
+    $baseDir  = __DIR__.'/lib/';
+    $fileName  = '';
+    $namespace = '';
+    if ($lastNsPos = strripos($className, '\\')) {
+        $namespace = substr($className, 0, $lastNsPos);
+        $className = substr($className, $lastNsPos + 1);
+        $fileName  = str_replace('\\', '/', $namespace) . '/';
+    }
+    $fileName .= str_replace('_', '/', $className) . '.php';
+
+    if (file_exists($baseDir.'/'.$fileName)) {
+        include $baseDir.'/'.$fileName;
+        return true;
+    } else if (file_exists($baseDir.'/vendor/'.$fileName)) {
+        include $baseDir.'/vendor/'.$fileName;
+        return true;
+    } else {
+        print("File not found for ". $className .": ".$fileName);
+    }
+
+    return false;
+}
+
+spl_autoload_register('Authy_autoloader');

+ 157 - 0
lib/authy-php/lib/Authy/Api.php

@@ -0,0 +1,157 @@
+<?php
+/**
+ * ApiClient
+ *
+ * PHP version 5
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+
+/**
+ * Authy API interface.
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+class Authy_Api
+{
+    const VERSION = '1.3.0';
+    protected $rest;
+    protected $api_key;
+    protected $api_url;
+
+    /**
+     * Constructor.
+     *
+     * @param string $api_key Api Key
+     * @param string $api_url Optional api url
+     */
+    public function __construct($api_key, $api_url = "https://api.authy.com")
+    {
+        $this->rest = new Resty();
+        $this->rest->setBaseURL($api_url);
+        $this->rest->setUserAgent("authy-php v".Authy_Api::VERSION);
+
+        $this->api_key = $api_key;
+        $this->api_url = $api_url;
+    }
+
+    /**
+     * Register a user.
+     *
+     * @param string $email        New user's email
+     * @param string $cellphone    New user's cellphone
+     * @param string $country_code New user's country code. defaults to USA(1)
+     *
+     * @return Authy_User the new registered user
+     */
+    public function registerUser($email, $cellphone, $country_code = 1)
+    {
+        $params = $this->defaultParams();
+        $params['user'] = array(
+            "email" => $email,
+            "country_code" => $country_code,
+            "cellphone" => $cellphone
+        );
+
+        $resp = $this->rest->post('/protected/json/users/new', $params);
+
+        return new Authy_User($resp);
+    }
+
+    /**
+     * Verify a given token.
+     *
+     * @param string $authy_id User's id stored in your database
+     * @param string $token    The token entered by the user
+     * @param string $opts     Array of options, for example: 
+     *                           array("force" => "true")
+     *
+     * @return Authy_Response the server response
+     */
+    public function verifyToken($authy_id, $token, $opts = array())
+    {
+        $params = array_merge($this->defaultParams(), $opts);
+        if (!array_key_exists("force", $params)) {
+            $params["force"] = "true";
+        }
+        $url = '/protected/json/verify/'. urlencode($token)
+                                        .'/'. urlencode($authy_id);
+        $resp = $this->rest->get($url, $params);
+
+        return new Authy_Response($resp);
+    }
+
+    /**
+     * Request a valid token via SMS.
+     *
+     * @param string $authy_id User's id stored in your database
+     * @param string $opts     Array of options, for example:
+     *                           array("force" => "true")
+     *
+     * @return Authy_Response the server response
+     */
+    public function requestSms($authy_id, $opts = array())
+    {
+        $params = array_merge($this->defaultParams(), $opts);
+        $url = '/protected/json/sms/'.urlencode($authy_id);
+
+        $resp = $this->rest->get($url, $params);
+
+        return new Authy_Response($resp);
+    }
+	
+    /**
+     * Cellphone call, usually used with SMS Token issues or if no smartphone is available.
+	 * This function needs the app to be on Starter Plan (free) or higher.
+     *
+     * @param string $authy_id User's id stored in your database
+     * @param string $opts     Array of options, for example:
+     *                           array("force" => "true")
+     *
+     * @return Authy_Response the server response
+     */
+    public function phoneCall($authy_id, $opts = array())
+    {
+        $params = array_merge($this->defaultParams(), $opts);
+        $url = '/protected/json/call/'.urlencode($authy_id);
+
+        $resp = $this->rest->get($url, $params);
+
+        return new Authy_Response($resp);
+    }
+	
+    /**
+     * Deletes an user.
+     *
+     * @param string $authy_id User's id stored in your database
+     *
+     * @return Authy_Response the server response
+     */	
+    public function deleteUser($authy_id)
+    {
+		$params = array_merge($this->defaultParams());
+        $url = '/protected/json/users/delete/'.urlencode($authy_id);
+
+        $resp = $this->rest->post($url, $params);
+
+        return new Authy_Response($resp);
+    }	
+
+    /**
+     * Return the default parameters.
+     *
+     * @return array array with the default parameters
+     */
+    protected function defaultParams()
+    {
+        return array("api_key" => $this->api_key);
+    }
+};

+ 85 - 0
lib/authy-php/lib/Authy/Response.php

@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * ApiClient
+ *
+ * PHP version 5
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+
+/**
+ * Friendly class to parse response from the authy API
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+class Authy_Response
+{
+    protected $raw_response;
+    protected $body;
+    protected $errors;
+
+    /**
+     * Constructor.
+     *
+     * @param array $raw_response Raw server response
+     */
+    public function __construct($raw_response)
+    {
+        $this->raw_response = $raw_response;
+        $this->body = $raw_response['body'];
+        $this->errors = new stdClass();
+
+        // Handle errors
+        if (isset($this->body->errors)) {
+            $this->errors = $this->body->errors; // when response is {errors: {}}
+            unset($this->body->errors);
+        } else if ($raw_response['status'] == 400) {
+            $this->errors = $this->body; // body here is a stdClass
+            $this->body = new stdClass();
+        } else if (!$this->ok() && gettype($this->body) == 'string') {
+             // the response was an error so put the body as an error
+            $this->errors = (object) array("error" => $this->body);
+            $this->body = new stdClass();
+        }
+    }
+
+    /**
+     * Check if the response was ok
+     *
+     * @return boolean return true if the response code is 200
+     */
+    public function ok()
+    {
+        return $this->raw_response['status'] == 200;
+    }
+
+    /**
+     * Returns the id of the response if present
+     *
+     * @return integer id of the response
+     */
+    public function id()
+    {
+        return isset($this->body->id) ? $this->body->id : null;
+    }
+
+
+    /**
+     * Get the request errors
+     *
+     * @return stdClass object containing the request errors
+     */
+    public function errors()
+    {
+        return $this->errors;
+    }
+}

+ 41 - 0
lib/authy-php/lib/Authy/User.php

@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * ApiClient
+ *
+ * PHP version 5
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+
+/**
+ * User implementation. Extends from Authy_Response
+ *
+ * @category Services
+ * @package  Authy
+ * @author   David Cuadrado <[email protected]>
+ * @license  http://creativecommons.org/licenses/MIT/ MIT
+ * @link     http://authy.github.com/pear
+ */
+class Authy_User extends \Authy_Response
+{
+
+    /**
+     * Constructor.
+     *
+     * @param array $raw_response Raw server response
+     */
+    public function __construct($raw_response)
+    {
+        if (isset($raw_response['body']->user)) {
+            // response is {user: {id: id}}
+            $raw_response['body'] = $raw_response['body']->user;
+        }
+
+        parent::__construct($raw_response);
+    }
+}

+ 846 - 0
lib/authy-php/lib/vendor/Resty.php

@@ -0,0 +1,846 @@
+<?php
+/**
+ * A simple PHP library for doing RESTful HTTP stuff. Does *not* require the curl extension.
+ * @link https://github.com/fictivekin/resty.php
+ */
+class Resty
+{
+
+	/**
+	 * The version of this lib
+	 */
+	const VERSION = '0.3.8';
+
+	const DEFAULT_TIMEOUT = 240;
+
+	/**
+	 * @var bool enables debugging output
+	 */
+	protected $debug = false;
+
+	/**
+	 * logging function (should be a Closure)
+	 * @var Closure
+	 */
+	protected $logger = null;
+
+	/**
+	 * @var bool whether or not to auto-parse the response body as JSON or XML
+	 */
+	protected $parse_body = true;
+
+	/**
+	 * @var string
+	 * @see Resty::getUserAgent()
+	 */
+	protected $user_agent = null;
+
+	/**
+	 * @var string
+	 */
+	protected $base_url;
+
+	/**
+	 * Stores the last request hash
+	 * @var array
+	 */
+	protected $last_request;
+
+	/**
+	 * Stores the last response hash
+	 * @var array
+	 */
+	protected $last_response;
+
+	/**
+	 * stores anon func callbacks (because you can't store them as obj props
+	 * @var array
+	 */
+	protected $callbacks = array();
+
+	/**
+	 * username for basic auth
+	 * @var string
+	 */
+	protected $username;
+
+	/**
+	 * password for basic auth
+	 * @var string
+	 */
+	protected $password;
+
+	/**
+	 * by default, silence the fopen warning if we can't open the stream
+	 */
+	protected $silence_fopen_warning = true;
+
+	/**
+	 * by default, don't raise an exception if fopen() fails
+	 * @var boolean
+	 */
+	protected $raise_fopen_exception = false;
+
+
+	/**
+	 * content-types that will trigger JSON parsing of body
+	 * @var array
+	 */
+	public static $JSON_TYPES = array(
+		'application/json',
+		'text/json',
+		'text/x-json',
+	);
+
+	/**
+	 * content-types that will trigger XML parsing
+	 * @var array
+	 */
+	public static $XML_TYPES = array(
+		'application/xml',
+		'text/xml',
+		'application/rss+xml',
+		'application/xhtml+xml',
+		'application/atom+xml',
+		'application/xslt+xml',
+		'application/mathml+xml',
+	);
+
+
+	/**
+	 * Passed opts can include
+	 * $opts['onRequestLog'] - an anonymous function that takes the Resty::last_request property as arg
+	 * $opts['onResponseLog'] - an anonymous function that takes the Resty::last_response property as arg
+	 *
+	 * @see Resty::last_request
+	 * @see Resty::last_response
+	 * @see Resty::sendRequest()
+	 * @param array $opts OPTIONAL array of options
+	 */
+	function __construct($opts=null) {
+		if (!empty($opts['onRequestLog']) && ($opts['onRequestLog'] instanceof Closure)) {
+			$this->callbacks['onRequestLog'] = $opts['onRequestLog'];
+		}
+		if (!empty($opts['onResponseLog']) && ($opts['onResponseLog'] instanceof Closure)) {
+			$this->callbacks['onResponseLog'] = $opts['onResponseLog'];
+		}
+		if (isset($opts['silence_fopen_warning'])) {
+			$this->silenceFopenWarning((bool)$opts['silence_fopen_warning']);
+		}
+		if (isset($opts['raise_fopen_exception'])) {
+			$this->raiseFopenException((bool)$opts['raise_fopen_exception']);
+		}
+	}
+
+
+	/**
+	 * retrieve the last request we sent
+	 *
+	 * valid keys are ['url', 'method', 'querydata', 'headers', 'options', 'opts']
+	 *
+	 * @param string $key just retrieve a given field from the hash
+	 * @return mixed
+	 */
+	public function getLastRequest($key=null) {
+		if (!isset($key)) {
+			return $this->last_request;
+		}
+
+		return $this->last_request[$key];
+
+	}
+
+	/**
+	 * retrieve the last response we got
+	 *
+	 * valid keys are ['meta', 'status', 'headers', 'body']
+	 *
+	 * @param string $key just retrieve a given field from the hash
+	 * @return mixed
+	 */
+	public function getLastResponse($key=null) {
+		if (!isset($key)) {
+			return $this->last_response;
+		}
+
+		return $this->last_response[$key];
+
+	}
+
+	/**
+	 * make a GET request
+	 *
+	 * @param string the URL. This will be appended to the base_url, if any set
+	 * @param array $querydata hash of key/val pairs
+	 * @param array $headers hash of key/val pairs
+	 * @param array $options hash of key/val pairs ('timeout')
+	 * @return array the response hash
+	 * @see Resty::sendRequest()
+	 */
+	public function get($url, $querydata=null, $headers=null, $options=null) {
+		return $this->sendRequest($url, 'GET', $querydata, $headers, $options);
+	}
+
+	/**
+	 * make a POST request
+	 *
+	 * @param string the URL. This will be appended to the base_url, if any set
+	 * @param array $querydata hash of key/val pairs
+	 * @param array $headers hash of key/val pairs
+	 * @param array $options hash of key/val pairs ('timeout')
+	 * @return array the response hash
+	 * @see Resty::sendRequest()
+	 */
+	public function post($url, $querydata=null, $headers=null, $options=null) {
+		return $this->sendRequest($url, 'POST', $querydata, $headers, $options);
+	}
+
+	/**
+	 * make a PUT request
+	 *
+	 * @param string the URL. This will be appended to the base_url, if any set
+	 * @param array $querydata hash of key/val pairs
+	 * @param array $headers hash of key/val pairs
+	 * @param array $options hash of key/val pairs ('timeout')
+	 * @return array the response hash
+	 * @see Resty::sendRequest()
+	 */
+	public function put($url, $querydata=null, $headers=null, $options=null) {
+		return $this->sendRequest($url, 'PUT', $querydata, $headers, $options);
+	}
+
+	/**
+	 * make a DELETE request
+	 *
+	 * @param string the URL. This will be appended to the base_url, if any set
+	 * @param array $querydata hash of key/val pairs
+	 * @param array $headers hash of key/val pairs
+	 * @param array $options hash of key/val pairs ('timeout')
+	 * @return array the response hash
+	 * @see Resty::sendRequest()
+	 */
+	public function delete($url, $querydata=null, $headers=null, $options=null) {
+		return $this->sendRequest($url, 'DELETE', $querydata, $headers, $options);
+	}
+
+	/**
+	 * @param string $url
+	 * @param array  $files
+	 * @param array  $params
+	 * @param array  $headers
+	 * @param array  $options
+	 *
+	 * The $files array should be a set of key/val pairs, with the key being
+	 * the field name, and the val the file path. ex:
+	 * $files['avatar'] = '/path/to/file.jpg';
+	 * $files['background'] = '/path/to/file2.jpg';
+	 *
+	 */
+	public function postFiles($url, $files, $params=null, $headers=null, $options=null) {
+
+		$datastr = "";
+		$boundary = "---------------------".substr(md5(rand(0,32000)), 0, 10);
+
+		// build params
+		if (isset($params)) {
+			foreach($params as $key => $val) {
+				$datastr .= "--$boundary\n";
+				$datastr .= "Content-Disposition: form-data; name=\"".$key."\"\n\n".$val."\n";
+			}
+		}
+		$datastr .= "--$boundary\n";
+
+		// build files
+		foreach($files as $key => $file)
+		{
+			$filename = pathinfo($file, PATHINFO_BASENAME);
+			$content_type = $this->getMimeType($file);
+			$fileContents = file_get_contents($file);
+
+			$datastr .= "Content-Disposition: form-data; name=\"{$key}\"; filename=\"{$filename}\"\n";
+			$datastr .= "Content-Type: {$content_type}\n";
+			$datastr .= "Content-Transfer-Encoding: binary\n\n";
+			$datastr .= $fileContents."\n";
+			$datastr .= "--$boundary\n";
+		}
+
+		if (!isset($headers)) {
+			$headers = array();
+		}
+		$headers['Content-Type'] = 'multipart/form-data; boundary='.$boundary;
+
+		return $this->post($url, $datastr, $headers, $options);
+	}
+
+
+	/**
+	 * @param string $url
+	 * @param array  $binary_data
+	 * @param array  $params
+	 * @param array  $headers
+	 * @param array  $options
+	 *
+	 * The $binary_data array should be a set of key/val pairs, with the key being
+	 * the field name, and the val the binary data. ex:
+	 * $files['avatar'] = <BINARY>;
+	 * $files['background'] = <BINARY>;
+	 *
+	 * with that data, a multipart POST body is created, identical to a file
+	 * upload, just without reading the data from a file
+	 *
+	 */
+	public function postBinary($url, $binary_data, $params=null, $headers=null, $options=null) {
+
+		$datastr = "";
+		$boundary = "---------------------".substr(md5(rand(0,32000)), 0, 10);
+
+		// build params
+		if (isset($params)) {
+			foreach($params as $key => $val) {
+				$datastr .= "--$boundary\n";
+				$datastr .= "Content-Disposition: form-data; name=\"".$key."\"\n\n".$val."\n";
+			}
+		}
+		$datastr .= "--$boundary\n";
+
+		// build files
+		foreach($binary_data as $key => $bdata)
+		{
+			$filename = 'bdata';
+			$content_type = 'application/octet-stream';
+
+			$datastr .= "Content-Disposition: form-data; name=\"{$key}\"; filename=\"{$filename}\"\n";
+			$datastr .= "Content-Type: {$content_type}\n";
+			$datastr .= "Content-Transfer-Encoding: binary\n\n";
+			$datastr .= $bdata."\n";
+			$datastr .= "--$boundary\n";
+		}
+
+		if (!isset($headers)) {
+			$headers = array();
+		}
+		$headers['Content-Type'] = 'multipart/form-data; boundary='.$boundary;
+
+		return $this->post($url, $datastr, $headers, $options);
+	}
+
+	/**
+	 * @see Resty::postFiles()
+	 *
+	 * Stole this from the Amazon S3 class:
+	 *
+	 * Copyright (c) 2008, Donovan Schönknecht.  All rights reserved.
+	 *
+	 * Redistribution and use in source and binary forms, with or without
+	 * modification, are permitted provided that the following conditions are met:
+	 *
+	 * - Redistributions of source code must retain the above copyright notice,
+	 *   this list of conditions and the following disclaimer.
+	 * - Redistributions in binary form must reproduce the above copyright
+	 *   notice, this list of conditions and the following disclaimer in the
+	 *   documentation and/or other materials provided with the distribution.
+	 *
+	 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+	 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+	 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+	 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+	 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+	 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+	 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+	 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+	 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+	 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+	 * POSSIBILITY OF SUCH DAMAGE.
+	 *
+	 * Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates.
+	 *
+	 */
+	protected function getMimeType($filepath) {
+
+		if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
+			($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) {
+
+			if (($type = finfo_file($finfo, $filepath)) !== false) {
+				// Remove the charset and grab the last content-type
+				$type = explode(' ', str_replace('; charset=', ';charset=', $type));
+				$type = array_pop($type);
+				$type = explode(';', $type);
+				$type = trim(array_shift($type));
+			}
+			finfo_close($finfo);
+
+		// If anyone is still using mime_content_type()
+		} elseif (function_exists('mime_content_type')) {
+			$type = trim(mime_content_type($filepath));
+		}
+
+		if ($type !== false && strlen($type) > 0) { return $type; }
+
+		// Otherwise do it the old fashioned way
+		static $exts = array(
+			'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png',
+			'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon',
+			'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf',
+			'zip' => 'application/zip', 'gz' => 'application/x-gzip',
+			'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
+			'bz2' => 'application/x-bzip2', 'txt' => 'text/plain',
+			'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
+			'css' => 'text/css', 'js' => 'text/javascript',
+			'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
+			'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
+			'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
+			'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php'
+		);
+		$ext = strtolower(pathInfo($filepath, PATHINFO_EXTENSION));
+
+		return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream';
+	}
+
+	/**
+	 * bc wrapper
+	 */
+	public function enableDebugging($state=false) {
+		$this->debug($state);
+	}
+
+	/**
+	 * enable or disable debugging. If no arg passed, just returns current state
+	 * @param bool $state=null if not passed, state not changed
+	 * @return boolean the current state
+	 */
+	public function debug($state=null) {
+		if (isset($state)) {
+			$this->debug = (bool)$state;
+		}
+		return $this->debug;
+	}
+
+	/**
+	 * raise an exception from fopen if trying to open stream fails
+	 * @param  boolean $state=null optional, set the state
+	 * @return boolean the current state
+	 */
+	public function raiseFopenException($state=null) {
+		if (isset($state)) {
+			$this->raise_fopen_exception = (bool)$state;
+		}
+		return $this->raise_fopen_exception;
+	}
+
+	/**
+	 * silence warnings from fopen when trying to open stream
+	 * @param  boolean $state=null optional, set the state
+	 * @return boolean the current state
+	 */
+	public function silenceFopenWarning($state=null) {
+		if (isset($state)) {
+			$this->silence_fopen_warning = (bool)$state;
+		}
+		return $this->silence_fopen_warning;
+	}
+
+	/**
+	 * sets an alternate logging method
+	 * @param Closure $logger
+	 */
+	public function setLogger(Closure $logger) {
+		$this->logger = $logger;
+	}
+
+	/**
+	 * enable or disable automatic parsing of body. default is true
+	 * @param bool $state default TRUE
+	 */
+	public function parseBody($state=true) {
+		$state = (bool)$state;
+		$this->parse_body = $state;
+	}
+
+	/**
+	 * Sets the base URL for all subsequent requests
+	 * @param string $base_url
+	 */
+	public function setBaseURL($base_url) {
+		$this->base_url = $base_url;
+	}
+
+	/**
+	 * retrieves the current Resty::$base_url
+	 * @return string
+	 */
+	public function getBaseURL() {
+		return $this->base_url;
+	}
+
+	/**
+	 * Sets the user-agent
+	 * @param string $user_agent
+	 */
+	public function setUserAgent($user_agent) {
+		$this->user_agent = $user_agent;
+	}
+
+	/**
+	 * Gets the current user agent. if Resty::$user_agent is not set, uses a default
+	 * @return string
+	 */
+	public function getUserAgent() {
+		if (empty($this->user_agent)) {
+			$this->user_agent = 'Resty ' . static::VERSION;
+		}
+		return $this->user_agent;
+	}
+
+	/**
+	 * Sets credentials for http basic auth
+	 * @param string $username
+	 * @param string $password
+	 */
+	public function setCredentials($username, $password) {
+		$this->username = $username;
+		$this->password = $password;
+	}
+
+	/**
+	 * removes current credentials
+	 */
+	public function clearCredentials() {
+		$this->username = null;
+		$this->password = null;
+	}
+
+	/**
+	 * takes a set of key/val pairs and builds an array of raw header strings
+	 *
+	 * @param string $headers
+	 * @return void
+	 * @author Ed Finkler
+	 */
+	protected function buildHeadersArray($headers) {
+		$str_headers = array();
+		foreach ($headers as $key => $value) {
+			$str_headers[] = "{$key}: {$value}";
+		}
+		return $str_headers;
+	}
+
+	/**
+	 * Extracts the headers of a response from the stream's meta data
+	 * @param array $meta
+	 * @return array
+	 */
+	protected function metaToHeaders($meta) {
+		$headers = array();
+
+		if (!isset($meta['wrapper_data'])) {
+			return $headers;
+		}
+
+		foreach ($meta['wrapper_data'] as $value) {
+			if (strpos($value, 'HTTP') !== 0) {
+				preg_match("|^([^:]+):\s?(.+)$|", $value, $matches);
+				if (is_array($matches) && isset($matches[2])) {
+					$headers[trim($matches[1])] = trim($matches[2], " \t\n\r\0\x0B\"");
+				}
+			}
+		}
+		return $headers;
+	}
+
+	/**
+	 * extracts the status code from the stream meta data
+	 * @param array $meta
+	 * @return integer
+	 */
+	protected function getStatusCode($meta) {
+		$matches = array();
+		$status = 0;
+		preg_match("|\s(\d\d\d)\s?|", $meta['wrapper_data'][0], $matches);
+		if (is_array($matches) && isset($matches[1])) {
+			$status = (int)trim($matches[1]);
+		}
+		return $status;
+	}
+
+	/**
+	 * Sends the HTTP request and retrieves/parses the response
+	 *
+	 * @param string $url
+	 * @param string $method
+	 * @param array $querydata OPTIONAL
+	 * @param array $headers OPTIONAL
+	 * @param array $options OPTIONAL
+	 * @return array
+	 * @author Ed Finkler
+	 */
+	public function sendRequest($url, $method='GET', $querydata=null, $headers=null, $options=null) {
+		$resp = array();
+
+		if ($this->base_url) {
+			$url = $this->base_url.$url;
+		}
+
+		// we need to supply a default content-type
+		if (!isset($headers['Content-Type'])) {
+			$headers['Content-Type'] = 'application/x-www-form-urlencoded';
+		}
+
+		// by default, pass the header "Connection: close"
+		if (!isset($headers['Connection'])) {
+			$headers['Connection'] = 'close';
+		}
+
+		// if we have a username and password, use it
+		if (isset($this->username) && isset($this->password) && !isset($headers['Authorization'])) {
+			$this->log("{$this->username}:{$this->password}");
+			$headers['Authorization'] = 'Basic '.base64_encode("{$this->username}:{$this->password}");
+		}
+
+		// default timeout
+		$timeout = isset($options['timeout']) ? $options['timeout'] : static::DEFAULT_TIMEOUT;
+
+		$content = null;
+
+		// if querydata is a string, just pass it as-is
+		if (isset($querydata) && is_string($querydata)) {
+
+			$content = $querydata;
+
+		// else if it's an array, make an http query
+		} elseif (isset($querydata) && is_array($querydata)) {
+
+			$content = http_build_query($querydata);
+
+		}
+
+		// create an array of header strings from the hash
+		$headerarr = isset($headers) ? $this->buildHeadersArray($headers) : array();
+
+		// GET and DELETE should use the URL to pass data
+		$urlcontent = ('GET' === $method || 'DELETE' === $method);
+
+		// if this is a GET or DELETE and we have some $content, append to URL
+		if ($urlcontent && isset($content)) {
+			$url .= '?'.$content;
+		}
+
+
+		$opts = array(
+			'http'=>array(
+				'timeout'=>$timeout,
+				'method'=>$method,
+				'content'=> (!$urlcontent) ? $content : null,
+				'user_agent'=>$this->getUserAgent(),
+				'header'=>$headerarr,
+				'ignore_errors'=>1
+			)
+		);
+
+		$this->log('URL =================');
+		$this->log($url);
+
+		$this->log('METHOD =================');
+		$this->log($method);
+
+		$this->log('QUERYDATA =================');
+		$this->log($querydata);
+
+		$this->log('HEADERS =================');
+		$this->log($headers);
+
+		$this->log('OPTIONS =================');
+		$this->log($options);
+
+		$this->log('OPTS =================');
+		$this->log($opts);
+
+		$this->last_request = compact('url', 'method', 'querydata', 'headers', 'options', 'opts');
+		// call custom req log callback
+		if (!empty($this->callbacks['onRequestLog'])) {
+			$this->callbacks['onRequestLog']($this->last_request);
+		}
+
+
+		$resp_data = $this->makeStreamRequest($url, $opts);
+
+		$resp['meta'] = $resp_data['meta'];
+		$resp['body'] = $resp_data['body'];
+		$resp['error'] = $resp_data['error'];
+		$resp['error_msg'] = $resp_data['error_msg'];
+		$resp['status'] = $this->getStatusCode($resp['meta']);
+		$resp['headers'] = $this->metaToHeaders($resp['meta']);
+		$this->log($resp);
+
+		$this->log("Processing response body…");
+		$resp = $this->processResponseBody($resp);
+		$this->log($resp['body']);
+
+		$this->last_response = $resp;
+
+		// call custom resp log callback
+		if (!empty($this->callbacks['onResponseLog'])) {
+			$this->callbacks['onResponseLog']($this->last_response);
+		}
+
+		return $resp;
+	}
+
+
+	/**
+	 * opens an http stream, sends the request, and returns result
+	 * @param  [type] $url  [description]
+	 * @param  [type] $opts [description]
+	 * @return [type]       [description]
+	 */
+	protected function makeStreamRequest($url, $opts) {
+
+		$resp_data = array(
+			'meta' => null,
+			'body' => null,
+			'error' => true,
+			'error_msg' => null,
+		);
+
+		$context = stream_context_create($opts);
+
+		$this->log("Sending…");
+		$start_time = microtime(true);
+
+		$this->log("Opening stream…");
+		if ($this->silence_fopen_warning) {
+			$stream = @fopen($url, 'r', false, $context);
+		} else {
+			$stream = fopen($url, 'r', false, $context);
+		}
+
+		if (!$stream) {
+
+			$req_time = static::calc_time_passed($start_time);
+			$opts_json = !empty($opts) ? json_encode($opts) : 'null';
+			$msg = "Stream open failed for '{$url}'; req_time: {$req_time}; opts: {$opts_json}";
+			$this->log($msg);
+
+			if ($this->raise_fopen_exception) {
+				throw new Exception($msg);
+			} else {
+				$resp_data['error'] = true;
+				$resp_data['error_msg'] = $msg;
+			}
+
+		} else {
+
+			$this->log("Getting metadata…");
+			$resp_data['meta'] = stream_get_meta_data($stream);
+
+			$this->log("Getting response…");
+			$resp_data['body'] = stream_get_contents($stream);
+
+			$this->log("Closing stream…");
+			fclose($stream);
+
+		}
+
+
+		if ($this->debug) {
+			$req_time = static::calc_time_passed($start_time);
+			$this->log(sprintf("Request time for \"%s %s\": %f", $opts['http']['method'], $url, $req_time));
+		}
+
+		return $resp_data;
+
+	}
+
+
+	/**
+	 * If we get back something that claims to be XML or JSON, parse it as such and assign to $resp['body']
+	 *
+	 * @param string $resp
+	 * @return string|object
+	 * @see Resty::$JSON_TYPES
+	 * @see Resty::$XML_TYPES
+	 */
+	protected function processResponseBody($resp) {
+
+		if ($this->parse_body === true) {
+
+			$header_content_type = isset($resp['headers']['Content-Type']) ? $resp['headers']['Content-Type'] : null;
+			$content_type = preg_split('/[;\s]+/', $header_content_type);
+			$content_type = $content_type[0];
+
+			if (in_array($content_type, static::$JSON_TYPES)) {
+
+				$this->log("Response body is JSON");
+				$resp['body_raw'] = $resp['body'];
+				$resp['body'] = json_decode($resp['body']);
+				return $resp;
+
+			} elseif (in_array($content_type, static::$XML_TYPES)) {
+
+				$this->log("Response body is XML");
+				$resp['body_raw'] = $resp['body'];
+				$resp['body'] = new \SimpleXMLElement($resp['body']);
+				return $resp;
+
+			}
+
+		}
+
+		$this->log("Response body not parsed");
+
+		return $resp;
+
+	}
+
+	/**
+	 * calculate time passed in microtime
+	 * @param  float $start_time should be result of microtime(true)
+	 * @return float the diff between passed microtime and current microtime
+	 */
+	protected static function calc_time_passed($start_time) {
+		$stop_time = microtime(true);
+		$req_time = $stop_time - $start_time;
+		return $req_time;
+	}
+
+
+	protected function log($msg) {
+		if (!$this->debug) { return; }
+
+		if (is_callable($this->logger)) {
+			$logger = $this->logger;
+			return $logger($msg);
+		}
+
+		return $this->default_logger($msg);
+	}
+
+
+	/**
+	 * logging helper
+	 *
+	 * @param mixed $msg
+	 */
+	protected function default_logger($msg) {
+
+		$line = date(\DateTime::RFC822) . " :: ";
+
+		if (is_string($msg)) {
+			$line .= "{$msg}\n";
+		} else {
+			ob_start();
+			var_dump($msg);
+			$line = ob_get_clean();
+			$line .= "\n";
+		}
+
+		if (PHP_SAPI !== 'cli') {
+			$line = "<pre>$line</pre>\n";
+		}
+
+		return error_log($line);
+	}
+
+
+
+}
+

+ 135 - 0
lib/configuration.php

@@ -0,0 +1,135 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	function get_conf($key){
+		$res = query("SELECT c.value FROM configuration c WHERE c.key = '%s'",array($key));
+		if(!$res || $res->num_rows != 1 ){
+			return false;
+		}
+		$res = $res->fetch_assoc();
+		return $res['value'];
+	}
+	function set_conf($key,$val){
+		return query("UPDATE configuration c SET c.value = '%s' WHERE c.key = '%s'",array($val,$key));
+	}
+	function get_conf_type($key){
+		$res = query("SELECT c.type FROM configuration c WHERE c.key = '%s'",array($key));
+		if($res && $row = $res->fetch_assoc()){
+			return $row['type'];
+		}
+		return 'string';
+	}
+	function get_conf_values($key,$labels=false){
+		$ret = array();
+		switch(get_conf_type($key)){
+			case 'list':
+				$res = query("SELECT cl.value,cl.label FROM configuration_lists cl WHERE cl.key='%s'",array($key));
+				if($res){
+					while($row = $res->fetch_assoc()){
+						if(!$labels){
+							$row = $row['value'];
+						}
+						array_push($ret,$row);
+					}
+				}else{
+					array_push($ret,get_conf($key));
+				}
+			break;
+			case 'lookup':
+				$res = query("SELECT cl.table,cl.column,cl.label_column,cl.enabled_column FROM configuration_lookups cl WHERE cl.key='%s'",array($key));
+				if($res && $res->num_rows == 1){
+					$lookup = $res->fetch_assoc();
+					if(isset($lookup['enabled_column']) && !is_null($lookup['enabled_column']) && $lookup['enabled_column'] != ''){
+						$eq = 1;
+						if(substr($lookup['enabled_column'],0,1) == '!'){
+							$eq = 0;
+							$lookup['enabled_column'] = substr($lookup['enabled_column'],1);
+						}
+						$res = query("SELECT t.%s AS value, t.%s AS label FROM %s t WHERE t.%s = {$eq}",array($lookup['column'],$lookup['label_column'],$lookup['table'],$lookup['enabled_column']));
+					}else{
+						$res = query("SELECT t.%s AS value, t.%s AS label FROM %s t",array($lookup['column'],$lookup['label_column'],$lookup['table']));
+					}
+					if($res){
+						while($row = $res->fetch_assoc()){
+							if(!$labels){
+								$row = $row['value'];
+							}
+							array_push($ret,$row);
+						}
+					}else{
+						array_push($ret,get_conf($key));
+					}
+				}else{
+					array_push($ret,get_conf($key));
+				}
+			break;
+		}
+		return $ret;
+	}
+	function get_conf_list(){
+		$conf = array();
+		$res = query("SELECT c.key,c.description,c.value,c.type FROM configuration c");
+		if($res){
+			while($row = $res->fetch_assoc()){
+				$item = array(
+					'key'=>$row['key'],
+					'type'=>get_conf_type($row['key']),
+					'label'=>isset($row['description'])?$row['description']:''
+				);
+				$item['value'] = $row['value'];
+				if(!isset($item['value'])){
+					$item['value'] = '';
+				}
+				if(isset($item['type'])){
+					switch($item['type']){
+						case 'list':case 'lookup':
+							$item['type'] = 'select';
+							$values = get_conf_values($item['key'],true);
+							$item['values'] = array();
+							foreach($values as $value){
+								if(isset($item['value']) && $value['value'] == $item['value']){
+									$value['attributes'] = array(
+										'selected'=>'selected'
+									);
+								}
+								array_push($item['values'],$value);
+							}
+						break;
+					}
+					array_push($conf,$item);
+				}
+			}
+		}
+		return $conf;
+	}
+	function render_configuration_table(){
+		$items = array(
+			array(
+				'name'=>'action',
+				'type'=>'hidden',
+				'value'=>'config'
+			)
+		);
+		$config = get_conf_list();
+		foreach($config as $k => $conf){
+			switch($conf['type']){
+				case 'select':
+					$item = array(
+						'name'=>$conf['key'],
+						'values'=>$conf['values'],
+						'label'=>$conf['label'],
+						'type'=>'select'
+					);
+				break;
+				default:
+					$item = array(
+						'name'=>$conf['key'],
+						'value'=>$conf['value'],
+						'label'=>$conf['label'],
+						'type'=>$conf['type']
+					);
+			}
+			array_push($items,$item);
+		}
+		return get_form_html('configuration',$items,'Save');
+	}
+?>

+ 74 - 0
lib/forms.php

@@ -0,0 +1,74 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	function get_form_html($id,$fields,$sublabel){
+		array_push($fields,Array(
+			'type'=>'submit',
+			'value'=>$sublabel
+		));
+		return get_form_html_advanced(Array(
+			'id'=>$id
+		),$fields);
+	}
+	function get_form_html_advanced($attributes,$fields){
+		$r = "<form";
+		foreach($attributes as $attribute => $value){
+			$r .= " {$attribute}=\"{$value}\"";
+		}
+		$r.= ">\n";
+		foreach($fields as $k => $field){
+			$r .= get_field_html($field);
+		}
+		return $r."</form>\n";
+	}
+	function get_field_html($field){
+		$a = '';
+		if(isset($field['attributes'])){
+			foreach($field['attributes'] as $attribute => $value){
+				$a .= " {$attribute}=\"{$value}\"";
+			}
+		}
+		$v = '';
+		if(isset($field['value'])&&!is_null($field['value'])&&$field['value']!=''){
+			$v = "value='{$field['value']}'";
+		}
+		switch($field['type']){
+			case 'select':
+				$r = "<div class='row'><label for='{$field['name']}'>{$field['label']}</label><span><select name='{$field['name']}'{$a}>";
+				foreach($field['values'] as $k => $opt){
+					$a = '';
+					if(isset($opt['attributes']) && is_array($opt['attributes'])){
+						foreach($opt['attributes'] as $attribute => $value){
+							$a .= " {$attribute}=\"{$value}\"";
+						}
+					}
+					if(isset($field['value'])&&$field['value']==$opt['value']){
+						$a .= "selected=\"selected\"";
+					}
+					$r .= "<option value='{$opt['value']}'{$a}>{$opt['label']}</option>";
+				}
+				$r .= "</select></span></div>";
+			break;
+			case 'hidden':
+				$r = "<input type='hidden' name='{$field['name']}'{$v}{$a}/>";
+			break;
+			case 'custom':
+				$r = $field['html'];
+			break;
+			case 'section':
+				$r = "<div class='form_section'{$a}>";
+				if(isset($field['fields'])){
+					foreach($field['fields'] as $k => $subfield){
+						$r .= get_field_html($subfield);
+					}
+				}
+				$r .= "</div>";
+			break;
+			case 'submit':
+				$r = "<input type='submit' {$v}{$a}/>";
+			break;
+			case 'text':default:
+				$r = "<div class='row'><label for='{$field['name']}'>{$field['label']}</label><span><input type='{$field['type']}' name='{$field['name']}'{$v}{$a}/></span></div>";
+		}
+		return $r."\n";
+	}
+?>

+ 207 - 0
lib/irc.php

@@ -0,0 +1,207 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	require_once('xmlrpc.php');
+	function atheme_login($hostname,$port, $path, $username, $password){
+		$message = new xmlrpcmsg("atheme.login");
+		$message->addParam(new xmlrpcval($username, "string"));
+		$message->addParam(new xmlrpcval($password, "string"));
+		$client = new xmlrpc_client($path, $hostname, $port);
+		$response = $client->send($message);
+		if(!$response->faultCode()){
+			$session = explode("<string>", $response->serialize());
+			$session = explode("</string", $session[1]);
+			$session = $session[0];
+			return Array(true,$session);
+		}else{
+			return Array(
+				false,
+				'['.$response->faultCode().'] '.$response->faultString()
+			);
+		}
+	}
+	function atheme_command($hostname, $port, $path, $sourceip, $username, $password, $service, $command, $params=NULL){
+		$message = new xmlrpcmsg("atheme.login");
+		$message->addParam(new xmlrpcval($username, "string"));
+		$message->addParam(new xmlrpcval($password, "string"));
+		$client = new xmlrpc_client($path, $hostname, $port);
+		$response = $client->send($message);
+
+		$session = NULL;
+		if(!$response->faultCode()){
+			$session = explode("<string>", $response->serialize());
+			$session = explode("</string", $session[1]);
+			$session = $session[0];
+		}else{
+			switch($response->faultCode()){
+				case 1:
+					$m = 'Insufficient Parameters to login';
+				break;
+				case 3:
+					$m = "Account is not registered";
+				break;
+				case 5:
+					$m = "Invalid Username/Password";
+				break;
+				case 6:
+					$m = "Account is frozen";
+				break;
+				default:
+					$m = "Could not log in";
+			}
+			return Array(false,$m);
+		}
+		$message = new xmlrpcmsg("atheme.command");
+		$message->addParam(new xmlrpcval($session, "string"));
+		$message->addParam(new xmlrpcval($username, "string"));
+		$message->addParam(new xmlrpcval($sourceip, "string"));
+		$message->addParam(new xmlrpcval($service, "string"));
+		$message->addParam(new xmlrpcval($command, "string"));
+		if($params != NULL){
+			if(sizeof($params) < 2){
+				foreach($params as $param){
+					$message->addParam(new xmlrpcval($param, "string"));
+				}
+			}else{
+				$firstparam = $params[0];
+				$secondparam = "";
+				for($i = 1; $i < sizeof($params); $i++){
+					$secondparam .= $params[$i] . " ";
+				}
+				$secondparam = rtrim($secondparam);
+				$message->addParam(new xmlrpcval($firstparam, "string"));
+				$message->addParam(new xmlrpcval($secondparam, "string"));
+			}
+		}
+		$response = $client->send($message);
+		if(!$response->faultCode()){
+			$response = explode("<string>", $response->serialize());
+			$response = explode("</string", $response[1]);
+			$response = $response[0];
+			return Array(true,$response);
+		}else{
+			return Array(false,"Command failed: " . $response->faultString());
+		}
+	}
+	$ircret = "";
+	function ircputs($line){
+		global $msg;
+		global $irc;
+		$msg .= str_replace(get_conf('rehash-pass','string'),'**********',$line);
+		try{
+			error_reporting(0);
+			$r = fputs($irc,$line);
+			error_reporting(E_ALL);
+		}catch(Exception $e){
+			$r = false;
+			ircclose($e->code,$e->message);
+		}
+		return $r;
+	}
+	function ircclose($code=0,$message=null){
+		global $msg;
+		global $irc;
+		global $ircret;
+		try{
+			error_reporting(0);
+			$msg .= 'QUIT :'.$message;
+			fputs($irc,'QUIT :'.$message);
+			error_reporting(E_ALL);
+		}catch(Exception $e){}
+		while(!feof($irc) && $line = fgets($irc,128)){
+			if(is_string($line)){
+				$msg .= $line;
+			}
+		}
+		fclose($irc);
+		$ircret = '{"code":'.$code.',"message":"'.$message.'","log":'.json_encode($msg).'}';
+		return $ircret;
+	}
+	function isval($src,$prop,$val){
+		return isset($src[$prop]) && $src[$prop] == $val;
+	}
+	function ircrehash(){
+		global $msg;
+		global $irc;
+		global $ircret;
+		global $u;
+		global $user;
+		if(!isset($u)){
+			$u = $user;
+		}
+		$msg = '';
+		if(!$irc = fsockopen(get_conf('irc-server'),get_conf('irc-port'))){return ircclose(1,"Could not connect.");}
+		stream_set_timeout($irc,1) or ircclose(2,"Could not set timeout.");
+		while(!feof($irc)&&!$msg = fgets($irc,128)){}
+		if(!ircputs("NICK RehashServ\r\n")){return $ircret;}
+		if(!ircputs("USER RehashServ omni.irc.omnimaga.org RehashServ :RehashServ\r\n")){return $ircret;}
+		while(!feof($irc)){
+			$line = fgets($irc,128);
+			if(is_string($line)){
+				$msg .= $line;
+				$data = explode(' ',$line);
+				if(isval($data,1,'433')){
+					return ircclose(4,"RehashServ is already running.");
+				}elseif(strrpos($line,'ERROR :Closing Link:') !== false){
+					return ircclose(3,"IRC Server refused the connection.");
+				}elseif($data[0] == 'PING'){
+					if(!ircputs("PONG {$data[1]}")){return $ircret;}
+				}elseif(isval($data,1,'001')){
+					break;
+				}
+			}
+		}
+		if(!ircputs("IDENTIFY ".get_conf('rehash-pass','string')."\r\n")){return $ircret;}
+		while(!feof($irc)){
+			$line = fgets($irc,128);
+			if(is_string($line)){
+				$msg .= $line;
+				$data = explode(' ',$line);
+				if(isval($data,1,'433')){
+					return ircclose(4,"RehashServ is already running.");
+				}elseif(strrpos($line,'ERROR :Closing Link:') !== false){
+					return ircclose(3,"IRC Server refused the connection.");
+				}elseif(strrpos($line,":You are now identified for") !== false){
+					break;
+				}elseif(strrpos($line,'Password incorrect.') !== false){
+					return ircclose(5,"Failed to authenticate with NickServ");
+				}
+			}
+		}
+		if(!ircputs("HS ON\r\n")){return $ircret;}
+		while(!feof($irc)){
+			$line = fgets($irc,128);
+			if(is_string($line)){
+				$msg .= $line;
+				$data = explode(' ',$line);
+				if(isval($data,1,'433')){
+					return ircclose(4,"RehashServ is already running.");
+				}elseif(strrpos($line,'ERROR :Closing Link:') !== false){
+					return ircclose(3,"IRC Server refused the connection.");
+				}elseif(strrpos($line,':Your vhost of') !== false && strrpos($line,'is now activated') !== false){
+					break;
+				}elseif(strrpos($line,"Please contact an Operator to get a vhost assigned to this nick") !== false){
+					return ircclose(6,"vhost not set.");
+				}
+			}
+		}
+		if(!ircputs("OPER RehashServ ".get_conf('rehash-pass','string')."\r\n")){return $ircret;}
+		if(!ircputs("REHASH -global\r\n")){return $ircret;}
+		if(!ircputs("WALLOPS :{$u['nick']} has rehashed the server\r\n")){return $ircret;}
+		try{
+			error_reporting(0);
+			$msg .= 'QUIT :'.$message;
+			fputs($irc,'QUIT :'.$message);
+			error_reporting(E_ALL);
+		}catch(Exception $e){}
+		while(!feof($irc) && $line = fgets($irc,128)){
+			if(is_string($line)){
+				$msg .= $line;
+			}
+		}
+		fclose($irc);
+		if(strrpos($msg,':*** Notice -- Configuration loaded without any problems ..') === false){
+			return '{"code":6,"message":"There is an error in the config. See console for output.","log":'.json_encode($msg).'}';
+		}
+		return '{"code":0,"message":"Rehashed. View console for output.","log":'.json_encode($msg).'}';
+	}
+?>

+ 108 - 0
lib/opers.php

@@ -0,0 +1,108 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	function get_opers_for_current_user_obj(){
+		global $user;
+		return get_opers_for_user_obj($user['id']);
+	}
+	function get_opers_obj(){
+		$opers = Array();
+		$res = query("SELECT o.id FROM opers o");
+		if($res && $res->num_rows != 0){
+			while($oper = $res->fetch_assoc()){
+				array_push($opers,get_oper_from_id_obj($oper['id']));
+			}
+		}
+		return $opers;
+	}
+	function get_opers_for_user_obj($id){
+		$opers = Array();
+		$res = query("SELECT o.id FROM opers o WHERE o.manager_id = %d OR o.user_id = %d",Array($id,$id));
+		if($res && $res->num_rows != 0){
+			while($oper = $res->fetch_assoc()){
+				array_push($opers,get_oper_from_id_obj($oper['id']));
+			}
+		}
+		return $opers;
+	}
+	function get_oper_from_id_obj($id){
+		$opers = Array();
+		$res = query("SELECT o.id,o.nick,o.password,o.password_type,o.swhois,o.flags,o.user_id,o.manager_id FROM opers_v o WHERE o.id = %d",Array($id));
+		if($res && $res->num_rows == 1){
+			$oper = $res->fetch_assoc();
+			$hosts = query("SELECT h.host FROM hosts h WHERE oper_id = %d",Array($oper['id']));
+			if($hosts->num_rows != 0){
+				$oper['hosts'] = Array();
+				while($host = $hosts->fetch_assoc()){
+					array_push($oper['hosts'],$host['host']);
+				}
+			}else{
+				$oper['hosts'] = Array('*@*');
+			}
+			if(!isset($oper['user_id'])){
+				$oper['user_id'] = '-';
+			}
+			return $oper;
+		}
+		return $opers;
+	}
+	function get_opers_html($opers){
+		global $u;
+		global $user;
+		if(!isset($u)){
+			$u = $user;
+		}
+		$ret = "";
+		foreach($opers as $k => $oper){
+			$ret .= "<h3>".($u['id'] != $oper['user_id']?"Managed Oper:":"Personal Oper:")."</h3>".get_form_html('oper-form-'.$oper['id'],Array(
+				Array(
+					'name'=>'nick',
+					'label'=>'Nick',
+					'type'=>'text',
+					'value'=>$oper['nick']
+				),
+				Array(
+					'name'=>'swhois',
+					'label'=>'Omnimaga Profile',
+					'type'=>'text',
+					'value'=>$oper['swhois']
+				),
+				Array(
+					'name'=>'password',
+					'label'=>'New Password',
+					'type'=>'password',
+					'value'=>''
+				),
+				Array(
+					'name'=>'id',
+					'type'=>'hidden',
+					'value'=>$oper['id']
+				),
+				Array(
+					'name'=>'action',
+					'type'=>'hidden',
+					'value'=>'oper'
+				)
+			),'Save')."<hr/>";
+		}
+		return $ret;
+	}
+	function get_opers_for_server_obj($id){
+		$opers = Array();
+		$res = query("SELECT o.id,o.nick,o.password,o.password_type,o.swhois,o.flags,o.role FROM opers_v o WHERE o.server_id = %d OR o.server_id IS NULL",Array($id));
+		if($res && $res->num_rows != 0){
+		while($oper = $res->fetch_assoc()){
+			$hosts = query("SELECT h.host FROM hosts h WHERE oper_id = %d",Array($oper['id']));
+			if($hosts->num_rows != 0){
+				$oper['hosts'] = Array();
+				while($host = $hosts->fetch_assoc()){
+					array_push($oper['hosts'],$host['host']);
+				}
+			}else{
+				$oper['hosts'] = Array('*@*');
+			}
+				array_push($opers,$oper);
+			}
+		}
+		return $opers;
+	}
+?>

+ 276 - 0
lib/security.php

@@ -0,0 +1,276 @@
+<?php
+	require_once(dirname(dirname(__FILE__))."/header.php");
+	require_once("configuration.php");
+	require_once("users.php");
+	switch(get_conf('2-factor-method')){
+		case 'authy':
+			require_once("authy-php/Authy.php");
+		//break;
+		case 'google-authenticator':
+			require_once("GoogleAuthenticator.php");
+		break;
+	}
+	function get_api(){
+		static $api;
+		if(!$api){
+			switch(get_conf('2-factor-method')){
+				case 'authy':
+					$api = new Authy_Api(get_conf('authy-api-key'),get_conf('authy-endpoint'));
+				break;
+				case 'google-authenticator':
+					$api = new PHPGangsta_GoogleAuthenticator();
+				break;
+			}
+		}
+		return $api;
+	}
+	function login($nick,$pass,$type,$effective_role=null){
+		if($type == 'user'){
+			$user = atheme_command(get_conf('xmlrpc-server'),get_conf('xmlrpc-port'),get_conf('xmlrpc-path'),USER_IP,$nick,$pass,'NickServ','info',Array($nick));
+			if($user[0]){
+				$user[2] = explode('&#10;',$user[1]);
+				$user[3] = Array();
+				foreach($user[2] as $k => $row){
+					$row = preg_split('/\s+:\s/',$row);
+					if(isset($row[1])){
+						$row[2] = explode(' ',$row[1]);
+					}else{
+						$row[1] = '';
+						$row[2] = Array();
+					}
+					$user[3][$row[0]] = Array($row[1],$row[2]);
+				}
+				$_SESSION['password'] = $pass;
+				$_SESSION['key'] = uniqid();
+				$_SESSION['real_name'] = $nick;
+				if(isset($user[3]['Email'][0])){
+					$_SESSION['email'] = $user[3]['Email'][0];
+				}else{
+					$_SESSION['email'] = '';
+				}
+				if($res = query("SELECT u.api_key, u.real_name FROM users u WHERE lower(u.nick) = lower('%s')",Array($nick))){
+					if($res->num_rows == 1){
+						$res = $res->fetch_assoc();
+						$_SESSION['key'] = $res['api_key'];
+						$_SESSION['real_name'] = $res['real_name'];
+					}
+				}
+				setcookie('key',$_SESSION['key'],null,'/');
+				setcookie('user',$nick,null,'/');
+				setcookie('type','user',null,'/');
+				return true;
+			}else{
+				return "Invalid credentials";
+			}
+		}elseif($type=='persona'){
+			if(!$user = get_user_obj($nick,$effective_role)){
+				return "User {$nick} does not exist";
+			}
+			if(!isset($_COOKIE['personaUser'])){
+				return false;
+			}
+			if(!in_array($_COOKIE['personaUser'],get_emails($user['id']))){
+				return "Invalid persona email";
+			}
+			setcookie('user',$nick,null,'/');
+			setcookie('key',$user['api_key'],null,'/');
+			setcookie('type',$user['type'],null,'/');
+			return true;
+		}else{
+			if(!$user = get_user_obj($nick,$type)){
+				return "User {$nick} does not exist";
+			}
+			if($user['password'] != mkpasswd($pass,$user['salt'])){
+				return "Invalid password";
+			}
+			setcookie('user',$nick,null,'/');
+			setcookie('key',$user['api_key'],null,'/');
+			setcookie('type',$user['type'],null,'/');
+			return true;
+		}
+	}
+	function verify($token){
+		$api = get_api();
+		if($u = is_logged_in()){
+			switch(get_conf('2-factor-method')){
+				case 'authy':
+					$verification = $api->verifyToken($u['secret_key'],$token);
+					if($verification->ok()){
+						setcookie('token',$u['secret_key'],null,'/');
+						$r = true;
+					}else{
+						$r = 'Failed to create Authy user: ';
+						foreach($verification->errors() as $field => $message){
+							$message = json_decode($message);
+							$r .= $message['message'];
+						}
+						logout();
+					}
+				break;
+				case 'google-authenticator':
+					if($api->verifyCode($u['secret_key'],$token,2)){
+						$_SESSION['secret_key'] = $u['secret_key'];
+						$r = true;
+					}else{
+						$r = "Token didn't match ".$u['secret_key'];
+					}
+				break;
+				default:
+					$r = true;
+			}
+		}else{
+			$r = "You have been logged out";
+		}
+		return $r;
+	}
+	function delete_token(){
+		$r = true;
+		$api = get_api();
+		$u = is_logged_in();
+		if($u){
+			switch(get_conf('2-factor-method')){
+				case 'authy':
+					$deletion = $api->deleteUser($u['secret_key']);
+					if($deletion->ok()){
+						setcookie('secret_key','',time() - 3600,'/');
+						if(!query("UPDATE users u SET u.secret_key=NULL WHERE u.id=%d",Array($u['id']))){
+							$r = 'Failed to disable 2-factor authentication';
+						}
+					}else{
+						$r = 'Failed to disable 2-factor authentication: ';
+						foreach($deletion->errors() as $field => $message){
+							$message = json_decode($message);
+							$r .= $message->message;
+						}
+					}
+				break;
+				case 'google-authenticator':
+					setcookie('secret_key','',time() - 3600,'/');
+					if(!query("UPDATE users u SET u.secret_key=NULL WHERE u.id=%d",Array($u['id']))){
+						$r = 'Failed to disable 2-factor authentication';
+					}
+				break;
+				default:
+			}
+		}
+		return $r;
+	}
+	function register_token(){
+		$api = get_api();
+		$u = is_logged_in();
+		if($u){
+			switch(get_conf('2-factor-method')){
+				case 'authy':
+					if(isset($_GET['country-code'])){
+						if(isset($_GET['cellphone'])){
+							$user = $api->registerUser($u['email'],$_GET['cellphone'],$_GET['country-code']);
+							if($user->ok()){
+								query("UPDATE users u SET u.secret_key='%s' WHERE u.id=%d",Array($user->id(),$u['id']));
+								$r = true;
+							}else{
+								$r = 'Failed to create Authy user: ';
+								foreach($user->errors() as $field => $message){
+									$message = json_decode($message);
+									$r .= $message['message'];
+								}
+							}
+						}else{
+							$r = "No cell number set";
+						}
+					}else{
+						$r = "No country code set";
+					}
+				break;
+				case 'google-authenticator':
+					if(isset($_GET['token'])){
+						if(isset($_SESSION['secret_key'])){
+							if($api->verifyCode($_SESSION['secret_key'],$_GET['token'], 2)){
+								query("UPDATE users u SET u.secret_key='%s' WHERE u.id=%d",Array($_SESSION['secret_key'],$u['id']));
+								$r = true;
+							}else{
+								$r = 'Could not register';
+							}
+						}else{
+							$r = 'No secret key defined';
+						}
+					}else{
+						$r = 'No token provided';
+					}
+				break;
+				default:
+					$r = true;
+			}
+		}else{
+			$r = "You have been logged out";
+		}
+		return $r;
+	}
+	function is_logged_in(){
+		$user = false;
+		if(isset($_COOKIE['user']) && isset($_COOKIE['key']) && isset($_COOKIE['type'])){