Merge branch 'sshtunnel' into develop

feature/node-rewrite
Sven Slootweg 12 years ago
commit a150a133c4

@ -0,0 +1,109 @@
#!/usr/bin/env python
import sys, os
import subprocess
import json, urlparse
import SocketServer, SimpleHTTPServer
from optparse import OptionParser
parser = OptionParser()
(options, cmdargs) = parser.parse_args()
try:
f = open("session_key", "r")
session_key = f.read().strip()
f.close()
except IOError, e:
sys.stderr.write("You must specify a session key.\n")
exit(1)
os.remove("session_key")
class CommandHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def do_GET(self):
global session_key
req = urlparse.urlparse(self.path)
get_params = urlparse.parse_qs(req.query)
path = req.path
if path=='/':
try:
command = json.loads(get_params['command'][0])
except KeyError, e:
self.send_404()
return
except IndexError, e:
self.send_404()
return
except ValueError, e:
self.send_404()
return
try:
key = get_params['key'][0]
except KeyError, e:
self.send_403()
return
except IndexError, e:
self.send_403()
return
if key != session_key:
self.send_403()
return
try:
result = json.dumps(self.run_command(command))
except Exception, e:
print e
self.send_404()
return
self.send_response(200)
self.send_header('Content-type','text/json')
self.end_headers()
self.wfile.write(result)
else:
self.send_404()
return
def send_404(self):
self.send_response(404)
self.send_header('Content-type','text/plain')
self.end_headers()
self.wfile.write("404 Not Found")
def send_403(self):
self.send_response(403)
self.send_header('Content-type','text/plain')
self.end_headers()
self.wfile.write("403 Forbidden")
def run_command(self, args):
pr = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
data = pr.communicate()
pr.wait()
return {
'stdout': data[0],
'stderr': data[1],
'returncode': pr.returncode
}
if os.fork(): exit(0)
os.umask(0)
os.setsid()
if os.fork(): exit(0)
sys.stdout.flush()
sys.stderr.flush()
si = file('/dev/null', 'r')
so = file('/dev/null', 'a+')
se = file('/dev/null', 'a+', 0)
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
httpd = SocketServer.ThreadingTCPServer(("localhost", 3434), CommandHandler)
httpd.serve_forever()

@ -0,0 +1,28 @@
import sys, os, subprocess
stfu = open("/dev/null", "w")
def run_command(args):
pr = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
data = pr.communicate()
pr.wait()
return pr.returncode
host = sys.argv[1]
user = sys.argv[2]
port = int(sys.argv[3])
keyfile = sys.argv[4]
session_key = sys.argv[5]
if run_command(["ssh", "%s@%s" % (user, host), "-o", "UserKnownHostsFile=/etc/cvm/knownhosts", "-o", "StrictHostKeyChecking=no", "-i", keyfile, "cd /etc/cvm/command_daemon; echo '%s' > session_key && ./command_daemon" % session_key]) == 0:
# Make autossh verify the connection is still alive every 10 seconds.
os.environ["AUTOSSH_POLL"] = 10
os.environ["AUTOSSH_FIRST_POLL"] = 10
if run_command(["autossh", "-f", "-i", keyfile, "-M", str(port + 1), "-o", "UserKnownHostsFile=/etc/cvm/knownhosts", "-o", "StrictHostKeyChecking=no", "%s@%s" % (user, host), "-L", "%s:localhost:3434" % port, "-N"]) == 0:
sys.stdout.write("Tunnel established.\n");
exit(0)
else:
sys.stderr.write("Failed to establish tunnel.\n")
else:
sys.stderr.write("Failed to start daemon.\n")

@ -26,10 +26,12 @@ class Node extends CPHPDatabaseRecordClass
'PhysicalLocation' => "PhysicalLocation",
'PrivateKey' => "CustomPrivateKey",
'PublicKey' => "CustomPublicKey",
'User' => "User"
'User' => "User",
'TunnelKey' => "TunnelKey"
),
'numeric' => array(
'Port' => "Port"
'Port' => "Port",
'TunnelPort' => "TunnelPort"
),
'boolean' => array(
'HasCustomKey' => "HasCustomKey"
@ -47,6 +49,9 @@ class Node extends CPHPDatabaseRecordClass
$this->ssh->host = $this->sHostname;
$this->ssh->port = $this->sPort;
$this->ssh->user = $this->sUser;
$this->ssh->tunnel_port = $this->sTunnelPort;
$this->ssh->tunnel_key = $this->sTunnelKey;
$this->ssh->node = $this;
if($this->HasCustomKey === true)
{

@ -35,15 +35,6 @@ class SshConnector extends CPHPBaseClass
{
try
{
if($this->failed == false && ($this->connected == false || $this->authenticated == false))
{
$this->Connect();
}
elseif($this->failed == true)
{
throw new SshConnectException("Previous connection attempt failed.");
}
return $this->DoCommand($command, $throw_exception);
}
catch (SshConnectException $e)
@ -78,81 +69,247 @@ class SshConnector extends CPHPBaseClass
public function Connect()
{
$fp = @fsockopen($this->host, $this->port, $errno, $errstr, 3);
if(!$fp)
/* TODO: TIME_WAIT status for a previous socket on the same port may cause issues
* when attempting to restart the command daemon. There is currently no way
* to detect this from the code, and it makes all subsequent requests fail
* (silently?) because the tunnel is available but nothing is listening on
* the other end. This kind of edge case should be detected and dealt with.
* A browser displays a 'no data received' error in this case. */
if($this->failed)
{
throw new SshConnectException("Could not connect to {$this->host}:{$this->port}: {$errstr}");
throw new SshConnectException("A previous connection attempt failed.");
}
fclose($fp);
$sHost = escapeshellarg($this->host);
$sUser = escapeshellarg($this->user);
$sPort = $this->tunnel_port = $this->node->uTunnelPort = $this->ChoosePort();
$sKeyFile = escapeshellarg($this->key);
$this->node->uTunnelKey = $this->tunnel_key = random_string(16);
$sSessionKey = escapeshellarg($this->node->uTunnelKey);
$command = "python /etc/cvm/start_tunnel.py {$sHost} {$sUser} {$sPort} {$sKeyFile} {$sSessionKey}";
$options = array(
'hostkey' => $this->keytype
);
cphp_debug_snapshot(array(
"action" => "start tunnel",
"db-tunnelkey" => $this->node->sTunnelKey,
"db-utunnelkey" => $this->node->uTunnelKey,
"ssh-tunnelkey" => $this->tunnel_key,
"arg-tunnelkey" => $sSessionKey
));
if($this->connection = @ssh2_connect($this->host, $this->port, $options))
exec($command, $output, $returncode);
if($returncode === 0)
{
$this->connected = true;
/* autossh returns before the SSH connection has actually been established. We'll make the
* script sleep until a connection has been established, with a timeout of 10 seconds, after
* which an exception is raised. The polling interval is 100ms. */
$start_time = time();
if(empty($this->passphrase))
while(true)
{
$result = @ssh2_auth_pubkey_file($this->connection, $this->user, $this->pubkey, $this->key);
if(time() > $start_time + 10)
{
throw new SshConnectException("The SSH tunnel could not be fully established within the timeout period.");
}
if($pollsock = @fsockopen("localhost", $this->tunnel_port, $errno, $errstr, 1))
{
/* The tunnel has been fully established. */
fclose($pollsock);
break;
}
usleep(100000);
}
else
cphp_debug_snapshot(array(
"action" => "pre insert db",
"db-tunnelkey" => $this->node->sTunnelKey,
"db-utunnelkey" => $this->node->uTunnelKey,
"ssh-tunnelkey" => $this->tunnel_key,
"arg-tunnelkey" => $sSessionKey
));
$this->node->InsertIntoDatabase();
cphp_debug_snapshot(array(
"action" => "inserted db",
"db-tunnelkey" => $this->node->sTunnelKey,
"db-utunnelkey" => $this->node->uTunnelKey,
"ssh-tunnelkey" => $this->tunnel_key,
"arg-tunnelkey" => $sSessionKey
));
return true;
}
else
{
throw new SshConnectException("Could not establish tunnel to {$this->host}:{$this->port}.");
}
}
private function ChoosePort()
{
try
{
$sPorts = array();
foreach(Node::CreateFromQuery("SELECT * FROM nodes WHERE `TunnelPort` != 0") as $node)
{
$result = @ssh2_auth_pubkey_file($this->connection, $this->user, $this->pubkey, $this->key, $this->passphrase);
$sPorts[] = $node->sTunnelPort;
$sPorts[] = $node->sTunnelPort + 1;
$sPorts[] = $node->sTunnelPort + 2;
}
if($result === true)
/* TODO: Figure out a more intelligent way of choosing ports. */
$start = max($sPorts) + 1;
}
catch (NotFoundException $e)
{
$start = 2000;
}
$current = $start;
while(true)
{
if($current > 65534)
{
/* TODO: We really need to deal with this properly... */
throw new SshConnectException("No free tunnel ports left.");
}
if(!$this->TestPort($current))
{
$this->authenticated = true;
return true;
if(!$this->TestPort($current + 1))
{
if(!$this->TestPort($current + 2))
{
break;
}
else
{
$current = $current + 3;
}
}
else
{
$current = $current + 2;
}
}
else
{
throw new SshAuthException("Could not connect to {$this->host}:{$this->port}: Key authentication failed");
$current = $current + 1;
}
}
return $current;
}
private function TestPort($port)
{
if($testsock = @fsockopen("localhost", $port, $errno, $errstr, 1))
{
fclose($testsock);
return true;
}
else
{
throw new SshConnectException("Could not connect to {$this->host}:{$this->port}: {$error}");
return false;
}
return false;
}
private function DoCommand($command, $throw_exception)
private function DoCommand($command, $throw_exception, $allow_retry = true)
{
$command = base64_encode(json_encode($command));
$command = "{$this->helper} {$command}";
cphp_debug_snapshot(array(
"action" => "pre run command",
"db-tunnelkey" => $this->node->sTunnelKey,
"db-utunnelkey" => $this->node->uTunnelKey,
"ssh-tunnelkey" => $this->tunnel_key,
"command" => $command,
"allow-retry" => $allow_retry
));
$stream = ssh2_exec($this->connection, $command);
$error_stream = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR);
$cmd = urlencode(json_encode($command));
$url = "http://localhost:{$this->tunnel_port}/?key={$this->tunnel_key}&command={$cmd}";
stream_set_blocking($stream, true);
stream_set_blocking($error_stream, true);
$context = stream_context_create(array(
'http' => array(
'timeout' => 2.0
)
));
$result = stream_get_contents($stream);
$error = stream_get_contents($error_stream);
$response = @file($url, 0, $context);
if(strpos($error, "No such file or directory") !== false)
{
throw new Exception("The runhelper is not installed on the node or an error occurred.");
}
cphp_debug_snapshot(array(
"action" => "post run command",
"db-tunnelkey" => $this->node->sTunnelKey,
"db-utunnelkey" => $this->node->uTunnelKey,
"ssh-tunnelkey" => $this->tunnel_key,
"command" => $command,
"allow-retry" => $allow_retry,
"response" => $response
));
$returndata = json_decode($result);
if($response === false)
{
/* Determine why the connection failed, and what we need to do to fix it. */
if($testsock = @fsockopen("localhost", $this->tunnel_port, $errno, $errstr, 1))
{
/* The socket works fine. */
fclose($testsock);
$returndata->stderr = trim($returndata->stderr);
/* Since the socket works but we can't make a request, there is most
* likely a serious problem with the command daemon (stuck, crashed,
* etc.) We'll throw an exception. TODO: Log error. */
$this->failed = true;
throw new SshCommandException("The command daemon is unavailable.");
}
else
{
/* The tunnel is gone for some reason. Either the connection broke
* and autossh is busy reconnecting, or autossh broke entirely. We
* will attempt to connect to the monitoring port to see if autossh
* is still running or not. */
if($testsock = @fsockopen("localhost", ($this->tunnel_port + 2), $errno, $errstr, 1))
{
/* The socket works fine. */
fclose($testsock);
fclose($stream);
fclose($error_stream);
/* Most likely autossh is very busy trying to reconnect to the node. We'll throw a
* connection exception for now. TODO: Consider waiting with a specified timeout. */
$this->failed = true;
throw new SshConnectException("The SSH connection to this node is currently unavailable.");
}
else
{
if($allow_retry)
{
$this->Connect();
$res = $this->DoCommand($command, $throw_exception, false);
}
else
{
$this->failed = true;
throw new SshConnectException("Could not connect to node.");
/* TODO: Log error, this is probably very bad. */
}
}
}
}
else
{
$response = json_decode(implode("", $response));
}
if($returndata->returncode != 0 && $throw_exception === true)
if($response->returncode != 0 && $throw_exception === true)
{
throw new SshExitException("Non-zero exit code returned: {$returndata->stderr}", $returndata->returncode);
throw new SshExitException("Non-zero exit code returned: {$response->stderr}", $response->returncode);
}
return $returndata;
return $response;
}
}

@ -100,7 +100,7 @@ class Vps extends CPHPDatabaseRecordClass
{
try
{
$command = array("vzctl", "status", $this->sInternalId);
$command = array("sudo", "vzctl", "status", $this->sInternalId);
$result = $this->sNode->ssh->RunCommandCached($command, false);
@ -276,7 +276,7 @@ class Vps extends CPHPDatabaseRecordClass
{
if(is_array($options))
{
$command_elements = array("vzctl", "set", $this->sInternalId);
$command_elements = array("sudo", "vzctl", "set", $this->sInternalId);
foreach($options as $key => $value)
{
@ -296,12 +296,12 @@ class Vps extends CPHPDatabaseRecordClass
public function RunCommand($command, $throw_exception = false)
{
return $this->sNode->ssh->RunCommand(array("vzctl", "exec", $this->sInternalId, $command), $throw_exception);
return $this->sNode->ssh->RunCommand(array("sudo", "vzctl", "exec", $this->sInternalId, $command), $throw_exception);
}
public function RunCommandCached($command, $throw_exception = false)
{
return $this->sNode->ssh->RunCommandCached(array("vzctl", "exec", $this->sInternalId, $command), $throw_exception);
return $this->sNode->ssh->RunCommandCached(array("sudo", "vzctl", "exec", $this->sInternalId, $command), $throw_exception);
}
public function Deploy($conf = array())
@ -311,7 +311,7 @@ class Vps extends CPHPDatabaseRecordClass
$this->uRootPassword = $sRootPassword;
$this->InsertIntoDatabase();
$command = array("vzctl", "create", $this->sInternalId, "--ostemplate", $this->sTemplate->sTemplateName);
$command = array("sudo", "vzctl", "create", $this->sInternalId, "--ostemplate", $this->sTemplate->sTemplateName);
$result = $this->sNode->ssh->RunCommand($command, false);
$result->returncode = 0;
@ -347,7 +347,7 @@ class Vps extends CPHPDatabaseRecordClass
$sDCacheLimit = (isset($conf['sDCacheLimit'])) ? $conf['sDCacheLimit'] : (int)($sDCache * 1.1);
$sAvgProc = (isset($conf['sAvgProc'])) ? $conf['sAvgProc'] : $dummy_processes / 2;
$command = array("vzctl", "set", $this->sInternalId,
$command = array("sudo", "vzctl", "set", $this->sInternalId,
"--onboot", "yes",
"--setmode", "restart",
"--hostname", $this->sHostname,
@ -441,7 +441,7 @@ class Vps extends CPHPDatabaseRecordClass
$this->Stop();
}
$command = array("vzctl", "destroy", $this->sInternalId);
$command = array("sudo", "vzctl", "destroy", $this->sInternalId);
$result = $this->sNode->ssh->RunCommand($command, false);
if($result->returncode == 0)
@ -487,7 +487,7 @@ class Vps extends CPHPDatabaseRecordClass
}
else
{
$command = array("vzctl", "start", $this->sInternalId);
$command = array("sudo", "vzctl", "start", $this->sInternalId);
$result = $this->sNode->ssh->RunCommand($command, false);
if($result->returncode == 0)
@ -515,7 +515,7 @@ class Vps extends CPHPDatabaseRecordClass
}
else
{
$command = array("vzctl", "stop", $this->sInternalId);
$command = array("sudo", "vzctl", "stop", $this->sInternalId);
$result = $this->sNode->ssh->RunCommand($command, false);
/* vzctl is retarded enough to return code 0 when the command fails because the container isn't running,
@ -577,7 +577,7 @@ class Vps extends CPHPDatabaseRecordClass
public function AddIp($ip)
{
$command = array("vzctl", "set", $this->sInternalId, "--ipadd", $ip, "--save");
$command = array("sudo", "vzctl", "set", $this->sInternalId, "--ipadd", $ip, "--save");
$result = $this->sNode->ssh->RunCommand($command, false);
@ -593,7 +593,7 @@ class Vps extends CPHPDatabaseRecordClass
public function RemoveIp($ip)
{
$command = array("vzctl", "set", $this->sInternalId, "--ipdel", $ip, "--save");
$command = array("sudo", "vzctl", "set", $this->sInternalId, "--ipdel", $ip, "--save");
$result = $this->sNode->ssh->RunCommand($command, false);
@ -611,7 +611,7 @@ class Vps extends CPHPDatabaseRecordClass
{
/* TODO: Don't rely on grep, and parse the output in this function itself. Also try to find another way to do this without relying
* on the container at all. */
$result = $this->sNode->ssh->RunCommand(array("vzctl", "exec", $this->sInternalId, "cat /proc/net/dev | grep venet0"), false);
$result = $this->sNode->ssh->RunCommand(array("sudo", "vzctl", "exec", $this->sInternalId, "cat /proc/net/dev | grep venet0"), false);
if($result->returncode == 0)
{
@ -668,6 +668,6 @@ class Vps extends CPHPDatabaseRecordClass
public function EnableTunTap()
{
/* TODO: Finish EnableTunTap function, check whether tun module is available on host */
$command = array("vzctl", "set", $this->sInternalId, "--devnodes", "net/tun:rw", "--save");
$command = array("sudo", "vzctl", "set", $this->sInternalId, "--devnodes", "net/tun:rw", "--save");
}
}

@ -17,6 +17,8 @@ try
{
$sNode = new Node($router->uParameters[1]);
$sVpsList = array();
if($result = $database->CachedQuery("SELECT * FROM containers WHERE `NodeId` = :NodeId", array(":NodeId" => $sNode->sId)))
{
foreach($result->data as $row)

@ -172,18 +172,23 @@ foreach($result->data as $row)
);
}
$result = $database->CachedQuery("SELECT * FROM templates WHERE `Available` = 1");
$sTemplates = array();
foreach($result->data as $row)
if($result = $database->CachedQuery("SELECT * FROM templates WHERE `Available` = 1"))
{
$sTemplate = new Template($row);
foreach($result->data as $row)
{
$sTemplate = new Template($row);
$sTemplates[] = array(
'id' => $sTemplate->sId,
'name' => $sTemplate->sName
);
$sTemplates[] = array(
'id' => $sTemplate->sId,
'name' => $sTemplate->sName
);
}
}
else
{
/* TODO: Show an error when no templates are available. */
}
$result = $database->CachedQuery("SELECT * FROM users WHERE `AccessLevel` > 0");

@ -15,36 +15,37 @@ if(!isset($_CVM)) { die("Unauthorized."); }
if($sLoggedIn === true)
{
$result = $database->CachedQuery("SELECT * FROM containers WHERE `UserId` = :UserId", array(":UserId" => $sUser->sId));
$sVpsList = array();
foreach($result->data as $row)
if($result = $database->CachedQuery("SELECT * FROM containers WHERE `UserId` = :UserId", array(":UserId" => $sUser->sId)))
{
$sVps = new Vps($row);
try
{
$sStatus = $sVps->sStatusText;
}
catch (SshException $e)
foreach($result->data as $row)
{
$sStatus = "unknown";
}
$sVps = new Vps($row);
try
{
$sStatus = $sVps->sStatusText;
}
catch (SshException $e)
{
$sStatus = "unknown";
}
$sVpsList[] = array(
'id' => $sVps->sId,
'hostname' => $sVps->sHostname,
'node' => $sVps->sNode->sName,
'node-hostname' => $sVps->sNode->sHostname,
'template' => $sVps->sTemplate->sName,
'diskspace' => number_format($sVps->sDiskSpace / 1024),
'diskspace-unit' => "GB",
'guaranteed-ram' => $sVps->sGuaranteedRam,
'guaranteed-ram-unit' => "MB",
'status' => $sStatus,
'virtualization-type' => $sVps->sVirtualizationType
);
$sVpsList[] = array(
'id' => $sVps->sId,
'hostname' => $sVps->sHostname,
'node' => $sVps->sNode->sName,
'node-hostname' => $sVps->sNode->sHostname,
'template' => $sVps->sTemplate->sName,
'diskspace' => number_format($sVps->sDiskSpace / 1024),
'diskspace-unit' => "GB",
'guaranteed-ram' => $sVps->sGuaranteedRam,
'guaranteed-ram-unit' => "MB",
'status' => $sStatus,
'virtualization-type' => $sVps->sVirtualizationType
);
}
}
$sMainContents = Templater::AdvancedParse("{$sTheme}/client/vps/list", $locale->strings, array(
@ -53,5 +54,5 @@ if($sLoggedIn === true)
}
else
{
throw new UnauthorizedException("You must be logged in to view this page.");
redirect("/login");
}

@ -239,7 +239,7 @@ try
if(empty($router->uVariables['raw']))
{
if($router->uVariables['menu'] == "vps" && $router->uVariables['display_menu'] === true)
if(isset($router->uVariables['menu']) && $router->uVariables['menu'] == "vps" && $router->uVariables['display_menu'] === true)
{
$sMainContents .= Templater::AdvancedParse("{$sTheme}/client/vps/main", $locale->strings, array(
'error' => $sError,
@ -247,7 +247,7 @@ try
'id' => $sVps->sId
));
}
elseif($router->uVariables['menu'] == "admin" && $router->uVariables['display_menu'] === true)
elseif(isset($router->uVariables['menu']) && $router->uVariables['menu'] == "admin" && $router->uVariables['display_menu'] === true)
{
$sMainContents .= Templater::AdvancedParse("{$sTheme}/admin/main", $locale->strings, array(
'error' => $sError,

@ -1,6 +1,7 @@
cd slave
echo "Copying needed files for slave SFX..."
cp ../../runhelper/runhelper .
cp ../../command_daemon/command_daemon .
cp ../../console/slave/dropper .
cp ../../logshell/logshell .
cp ../../logshell/cvmshell .

@ -229,12 +229,17 @@ sys.stdout.write("Lock on /etc/group removed.\n")
# Create the main CVM data directories
setuplib.create_directory("/etc/cvm", True, 0, cvm_gid, "u+rwx g+rwx o+rx")
setuplib.create_directory("/etc/cvm/log", True, 0, 0, "u+rwx")
setuplib.create_directory("/etc/cvm/command_daemon", True, cvm_uid, cvm_gid, "u+rwx")
sys.stdout.write("Created directories.\n")
# Copy the runhelper
setuplib.copy_file("runhelper", "/home/cvm/runhelper", True, cvm_uid, cvm_gid, "u+rwx")
sys.stdout.write("Installed runhelper.\n")
# Copy the command daemon
setuplib.copy_file("command_daemon", "/etc/cvm/command_daemon/command_daemon", True, cvm_uid, cvm_gid, "u+rwx")
sys.stdout.write("Installed command daemon.\n")
if enable_dropper == "y":
# Copy the shell dropper
setuplib.copy_file("dropper", "/home/vz/dropper", True, vz_uid, vz_gid, "u+rwx")

Loading…
Cancel
Save