master
Corne Oppelaar 9 years ago
parent 23c3cf530e
commit ebddf2f031

6
Vagrantfile vendored

@ -8,7 +8,7 @@ Vagrant.configure(2) do |config|
web.vm.provision :shell, inline: <<installphp web.vm.provision :shell, inline: <<installphp
add-apt-repository -y ppa:ondrej/php >/dev/null add-apt-repository -y ppa:ondrej/php >/dev/null
apt-get -qq update 2>/dev/null apt-get -qq update 2>/dev/null
apt-get -qq install php7.0-cli php7.0-sqlite3 2>/dev/null >/dev/null; apt-get -qq install php7.0-cli php7.0-sqlite3 php7.0-zip 2>/dev/null >/dev/null;
start-stop-daemon -bS --quiet --make-pidfile --pidfile /var/run/zerooo.pid --startas /bin/bash -- -c "exec php -S 0:8888 -t /vagrant/public/ /vagrant/public/index.php > /var/log/zerooo.log 2>&1"; start-stop-daemon -bS --quiet --make-pidfile --pidfile /var/run/zerooo.pid --startas /bin/bash -- -c "exec php -S 0:8888 -t /vagrant/public/ /vagrant/public/index.php > /var/log/zerooo.log 2>&1";
installphp installphp
web.vm.network "forwarded_port", guest: 8888, host: 8888 web.vm.network "forwarded_port", guest: 8888, host: 8888
@ -18,6 +18,10 @@ installphp
vpn.vm.box = "ubuntu/wily64" vpn.vm.box = "ubuntu/wily64"
vpn.vm.network "private_network", ip: "192.168.50.8" vpn.vm.network "private_network", ip: "192.168.50.8"
vpn.vm.synced_folder "../zer.ooo-server", "/server" vpn.vm.synced_folder "../zer.ooo-server", "/server"
vpn.vm.network "forwarded_port", guest: 7864, host: 7864
vpn.vm.provision :shell, inline: <<installnode
bash /server/main.sh all "http://192.168.50.4:8888";
installnode
end end
end end

@ -1,7 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
BINDIR=$(dirname $(realpath $0));
BASEDIR=$(realpath "$BINDIR/../"); set -e;
cd $BASEDIR;
BINDIR="$(dirname $(realpath "${0}"))";
BASEDIR="$(realpath "${BINDIR}/../")";
cd "${BASEDIR}";
rm -rf ./storage/ca/*; rm -rf ./storage/ca/*;
mkdir ./storage/ca/certs; mkdir ./storage/ca/certs;
touch ./storage/ca/{,certs/}.gitkeep; touch ./storage/ca/certs/.gitkeep ./storage/ca/.gitkeep;

@ -2,7 +2,7 @@
BASEDIR=$(realpath $(dirname $0)); BASEDIR=$(realpath $(dirname $0));
KEYDIR=$(realpath "$BASEDIR/../storage/ca/"); KEYDIR=$(realpath "$BASEDIR/../storage/ca/");
if [ -f $KEYDIR/ca.key ]; then if [ -f "$KEYDIR/ca.key" ]; then
echo "CA key already exists. not overwriting it." echo "CA key already exists. not overwriting it."
exit 1; exit 1;
fi fi
@ -10,7 +10,7 @@ fi
CN="ob.ae-cn"; CN="ob.ae-cn";
if [ ! -z "$1" ]; then if [ ! -z "$1" ]; then
CN=$1; CN="$1";
fi; fi;
openssl req -days 3650 -nodes -new -x509 -keyout $KEYDIR/ca.key -out $KEYDIR/ca.crt -subj "/CN=$CN" -extensions ca_ext -config "$BASEDIR/../etc/openssl.conf"; openssl req -days 3650 -nodes -new -x509 -keyout "$KEYDIR/ca.key" -out "$KEYDIR/ca.crt" -subj "/CN=$CN" -extensions ca_ext -config "$BASEDIR/../etc/openssl.conf";

@ -1,15 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e;
BASEDIR="$(dirname $(realpath $0))/../"; BASEDIR="$(dirname $(realpath $0))/../";
cd $BASEDIR; cd "${BASEDIR}";
STORAGE=$(realpath "$BASEDIR/storage/ca"); STORAGE="$(realpath "${BASEDIR}/storage/ca")";
mkdir -p $STORAGE; mkdir -p "${STORAGE}/certs";
mkdir -p $STORAGE/certs;
echo 01 > $STORAGE/serial; echo 01 > "${STORAGE}/serial";
echo 01 > $STORAGE/crl_serial; echo 01 > "${STORAGE}/crl_serial";
touch $STORAGE/database; touch "${STORAGE}/database";
touch $STORAGE/database.attr; touch "${STORAGE}/database.attr";
$BASEDIR/bin/create-ca $1; "${BASEDIR}/bin/create-ca" $1;
$BASEDIR/bin/create-crl; "${BASEDIR}/bin/create-crl";

@ -1,6 +1,9 @@
prefix: Eater\Glim\Handler\ prefix: Eater\Glim\Handler\
routes: routes:
/: Home /: Home
/install:
get: Install\Show
post: Install\Action
/login: /login:
get: Login\Show get: Login\Show
post: Login\Action post: Login\Action
@ -19,11 +22,15 @@ routes:
/revoke: /revoke:
post: Panel\Certificates\Revoke post: Panel\Certificates\Revoke
/server: /server:
/remove:
post: Panel\Servers\Remove
/sign: /sign:
post: Panel\Servers\Sign post: Panel\Servers\Sign
/{fingerprint}: /{fingerprint}:
get: Panel\Servers\Edit\Show get: Panel\Servers\Edit\Show
post: Panel\Servers\Edit\Action post: Panel\Servers\Edit\Action
/{fingerprint}/config: Panel\Servers\Config
/{fingerprint}/config/{cert}: Panel\Servers\Config
/server: /server:
/register: /register:
post: Server\Register post: Server\Register

@ -31,7 +31,7 @@ subjectKeyIdentifier=hash
authorityKeyIdentifier = keyid,issuer:always authorityKeyIdentifier = keyid,issuer:always
extendedKeyUsage = serverAuth extendedKeyUsage = serverAuth
keyUsage = digitalSignature,keyEncipherment keyUsage = digitalSignature,keyEncipherment
crlDistributionPoints = URI:http://0b.ae/crl crlDistributionPoints = URI:http://localhost:8888/crl
[client_ext] [client_ext]
subjectKeyIdentifier=hash subjectKeyIdentifier=hash
basicConstraints = CA:FALSE basicConstraints = CA:FALSE

@ -14,15 +14,17 @@ $(function () {
function save(callback) { function save(callback) {
var fingerprint = $('.server-form').data('fingerprint'); var fingerprint = $('.server-form').data('fingerprint');
var data = $('.server-form input').serializeArray(); var data = $('.server-form input,.server-form select').serializeArray();
$.post('/panel/server/' + fingerprint, data, function (data) { $.post('/panel/server/' + fingerprint, data, function (data) {
console.log(data);
if (!data.success) { if (!data.success) {
$('h2').after(error.text(data.error)); $('h2').after(error.text(data.error));
return;
} }
callback(); callback();
}); }, 'json');
} }
function sign() { function sign() {
@ -35,8 +37,12 @@ $(function () {
function (data) { function (data) {
if (!data.success) { if (!data.success) {
$('h2').after(error.text(data.error)); $('h2').after(error.text(data.error));
return;
} }
}
location.reload();
},
'json'
); );
} }
}); });

@ -9,8 +9,8 @@ $(function () {
return; return;
} }
if (!/^[A-Za-z0-9_-]+$/.test(name)) { if (!/^[A-Za-z0-9\-]+$/.test(name)) {
$('h2').after(error.text("Only alphanumeric, _ and - allowed in name")); $('h2').after(error.text("Only alphanumeric and - allowed in name"));
return; return;
} }
@ -57,7 +57,7 @@ $(function () {
} else { } else {
$('h2').after(error.text(data.error)); $('h2').after(error.text(data.error));
} }
}); }, 'json');
}); });
$("#wantsPassword").change(function () { $("#wantsPassword").change(function () {

@ -26,4 +26,11 @@ $(function(){
$('.revoke-password').val(''); $('.revoke-password').val('');
}); });
$('.remove').click(function () {
var that = this;
$.post('/panel/server/remove',{ fingerprint: $(this).data('fingerprint') }, function () {
$(that).parents('tr').first().remove();
});
});
}); });

@ -324,6 +324,7 @@ class Core implements ContainerInterface
public function handle($class, Request $request, Response $response, ContainerInterface $containerInterface) public function handle($class, Request $request, Response $response, ContainerInterface $containerInterface)
{ {
$this->startTimer(['response']); $this->startTimer(['response']);
/** @var Handler\Main $handler */
$handler = new $class($this, $request, $response, $containerInterface); $handler = new $class($this, $request, $response, $containerInterface);
$this->startTimer(['response/before-handle']); $this->startTimer(['response/before-handle']);

@ -2,11 +2,11 @@
namespace Eater\Glim\Handler; namespace Eater\Glim\Handler;
class CA extends Session class CA extends Main
{ {
function handle() function handle()
{ {
return$this->getResponse() return $this->getResponse()
->withHeader('Content-Type', 'plain/text') ->withHeader('Content-Type', 'plain/text')
->withHeader('Content-Disposition', 'attachment; filename="ca.crt"') ->withHeader('Content-Disposition', 'attachment; filename="ca.crt"')
->write(file_get_contents($this->getCore()->getBaseDir() . '/storage/ca/ca.crt')); ->write(file_get_contents($this->getCore()->getBaseDir() . '/storage/ca/ca.crt'));

@ -0,0 +1,21 @@
<?php
/**
* Created by PhpStorm.
* User: eater
* Date: 6/6/16
* Time: 11:28 PM
*/
namespace Eater\Glim\Handler;
class CRL extends Main
{
function handle()
{
return $this->getResponse()
->withHeader('Content-Type', 'plain/text')
->withHeader('Content-Disposition', 'attachment; filename="crl.pem"')
->write(file_get_contents($this->getCore()->getBaseDir() . '/storage/ca/crl.pem'));
}
}

@ -0,0 +1,53 @@
<?php
namespace Eater\Glim\Handler\Install;
use Eater\Glim\Service\User;
class Action extends Show
{
public function handle()
{
/** @var User $user */
$user = $this->get('user');
$caCN = $this->post('ca-cn');
/* @var \Aura\Session\Session $session */
$session = $this->get('session');
$segment = $session->getSegment('main');
if (trim($caCN) === "") {
$segment->setFlash('error', 'CA Common name can\'t be empty');
return $this->redirect('/install');
}
/** @var \Twig_Environment $twig */
$twig = $this->get('twig');
$opensslConf = $twig->render('etc/openssl-ca.conf.twig', [
'host' => $this->post('domainWithPort')
]);
file_put_contents($this->getCore()->getBaseDir() . '/etc/openssl.conf', $opensslConf);
exec($this->getCore()->getBaseDir() . '/bin/clean-all');
exec($this->getCore()->getBaseDir() . '/bin/setup ' . escapeshellarg($caCN));
$newUser = null;
try {
$newUser = $user->createSuperuser($this->post('username'), $this->post('password'));
} catch (\Exception $e) {
$segment->setFlash("error", $e->getMessage());
}
if ($newUser === null) {
return $this->redirect('/install');
}
$segment->set('user', $newUser);
return $this->redirect('/panel');
}
}

@ -0,0 +1,54 @@
<?php
namespace Eater\Glim\Handler\Install;
use Eater\Glim\Handler\Main;
use Eater\Glim\Model\UserQuery;
class Show extends Main
{
public function beforeHandle() {
if (UserQuery::create()->findOne()) {
return $this->redirect('/login');
}
}
public function handle()
{
$return = 1;
$execEnabled = $this->isExecEnabled();
$hasOpenSsl = false;
if ($execEnabled) {
exec('command -v openssl', $output, $return);
$hasOpenSsl = $return === 0;
}
$data = [
'hasExecEnabled' => $execEnabled,
'hasOpenSsl' => $hasOpenSsl,
'hasOpenSslExtension' => extension_loaded('openssl'),
'hasZipExtension' => extension_loaded('zip'),
'hostname' => parse_url($_SERVER['HTTP_HOST'], PHP_URL_HOST),
'hostnameWithPort' => $_SERVER['HTTP_HOST']
];
return $this->render('install.html.twig', $data);
}
private function isExecEnabled() {
if (ini_get('safe_mode')) {
return false;
} else {
$d = ini_get('disable_functions');
$s = ini_get('suhosin.executor.func.blacklist');
if ("$d$s") {
$array = preg_split('/,\s*/', "$d,$s");
if (in_array('exec', $array)) {
return false;
}
}
}
return true;
}
}

@ -11,9 +11,25 @@ namespace Eater\Glim\Handler\Login;
use Aura\Session\Segment; use Aura\Session\Segment;
use Eater\Glim\Handler\Main; use Eater\Glim\Handler\Main;
use Eater\Glim\Model\UserQuery;
class Show extends Main class Show extends Main
{ {
public function beforeHandle()
{
$session = $this->get('session');
$segment = $session->getSegment('main');
$user = $segment->get('user');
if ($user) {
$this->redirect('/panel');
}
if (!UserQuery::create()->findOne()) {
return $this->redirect('/install');
}
}
public function handle() public function handle()
{ {
/** @var Segment $segment */ /** @var Segment $segment */

@ -146,7 +146,6 @@ abstract class Main implements ContainerInterface
return $this->getResponse() return $this->getResponse()
->withHeader('Content-Type', 'application/json') ->withHeader('Content-Type', 'application/json')
->write(json_encode($array)); ->write(json_encode($array));
} }
/** /**

@ -29,7 +29,7 @@ class Download extends Session
return $this->getResponse() return $this->getResponse()
->withHeader('Content-Type', 'plain/text') ->withHeader('Content-Type', 'plain/text')
->withHeader('Content-Disposition', 'attachment; filename="' . $name . '.' . $this->getUser()->getUsername() .'.crt"') ->withHeader('Content-Disposition', 'attachment; filename="' . $name . '.' . $cert->getSerial() .'.crt"')
->write($cert->getCertificate()); ->write($cert->getCertificate());
} }
} }

@ -0,0 +1,89 @@
<?php
/**
* Created by PhpStorm.
* User: eater
* Date: 6/12/16
* Time: 3:26 PM
*/
namespace Eater\Glim\Handler\Panel\Servers;
use Eater\Glim\Handler\Session;
use Eater\Glim\Model\Certificate;
use Eater\Glim\Model\CertificateQuery;
use Eater\Glim\Model\Server;
use Eater\Glim\Model\ServerQuery;
class Config extends Session
{
public function handle()
{
$zipFile = tempnam(sys_get_temp_dir(), '0zip');
$zip = new \ZipArchive();
$zip->open($zipFile, \ZipArchive::CREATE);
$server = ServerQuery::create()->findOneByFingerprint($this->attr('fingerprint'));
$name = $server->getFqdn();
$this->fillZipWithCaAndConfig($zip, $server);
$cert = $this->attr('cert');
if ($cert !== null) {
$certModel = CertificateQuery::create()->findOneByUserAndName($this->getUser(), $cert);
$this->addClientCertificateData($zip, $certModel);
$name .= '-' . $certModel->getName() . '.' . $certModel->getSerial();
}
$zip->close();
$zipContents = file_get_contents($zipFile);
unlink($zipFile);
return $this->getResponse()
->withHeader('Content-Type', 'application/zip')
->withHeader('Content-Disposition', 'attachment; filename="' . $name . '.zip"')
->write($zipContents);
}
/**
* @param \ZipArchive $zip
* @param Server $server
*/
public function fillZipWithCaAndConfig($zip, $server)
{
$config = $this->getConfigForServerFingerprint($server);
$zip->addFromString('server.conf', $config);
$zip->addFromString('ca.crt', file_get_contents($this->getCore()->getBaseDir() . '/storage/ca/ca.crt'));
}
/**
* @param Server $server
* @return string
*/
public function getConfigForServerFingerprint($server)
{
/** @var \Twig_Environment $twig */
$twig = $this->get('twig');
$config = $twig->render('etc/openvpn-client.conf.twig', [
'server' => $server
]);
return $config;
}
/**
* @param \ZipArchive $zip
* @param Certificate $cert
*/
public function addClientCertificateData($zip, $cert)
{
$zip->addFromString('client.crt', $cert->getCertificate());
if ($cert->hasPrivateKey()) {
$zip->addFromString('client.key', $cert->getPrivateKey());
}
}
}

@ -4,6 +4,7 @@ namespace Eater\Glim\Handler\Panel\Servers\Edit;
use Eater\Glim\Handler\Session; use Eater\Glim\Handler\Session;
use Eater\Glim\Model\ServerQuery; use Eater\Glim\Model\ServerQuery;
use Eater\Glim\Service\Server;
class Action extends Session class Action extends Session
{ {
@ -23,13 +24,24 @@ class Action extends Session
# Config # Config
$server->setInternalIp($this->post('internal-ip')); $server->setInternalIp($this->post('internal-ip'));
$server->setNetmask($this->post('netmask')); $server->setNetmask($this->post('netmask'));
$server->setPort($this->post('post')); $server->setPort($this->post('port'));
$server->setProtocol($this->post('protocol')); $server->setProtocol($this->post('protocol'));
$server->setFirstDns($this->post('first-dns')); $server->setFirstDns($this->post('first-dns'));
$server->setSecondDns($this->post('second-dns')); $server->setSecondDns($this->post('second-dns'));
$server->save(); $server->save();
$this->json([ 'success'=> true ]); /** @var Server $serverService */
$serverService = $this->get('server');
try {
$serverService->deliverOpenVPNConfig($server);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'error' => $e->getMessage()
]);
}
return $this->json([ 'success'=> true ]);
} }
} }

@ -0,0 +1,24 @@
<?php
namespace Eater\Glim\Handler\Panel\Servers;
use Eater\Glim\Handler\Session;
use Eater\Glim\Model\ServerQuery;
use Eater\Glim\Service\CA;
use Eater\Glim\Service\Server;
use Symfony\Component\Config\Definition\Exception\Exception;
class Remove extends Session
{
protected $shouldHaveSuperuser = true;
public function handle()
{
$server = ServerQuery::create()->findOneByFingerprint($this->post('fingerprint'));
$server->delete();
return $this->json([
'success' => true
]);
}
}

@ -17,10 +17,12 @@ class Sign extends Session
/** @var CA $ca */ /** @var CA $ca */
$ca = $this->get('ca'); $ca = $this->get('ca');
$server = ServerQuery::create()->findOneByFingerprint($this->post('fingerprint')); $server = ServerQuery::create()->findOneByFingerprint($this->post('fingerprint'));
$publicKey = $server->getPublicKey();
/** @var Server $serverManager */
$serverManager = $this->get('server');
try { try {
$crt = $ca->signServerKey($publicKey, $server->getFqdn()); $csr = $serverManager->getCsrFromServer($server);
$crt = $ca->signServerKey($csr, $server->getFqdn());
} catch (\Exception $e) { } catch (\Exception $e) {
return $this->json([ return $this->json([
'success' => false, 'success' => false,
@ -31,9 +33,6 @@ class Sign extends Session
$server->setCertificate($crt); $server->setCertificate($crt);
$server->save(); $server->save();
/** @var Server $serverManager */
$serverManager = $this->get('server');
try { try {
$serverManager->deliverSignedCertificate($server); $serverManager->deliverSignedCertificate($server);
} catch (\Exception $e) { } catch (\Exception $e) {
@ -43,7 +42,7 @@ class Sign extends Session
]); ]);
} }
return $this->json([ return $this->json([
'success' => true 'success' => true
]); ]);
} }

@ -9,6 +9,7 @@
namespace Eater\Glim\Handler; namespace Eater\Glim\Handler;
use Aura\Session\Segment; use Aura\Session\Segment;
use Eater\Glim\Model\Base\UserQuery;
use Eater\Glim\Model\User; use Eater\Glim\Model\User;
use Eater\Glim\Service\TwigVars; use Eater\Glim\Service\TwigVars;
@ -48,6 +49,10 @@ class Session extends Main
public function beforeHandle() public function beforeHandle()
{ {
if (!UserQuery::create()->findOne()) {
return $this->redirect('/install');
}
/* @var \Aura\Session\Session */ /* @var \Aura\Session\Session */
$session = $this->get('session'); $session = $this->get('session');
/** @var TwigVars $twigVar */ /** @var TwigVars $twigVar */

@ -16,5 +16,12 @@ use Eater\Glim\Model\Base\Certificate as BaseCertificate;
*/ */
class Certificate extends BaseCertificate class Certificate extends BaseCertificate
{ {
/**
* @return bool
*/
public function hasPrivateKey()
{
$privateKey = $this->getPrivateKey();
return !empty($privateKey);
}
} }

@ -16,5 +16,8 @@ use Eater\Glim\Model\Base\Server as BaseServer;
*/ */
class Server extends BaseServer class Server extends BaseServer
{ {
public function getNetmaskIp()
{
return long2ip(-1 << (32 - (int)$this->getNetmask()));
}
} }

@ -16,5 +16,4 @@ use Eater\Glim\Model\Base\User as BaseUser;
*/ */
class User extends BaseUser class User extends BaseUser
{ {
} }

@ -13,6 +13,14 @@ use Eater\Glim\Core;
class CA extends Main class CA extends Main
{ {
public function getPrivateKey()
{
/** @var Core $core */
$core = $this->get('core');
return file_get_contents($core->getBaseDir() . '/storage/ca/ca.key');
}
/** /**
* @return string * @return string
*/ */
@ -56,47 +64,14 @@ class CA extends Main
return $crt; return $crt;
} }
/**
* @param string $publicKey
* @param string $fqdn
* @return string
* @throws \Exception
*/
public function createCSRForKeyAndFqdn($publicKey, $fqdn)
{
/** @var Core $core */
$core = $this->get('core');
$csrPath = tempnam(sys_get_temp_dir(), '0.');
$pubPath = tempnam(sys_get_temp_dir(), '0.');
file_put_contents($pubPath, $publicKey);
exec(escapeshellcmd($core->getBaseDir() . '/bin/create-csr') . ' ' . escapeshellarg($fqdn) . ' ' . escapeshellarg($csrPath) . ' ' . escapeshellarg($pubPath) . ' 2>&1', $output, $exitCode);
if ($exitCode !== 0) {
throw new \Exception("Failed creating CSR: " . implode("\n", $output));
}
$csr = file_get_contents($csrPath);
unlink($pubPath);
unlink($csrPath);
return $csr;
}
/** /**
* Signs a client certificate and returns the signed certificate * Signs a client certificate and returns the signed certificate
* @param string $publicKey * @param string $csr
* @param string $fqdn
* @return string * @return string
* @throws \Exception * @throws \Exception
*/ */
public function signServerKey($publicKey, $fqdn) public function signServerKey($csr)
{ {
$csr = $this->createCSRForKeyAndFqdn($publicKey, $fqdn);
/** @var Core $core */ /** @var Core $core */
$core = $this->get('core'); $core = $this->get('core');
@ -197,13 +172,10 @@ class CA extends Main
} }
public function signWithCA($data) { public function signWithCA($data) {
/** @var Core $core */ $privateKeyPlain = $this->getPrivateKey();
$core = $this->get('core');
$privateKeyPlain = file_get_contents($core->getBaseDir() . '/storage/ca/ca.key');
$privateKey = openssl_get_privatekey($privateKeyPlain); $privateKey = openssl_get_privatekey($privateKeyPlain);
$result = openssl_sign($data, $signature, $privateKey); $result = openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256);
if ($result) { if ($result) {
return $signature; return $signature;

@ -5,6 +5,7 @@ namespace Eater\Glim\Service;
use Eater\Glim\Model\Server as ServerModel; use Eater\Glim\Model\Server as ServerModel;
use GuzzleHttp\Client as HttpClient; use GuzzleHttp\Client as HttpClient;
use Eater\Glim\Core; use Eater\Glim\Core;
use Psr\Http\Message\ResponseInterface;
class Server extends Main class Server extends Main
{ {
@ -20,18 +21,52 @@ class Server extends Main
return $this->httpClient; return $this->httpClient;
} }
public function getCsrFromServer(ServerModel $server) {
$response = $this->doSignedRequest($server, '/create-csr', [
'hostname' => $server->getFqdn()
]);
if ($response->getStatusCode() !== 200) {
throw new \Exception('Requesting CSR failed (' . $response->getStatusCode() . '): ' . $response->getBody()->getContents());
}
$data = $this->decryptResponse($server, $response);
return $data['csr'];
}
/** /**
* @param ServerModel $server * @param ServerModel $server
* @throws \Exception * @throws \Exception
*/ */
public function deliverSignedCertificate(ServerModel $server) { public function deliverSignedCertificate(ServerModel $server) {
$response = $this->doSignedRequest($server, '/deliver-crt', [ $response = $this->doSignedRequest($server, '/deliver-crt', [
'certficate' => $server->getCertificate() 'certificate' => $server->getCertificate()
]); ]);
if ($response->getStatusCode() !== 200) { if ($response->getStatusCode() !== 200) {
throw new \Exception('Delivering signed certificate failed (' . $response->getStatusCode() . '): ' . $response->getBody()->getContents()); throw new \Exception('Delivering signed certificate failed (' . $response->getStatusCode() . '): ' . $response->getBody()->getContents());
} }
$server->setStatus('signed');
$server->save();
}
public function deliverOpenVPNConfig(ServerModel $server) {
/** @var \Twig_Environment $twig */
$twig = $this->get('twig');
$config = $twig->render('etc/openvpn-server.conf.twig', [
'server' => $server
]);
$response = $this->doSignedRequest($server, '/update-openvpn-config', [
'config' => $config
]);
if ($response->getStatusCode() !== 200) {
throw new \Exception('Updating openvpn server config failed (' . $response->getStatusCode() . '): ' . $response->getBody()->getContents());
}
} }
/** /**
@ -51,7 +86,7 @@ class Server extends Main
$json = json_encode($data); $json = json_encode($data);
$password = bin2hex(openssl_random_pseudo_bytes(32)); $password = bin2hex(openssl_random_pseudo_bytes(48));
$pubKey = openssl_get_publickey($server->getPublicKey()); $pubKey = openssl_get_publickey($server->getPublicKey());
$success = openssl_public_encrypt($password, $crypted, $pubKey, OPENSSL_PKCS1_PADDING); $success = openssl_public_encrypt($password, $crypted, $pubKey, OPENSSL_PKCS1_PADDING);
@ -59,19 +94,34 @@ class Server extends Main
throw new \Exception('Encrypting data failed: ' . openssl_error_string() . openssl_error_string()); throw new \Exception('Encrypting data failed: ' . openssl_error_string() . openssl_error_string());
} }
$this->get('logger')->addDebug('Password: ' . $password); $body = bin2hex($crypted) . bin2hex(openssl_encrypt($json, 'blowfish', substr($password, 0, 32), true, substr($password, 32, 8)));
return $client->post('http://' . $server->getExternalIp() . ':' . static::MANAGEMENT_PORT . $path, [
'body' => $body
]);
}
$body = [ public function decryptResponse(ServerModel $server, ResponseInterface $response) {
bin2hex($crypted), $body = $response->getBody()->getContents();
bin2hex(openssl_encrypt($server->getCertificate(), 'aes-256-cbc', $password, 'help')) $this->get('logger')->addDebug($body);
]; $encryptedPasswordAndIV = hex2bin(substr($body, 0, 512));
$encryptedBody = hex2bin(substr($body, 512));
$this->get('logger')->addDebug('Help: ' . var_export([$json, $body], true)); /** @var CA $ca */
$ca = $this->get('ca');
$success = openssl_private_decrypt($encryptedPasswordAndIV, $passwordAndIV, $ca->getPrivateKey(), OPENSSL_PKCS1_PADDING);
if (!$success) {
throw new \Exception('Decrypting data failed: ' . openssl_error_string() . openssl_error_string());
}
return $client->post('http://' . $server->getExternalIp() . ':' . static::MANAGEMENT_PORT . $path, [ $json = openssl_decrypt($encryptedBody, 'blowfish', substr($passwordAndIV, 0, 32), true, substr($passwordAndIV, 32, 8));
'json' => $body $data = \json_decode($json, true);
]); $ver = openssl_verify($server->getFingerprint(), hex2bin($data['signature']), $server->getPublicKey(), OPENSSL_ALGO_SHA256);
if ($ver === 1) {
return $data;
} else {
throw new \Exception("Checking signature failed ($ver): " . openssl_error_string());
}
} }
} }

@ -29,7 +29,8 @@ class Twig
$loader = new \Twig_Loader_Filesystem($core->getBaseDir() . '/views/'); $loader = new \Twig_Loader_Filesystem($core->getBaseDir() . '/views/');
$twig = new \Twig_Environment($loader, array( $twig = new \Twig_Environment($loader, array(
'cache' => $core->getBaseDir() . '/tmp/twig', 'cache' => $core->getBaseDir() . '/tmp/twig',
'debug' => $debug 'debug' => $debug,
'displayErrorDetails' => $debug
)); ));
if ($twig) { if ($twig) {

@ -24,17 +24,50 @@ class User extends Main
throw new \Exception("Invalid invite code"); throw new \Exception("Invalid invite code");
} }
$this->validateUserParams($username, $password);
$user = new UserModel();
$user->setUsername($username);
$user->setPassword(\password_hash($password, PASSWORD_DEFAULT));
$user->save();
$invite->delete();
return $user;
}
public function validateUserParams($username, $password) {
if ($username === "") {
throw new \Exception("No username given");
}
if (!preg_match('~^[a-z0-9\-]+$~', $username)) {
throw new \Exception("Username can only consist of a-z, 0-9 and -");
}
if ($password === "") {
throw new \Exception("Password is nothing, though strong. we rather not have you use that");
}
if (strlen($password) < 9) {
throw new \Exception("Please pick a password with more then 8 characters");
}
if ($this->exists($username)) { if ($this->exists($username)) {
throw new \Exception("User already exists"); throw new \Exception("User already exists");
} }
}
public function createSuperuser($username, $password) {
$this->validateUserParams($username, $password);
$user = new UserModel(); $user = new UserModel();
$user->setUsername($username); $user->setUsername($username);
$user->setPassword(\password_hash($password, PASSWORD_DEFAULT)); $user->setPassword(\password_hash($password, PASSWORD_DEFAULT));
$user->setSuperuser(true);
$user->save(); $user->save();
$invite->delete();
return $user; return $user;
} }

@ -6,15 +6,14 @@ default_ca=ca_default
[v3_ca] [v3_ca]
[ca_default] [ca_default]
crl_extensions=crl_ext crl_extensions=crl_ext
unique_subject=no private_key=storage/ca/ca.key
private_key=storage/ca.key certificate=storage/ca/ca.crt
certificate=storage/ca.crt new_certs_dir=storage/ca/certs/
new_certs_dir=storage/certs/ database=storage/ca/database
database=storage/database
default_md=sha256 default_md=sha256
policy=policy_only_commonname policy=policy_only_commonname
serial=storage/serial serial=storage/ca/serial
crlnumber=storage/crl_serial crlnumber=storage/ca/crl_serial
default_crl_days=1 default_crl_days=1
[policy_only_commonname] [policy_only_commonname]
countryName = optional countryName = optional
@ -32,14 +31,14 @@ subjectKeyIdentifier=hash
authorityKeyIdentifier = keyid,issuer:always authorityKeyIdentifier = keyid,issuer:always
extendedKeyUsage = serverAuth extendedKeyUsage = serverAuth
keyUsage = digitalSignature,keyEncipherment keyUsage = digitalSignature,keyEncipherment
crlDistributionPoints = URI:http://{{ hostname }}/crl crlDistributionPoints = URI:http://{{ host }}/crl
[client_ext] [client_ext]
subjectKeyIdentifier=hash subjectKeyIdentifier=hash
basicConstraints = CA:FALSE basicConstraints = CA:FALSE
crlDistributionPoints = URI:http://{{ hostname }}/crl crlDistributionPoints = URI:http://{{ host }}/crl
[ca_ext] [ca_ext]
basicConstraints = CA:TRUE basicConstraints = CA:TRUE
subjectKeyIdentifier=hash subjectKeyIdentifier=hash
crlDistributionPoints = URI:http://{{ hostname }}/crl crlDistributionPoints = URI:http://{{ host }}/crl
[crl_ext] [crl_ext]
authorityKeyIdentifier=keyid:always authorityKeyIdentifier=keyid:always

@ -0,0 +1,27 @@
client
dev zerooo
dev-type tun
proto {{ server.getProtocol() }}
remote {{ server.getExternalIp() }} {{ server.getPort() }}
resolv-retry infinite
nobind
user nobody
group nogroup
persist-key
persist-tun
ca ca.crt
cert client.crt
key client.key
remote-cert-tls server
cipher BF-CBC
comp-lzo
verb 3

@ -0,0 +1,32 @@
port {{ server.getPort() }}
proto {{ server.getProtocol() }}
dev ovpn0
dev-type tun
ca ca.crt
cert server.crt
key server.key
dh dh2048.pem
server {{ server.getInternalIp() }} {{ server.getNetmaskIp() }}
ifconfig-pool-persist ipp.txt
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS {{ server.getFirstDns() }}"
push "dhcp-option DNS {{ server.getSecondDns() }}"
keepalive 10 120
cipher BF-CBC
crl-verify crl.pem
comp-lzo
user nobody
group nogroup
persist-key
persist-tun

@ -0,0 +1,80 @@
{% extends "base.html.twig" %}
{% block content %}
<div class="container">
<div class="col-md-offset-3 col-md-6">
<div class="row">
<div class="col-md-12">
<div class="row">
<h2>Welcome to your Zer.ooo install</h2>
</div>
<div class="row">
<h3>Checking for extensions and settings</h3>
</div>
<div class="row">
<table class="table">
<tbody>
<tr>
<td>{{ hasExecEnabled ? 'Yes' : 'No' }}</td>
<td>Is <kbd>exec</kbd> available?</td>
</tr>
<tr>
<td>{{ hasOpenSsl ? 'Yes' : 'No' }}</td>
<td>Is the <kbd>openssl</kbd> binary available?</td>
</tr>
<tr>
<td>{{ hasOpenSslExtension ? 'Yes' : 'No' }}</td>
<td>Is the openssl module loaded?</td>
</tr>
<tr>
<td>{{ hasZipExtension ? 'Yes' : 'No' }}</td>
<td>Is the zip module loaded?</td>
</tr>
</tbody>
</table>
</div>
{% if not (hasExecEnabled and hasOpenSslExtension and hasOpenSsl and hasZipExtension) %}
<div class="row">
Those functions are essential for the functionality of Zer.ooo, please enable them<br>
<br>
<a href="/install" class="btn btn-primary">Refresh</a>
</div>
{% else %}
<div class="row">
<h3>Create your superuser</h3>
</div>
<form method="post" action="/install">
<div class="row">
<div class="form-group">
<label for="username">Username</label>
<input id="username" class="form-control" type="text" name="username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input id="password" class="form-control" type="password" name="password">
</div>
</div>
<div class="row">
<h3>Server details</h3>
</div>
<div class="row">
<div class="form-group">
<label for="ca-cn">CA Common Name</label>
<input type="text" id="ca-cn" name="ca-cn" class="form-control" value="{{ hostname }}" />
</div>
<div class="form-group">
<label for="domainWithPort">HTTP Host (NOT HTTPS! needed for CRL)</label>
<input type="text" id="domainWithPort" name="domainWithPort" class="form-control" value="{{ hostnameWithPort }}" />
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary pull-right">Finish install</button>
</div>
</div>
</form>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

@ -8,7 +8,9 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h2 id="certificates">Certificates</h2> <div class="row">
<h2 id="certificates">Certificates</h2>
</div>
<div class="row"> <div class="row">
<table class="table"> <table class="table">
<thead> <thead>
@ -53,59 +55,22 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<h2>Servers</h2> <div class="row">
<table class="table"> <h2>Servers</h2>
<thead> </div>
<tr> <div class="row">
<th>Hostname</th>
<th>Fingerprint</th>
<th>Location</th>
<th>Speed</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for server in servers %}\
<tr>
<td>
{{ server.getFqdn() }}
</td>
<td>
{{ server.getFingerprint() }}
</td>
<td>
{{ server.getLocation() }}
</td>
<td>
{{ server.getSpeed() }} MB/s
</td>
<td>
<div class="pull-right">
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5">
There don't seem to be any servers yet :(
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if user.getSuperuser() %}
<h2>Registered servers</h2>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Hostname</th> <th>Hostname</th>
<th>Fingerprint</th> <th>Fingerprint</th>
<th>Location</th>
<th>Speed</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for server in registeredServers %} {% for server in servers %}
<tr> <tr>
<td> <td>
{{ server.getFqdn() }} {{ server.getFqdn() }}
@ -113,9 +78,35 @@
<td> <td>
{{ server.getFingerprint() }} {{ server.getFingerprint() }}
</td> </td>
<td>
{{ server.getLocation() }}
</td>
<td>
{{ server.getSpeed() }} MB/s
</td>
<td> <td>
<div class="pull-right"> <div class="pull-right">
<a href="/panel/server/{{ server.getFingerprint() }}" class="btn btn-default">Details</a> <div class="btn-group">
<a href="/panel/server/{{ server.getFingerprint() }}/config" class="btn btn-default">Download config</a>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
{% for certificate in user.getCertificates() if certificate.getRevoked() == 0 %}
<li>
<a href="/panel/server/{{ server.getFingerprint() }}/config/{{ certificate.getName() }}">Download config for "{{ certificate.getName() }}"</a>
</li>
{% else %}
<li>
<a>You dont have any certificates</a>
</li>
{% endfor %}
</ul>
</div>
{% if user.isSuperUser() %}
<a href="/panel/server/{{ server.getFingerprint() }}" class="btn btn-default">Edit</a>
{% endif %}
</div> </div>
</td> </td>
</tr> </tr>
@ -128,6 +119,46 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
{% if user.getSuperuser() %}
<div class="row">
<h2>Registered servers</h2>
</div>
<div class="row">
<table class="table">
<thead>
<tr>
<th>Hostname</th>
<th>Fingerprint</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for server in registeredServers %}
<tr>
<td>
{{ server.getFqdn() }}
</td>
<td>
{{ server.getFingerprint() }}
</td>
<td>
<div class="pull-right">
<button data-fingerprint="{{ server.getFingerprint() }}" class="remove btn btn-warning">Remove</button>
<a href="/panel/server/{{ server.getFingerprint() }}" class="btn btn-default">Edit</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5">
There don't seem to be any servers yet :(
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %} {% endif %}
</div> </div>
<div class="modal fade in revoke-modal" tabindex="-1" role="dialog"> <div class="modal fade in revoke-modal" tabindex="-1" role="dialog">

@ -83,7 +83,9 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="pull-right"> <div class="pull-right">
<button type="button" class="btn save">Save</button> <button type="button" class="btn save">Save</button>
<button type="button" class="btn btn-primary save-and-sign">Sign and save</button> {% if server.getStatus() == 'registered' %}
<button type="button" class="btn btn-primary save-and-sign">Sign and save</button>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

Loading…
Cancel
Save