- no dockerhub

- trafic masking
This commit is contained in:
pokamest 2021-04-04 23:12:36 +03:00
parent 059c6404ab
commit 85b6b06cc9
31 changed files with 1106 additions and 256 deletions

View file

@ -9,24 +9,20 @@
#include <QJsonObject>
#include <QJsonDocument>
#include <QApplication>
#include <QTemporaryFile>
#include "sftpchannel.h"
#include "sshconnectionmanager.h"
#include "protocols/protocols_defs.h"
#include "server_defs.h"
#include "scripts_registry.h"
#include "utils.h"
using namespace QSsh;
QString ServerController::getContainerName(DockerContainer container)
{
switch (container) {
case(DockerContainer::OpenVpn): return "amnezia-openvpn";
case(DockerContainer::ShadowSocks): return "amnezia-shadowsocks";
default: return "";
}
}
ErrorCode ServerController::runScript(const QHash<QString, QString> &vars,
const SshConnectionParameters &sshParams, QString script,
ErrorCode ServerController::runScript(const SshConnectionParameters &sshParams, QString script,
const std::function<void(const QString &, QSharedPointer<SshRemoteProcess>)> &cbReadStdOut,
const std::function<void(const QString &, QSharedPointer<SshRemoteProcess>)> &cbReadStdErr)
{
@ -39,21 +35,36 @@ ErrorCode ServerController::runScript(const QHash<QString, QString> &vars,
qDebug() << "Run script";
QString totalLine;
const QStringList &lines = script.split("\n", QString::SkipEmptyParts);
for (int i = 0; i < lines.count(); i++) {
QString line = lines.at(i);
QString currentLine = lines.at(i);
QString nextLine;
if (i + 1 < lines.count()) nextLine = lines.at(i+1);
for (const QString &var : vars.keys()) {
//qDebug() << "Replacing" << var << vars.value(var);
line.replace(var, vars.value(var));
if (totalLine.isEmpty()) {
totalLine = currentLine;
}
else {
totalLine = totalLine + "\n" + currentLine;
}
if (line.startsWith("#")) {
QString lineToExec;
if (currentLine.endsWith("\\")) {
continue;
}
else {
lineToExec = totalLine;
totalLine.clear();
}
// Run collected line
if (totalLine.startsWith("#")) {
continue;
}
qDebug().noquote() << "EXEC" << line;
QSharedPointer<SshRemoteProcess> proc = client->createRemoteProcess(line.toUtf8());
qDebug().noquote() << "EXEC" << lineToExec;
QSharedPointer<SshRemoteProcess> proc = client->createRemoteProcess(lineToExec.toUtf8());
if (!proc) {
qCritical() << "Failed to create SshRemoteProcess, breaking.";
@ -103,61 +114,106 @@ ErrorCode ServerController::runScript(const QHash<QString, QString> &vars,
return ErrorCode::NoError;
}
ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container,
const ServerCredentials &credentials, QString &file, const QString &path)
ErrorCode ServerController::installDocker(const ServerCredentials &credentials)
{
QString script = QString("sudo docker exec -i %1 sh -c \"echo \'%2\' > %3\"").
arg(getContainerName(container)).arg(file).arg(path);
// Setup openvpn part
QString scriptData = amnezia::scriptData(SharedScriptType::install_docker);
if (scriptData.isEmpty()) return ErrorCode::InternalError;
// qDebug().noquote() << "uploadTextFileToContainer\n" << script;
QString stdOut;
auto cbReadStdOut = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
stdOut += data + "\n";
SshConnection *client = connectToHost(sshParams(credentials));
if (client->state() != SshConnection::State::Connected) {
return fromSshConnectionErrorCode(client->errorState());
}
if (data.contains("Automatically restart Docker daemon?")) {
proc->write("yes\n");
}
};
auto cbReadStdErr = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> ) {
stdOut += data + "\n";
};
QSharedPointer<SshRemoteProcess> proc = client->createRemoteProcess(script.toUtf8());
return runScript(sshParams(credentials),
replaceVars(scriptData, genVarsForScript(credentials, DockerContainer::OpenVpnOverCloak)),
cbReadStdOut, cbReadStdErr);
}
if (!proc) {
qCritical() << "Failed to create SshRemoteProcess, breaking.";
return ErrorCode::SshRemoteProcessCreationError;
}
ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container,
const ServerCredentials &credentials, const QString &file, const QString &path)
{
ErrorCode e;
QString tmpFileName = QString("/tmp/%1.tmp").arg(Utils::getRandomString(16));
uploadFileToHost(credentials, file.toUtf8(), tmpFileName);
QEventLoop wait;
int exitStatus = -1;
e = runScript(sshParams(credentials),
replaceVars(QString("sudo docker cp %1 $CONTAINER_NAME:/%2").arg(tmpFileName).arg(path),
genVarsForScript(credentials, container)));
// QObject::connect(proc.data(), &SshRemoteProcess::started, &wait, [](){
// qDebug() << "uploadTextFileToContainer started";
if (e) return e;
runScript(sshParams(credentials),
replaceVars(QString("sudo shred %1").arg(tmpFileName),
genVarsForScript(credentials, container)));
runScript(sshParams(credentials),
replaceVars(QString("sudo rm %1").arg(tmpFileName),
genVarsForScript(credentials, container)));
return e;
// QString script = QString("sudo docker exec -i %1 sh -c \"echo \'%2\' > %3\"").
// arg(amnezia::server::getContainerName(container)).arg(file).arg(path);
// qDebug().noquote() << "uploadTextFileToContainer\n" << script;
// SshConnection *client = connectToHost(sshParams(credentials));
// if (client->state() != SshConnection::State::Connected) {
// return fromSshConnectionErrorCode(client->errorState());
// }
// QSharedPointer<SshRemoteProcess> proc = client->createRemoteProcess(script.toUtf8());
// if (!proc) {
// qCritical() << "Failed to create SshRemoteProcess, breaking.";
// return ErrorCode::SshRemoteProcessCreationError;
// }
// QEventLoop wait;
// int exitStatus = -1;
//// QObject::connect(proc.data(), &SshRemoteProcess::started, &wait, [](){
//// qDebug() << "uploadTextFileToContainer started";
//// });
// QObject::connect(proc.data(), &SshRemoteProcess::closed, &wait, [&](int status){
// //qDebug() << "Remote process exited with status" << status;
// exitStatus = status;
// wait.quit();
// });
QObject::connect(proc.data(), &SshRemoteProcess::closed, &wait, [&](int status){
//qDebug() << "Remote process exited with status" << status;
exitStatus = status;
wait.quit();
});
// QObject::connect(proc.data(), &SshRemoteProcess::readyReadStandardOutput, [proc](){
// qDebug().noquote() << proc->readAllStandardOutput();
// });
QObject::connect(proc.data(), &SshRemoteProcess::readyReadStandardOutput, [proc](){
qDebug().noquote() << proc->readAllStandardOutput();
});
// QObject::connect(proc.data(), &SshRemoteProcess::readyReadStandardError, [proc](){
// qDebug().noquote() << proc->readAllStandardError();
// });
QObject::connect(proc.data(), &SshRemoteProcess::readyReadStandardError, [proc](){
qDebug().noquote() << proc->readAllStandardError();
});
// proc->start();
proc->start();
// if (exitStatus < 0) {
// wait.exec();
// }
if (exitStatus < 0) {
wait.exec();
}
return fromSshProcessExitStatus(exitStatus);
// return fromSshProcessExitStatus(exitStatus);
}
QString ServerController::getTextFileFromContainer(DockerContainer container,
const ServerCredentials &credentials, const QString &path, ErrorCode *errorCode)
{
if (errorCode) *errorCode = ErrorCode::NoError;
QString script = QString("sudo docker exec -i %1 sh -c \"cat \'%2\'\"").
arg(getContainerName(container)).arg(path);
arg(amnezia::server::getContainerName(container)).arg(path);
qDebug().noquote() << "Copy file from container\n" << script;
@ -201,28 +257,12 @@ QString ServerController::getTextFileFromContainer(DockerContainer container,
return proc->readAllStandardOutput();
}
ErrorCode ServerController::signCert(DockerContainer container,
const ServerCredentials &credentials, QString clientId)
{
QString script_import = QString("sudo docker exec -i %1 bash -c \"cd /opt/amneziavpn_data && "
"easyrsa import-req /opt/amneziavpn_data/clients/%2.req %2\"")
.arg(getContainerName(container)).arg(clientId);
QString script_sign = QString("sudo docker exec -i %1 bash -c \"export EASYRSA_BATCH=1; cd /opt/amneziavpn_data && "
"easyrsa sign-req client %2\"")
.arg(getContainerName(container)).arg(clientId);
QStringList script {script_import, script_sign};
return runScript(genVarsForScript(credentials, container), sshParams(credentials), script.join("\n"));
}
ErrorCode ServerController::checkOpenVpnServer(DockerContainer container, const ServerCredentials &credentials)
{
QString caCert = ServerController::getTextFileFromContainer(container,
credentials, ServerController::caCertPath());
credentials, amnezia::protocols::openvpn::caCertPath());
QString taKey = ServerController::getTextFileFromContainer(container,
credentials, ServerController::taKeyPath());
credentials, amnezia::protocols::openvpn::taKeyPath());
if (!caCert.isEmpty() && !taKey.isEmpty()) {
return ErrorCode::NoError;
@ -232,6 +272,68 @@ ErrorCode ServerController::checkOpenVpnServer(DockerContainer container, const
}
}
ErrorCode ServerController::uploadFileToHost(const ServerCredentials &credentials, const QByteArray &data, const QString &remotePath)
{
SshConnection *client = connectToHost(sshParams(credentials));
if (client->state() != SshConnection::State::Connected) {
return fromSshConnectionErrorCode(client->errorState());
}
bool err = false;
QEventLoop wait;
QTimer timer;
timer.setSingleShot(true);
timer.start(3000);
QSharedPointer<SftpChannel> sftp = client->createSftpChannel();
sftp->initialize();
QObject::connect(sftp.data(), &SftpChannel::initialized, &wait, [&](){
timer.stop();
wait.quit();
});
QObject::connect(&timer, &QTimer::timeout, &wait, [&](){
err= true;
wait.quit();
});
wait.exec();
if (!sftp) {
qCritical() << "Failed to create SftpChannel, breaking.";
return ErrorCode::SshRemoteProcessCreationError;
}
QTemporaryFile localFile;
localFile.open();
localFile.write(data);
localFile.close();
auto job = sftp->uploadFile(localFile.fileName(), remotePath, QSsh::SftpOverwriteMode::SftpOverwriteExisting);
QObject::connect(sftp.data(), &SftpChannel::finished, &wait, [&](QSsh::SftpJobId j, const QString &error){
if (job == j) {
qDebug() << "Sftp finished with status" << error;
wait.quit();
}
});
QObject::connect(sftp.data(), &SftpChannel::channelError, &wait, [&](const QString &reason){
qDebug() << "Sftp finished with error" << reason;
err= true;
wait.quit();
});
wait.exec();
if (err) {
return ErrorCode::SshSftpError;
}
else {
return ErrorCode::NoError;
}
}
ErrorCode ServerController::fromSshConnectionErrorCode(SshError error)
{
switch (error) {
@ -311,11 +413,16 @@ ErrorCode ServerController::removeServer(const ServerCredentials &credentials, P
scriptData = file.readAll();
if (scriptData.isEmpty()) return ErrorCode::InternalError;
return runScript(genVarsForScript(credentials, container), sshParams(credentials), scriptData);
return runScript(sshParams(credentials), replaceVars(scriptData, genVarsForScript(credentials, container)));
}
ErrorCode ServerController::setupServer(const ServerCredentials &credentials, Protocol proto)
{
ErrorCode e = runScript(sshParams(credentials),
replaceVars(amnezia::scriptData(SharedScriptType::install_docker),
genVarsForScript(credentials)));
if (e) return e;
if (proto == Protocol::OpenVpn) {
return ErrorCode::NoError;
//return setupOpenVpnServer(credentials);
@ -326,9 +433,9 @@ ErrorCode ServerController::setupServer(const ServerCredentials &credentials, Pr
else if (proto == Protocol::Any) {
//return ErrorCode::NotImplementedError;
// TODO: run concurently
//setupOpenVpnServer(credentials);
return setupShadowSocksServer(credentials);
//return setupShadowSocksServer(credentials);
return setupOpenVpnOverCloakServer(credentials);
}
return ErrorCode::NoError;
@ -336,100 +443,177 @@ ErrorCode ServerController::setupServer(const ServerCredentials &credentials, Pr
ErrorCode ServerController::setupOpenVpnServer(const ServerCredentials &credentials)
{
QString scriptData;
QString scriptFileName = ":/server_scripts/setup_openvpn_server.sh";
QFile file(scriptFileName);
if (! file.open(QIODevice::ReadOnly)) return ErrorCode::InternalError;
return ErrorCode::NotImplementedError;
scriptData = file.readAll();
// QString scriptData;
// QString scriptFileName = ":/server_scripts/setup_openvpn_server.sh";
// QFile file(scriptFileName);
// if (! file.open(QIODevice::ReadOnly)) return ErrorCode::InternalError;
// scriptData = file.readAll();
// if (scriptData.isEmpty()) return ErrorCode::InternalError;
// QString stdOut;
// auto cbReadStdOut = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
// stdOut += data + "\n";
// if (data.contains("Automatically restart Docker daemon?")) {
// proc->write("yes\n");
// }
// };
// auto cbReadStdErr = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
// stdOut += data + "\n";
// };
// ErrorCode e = runScript(genVarsForScript(credentials, DockerContainer::OpenVpn), sshParams(credentials), scriptData, cbReadStdOut, cbReadStdErr);
// if (e) return e;
// QApplication::processEvents();
// if (stdOut.contains("port is already allocated")) return ErrorCode::ServerPortAlreadyAllocatedError;
// if (stdOut.contains("Error response from daemon")) return ErrorCode::ServerCheckFailed;
// return checkOpenVpnServer(DockerContainer::OpenVpn, credentials);
}
ErrorCode ServerController::setupOpenVpnOverCloakServer(const ServerCredentials &credentials)
{
ErrorCode e;
DockerContainer container = DockerContainer::OpenVpnOverCloak;
// create folder on host
e = runScript(sshParams(credentials),
replaceVars(amnezia::scriptData(SharedScriptType::prepare_host),
genVarsForScript(credentials, container)));
if (e) return e;
uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, Protocol::OpenVpnOverCloak).toUtf8(),
amnezia::server::getDockerfileFolder(container) + "/Dockerfile");
// Setup openvpn part
QString scriptData = amnezia::scriptData(SharedScriptType::build_container);
if (scriptData.isEmpty()) return ErrorCode::InternalError;
QString stdOut;
auto cbReadStdOut = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
stdOut += data + "\n";
// QString stdOut;
// auto cbReadStdOut = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
// stdOut += data + "\n";
// };
// auto cbReadStdErr = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
// stdOut += data + "\n";
// };
if (data.contains("Automatically restart Docker daemon?")) {
proc->write("yes\n");
}
};
auto cbReadStdErr = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
stdOut += data + "\n";
};
ErrorCode e = runScript(genVarsForScript(credentials, DockerContainer::OpenVpn), sshParams(credentials), scriptData, cbReadStdOut, cbReadStdErr);
e = runScript(sshParams(credentials),
replaceVars(scriptData,
genVarsForScript(credentials, container)));
if (e) return e;
QApplication::processEvents();
if (stdOut.contains("port is already allocated")) return ErrorCode::ServerPortAlreadyAllocatedError;
if (stdOut.contains("Error response from daemon")) return ErrorCode::ServerCheckFailed;
return checkOpenVpnServer(DockerContainer::OpenVpn, credentials);
runScript(sshParams(credentials),
replaceVars(amnezia::scriptData(ProtocolScriptType::configure_container, Protocol::OpenVpnOverCloak),
genVarsForScript(credentials, container)));
if (e) return e;
uploadTextFileToContainer(DockerContainer::OpenVpnOverCloak, credentials,
replaceVars(amnezia::scriptData(ProtocolScriptType::container_startup, Protocol::OpenVpnOverCloak),
genVarsForScript(credentials, container)),
"/opt/amnezia/start.sh");
// qDebug().noquote() << "AAAA"
// << amnezia::scriptData(ProtocolScriptType::container_startup, Protocol::OpenVpnOverCloak),
// replaceVars("/opt/amnezia/start.sh",
// genVarsForScript(credentials, container));
runScript(sshParams(credentials),
replaceVars("sudo docker exec -d $CONTAINER_NAME sh -c \"chmod a+x /opt/amnezia/start.sh && /opt/amnezia/start.sh\"",
genVarsForScript(credentials, container)));
if (e) return e;
return e;
}
ErrorCode ServerController::setupShadowSocksServer(const ServerCredentials &credentials)
{
// Setup openvpn part
QString scriptData;
QString scriptFileName = ":/server_scripts/setup_shadowsocks_server.sh";
QFile file(scriptFileName);
if (! file.open(QIODevice::ReadOnly)) return ErrorCode::InternalError;
return ErrorCode::NotImplementedError;
// // Setup openvpn part
// QString scriptData;
// QString scriptFileName = ":/server_scripts/setup_shadowsocks_server.sh";
// QFile file(scriptFileName);
// if (! file.open(QIODevice::ReadOnly)) return ErrorCode::InternalError;
scriptData = file.readAll();
if (scriptData.isEmpty()) return ErrorCode::InternalError;
// scriptData = file.readAll();
// if (scriptData.isEmpty()) return ErrorCode::InternalError;
QString stdOut;
auto cbReadStdOut = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
stdOut += data + "\n";
// QString stdOut;
// auto cbReadStdOut = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
// stdOut += data + "\n";
if (data.contains("Automatically restart Docker daemon?")) {
proc->write("yes\n");
}
};
auto cbReadStdErr = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
stdOut += data + "\n";
};
// if (data.contains("Automatically restart Docker daemon?")) {
// proc->write("yes\n");
// }
// };
// auto cbReadStdErr = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
// stdOut += data + "\n";
// };
ErrorCode e = runScript(genVarsForScript(credentials, DockerContainer::ShadowSocks), sshParams(credentials), scriptData, cbReadStdOut, cbReadStdErr);
if (e) return e;
// ErrorCode e = runScript(genVarsForScript(credentials, DockerContainer::ShadowSocks), sshParams(credentials), scriptData, cbReadStdOut, cbReadStdErr);
// if (e) return e;
// Create ss config
QJsonObject ssConfig;
ssConfig.insert("server", "0.0.0.0");
ssConfig.insert("server_port", ssRemotePort());
ssConfig.insert("local_port", ssContainerPort());
ssConfig.insert("password", QString(QCryptographicHash::hash(credentials.password.toUtf8(), QCryptographicHash::Sha256).toHex()));
ssConfig.insert("timeout", 60);
ssConfig.insert("method", ssEncryption());
QString configData = QJsonDocument(ssConfig).toJson();
QString sSConfigPath = "/opt/amneziavpn_data/ssConfig.json";
// // Create ss config
// QJsonObject ssConfig;
// ssConfig.insert("server", "0.0.0.0");
// ssConfig.insert("server_port", amnezia::protocols::shadowsocks::ssRemotePort());
// ssConfig.insert("local_port", amnezia::protocols::shadowsocks::ssContainerPort());
// ssConfig.insert("password", QString(QCryptographicHash::hash(credentials.password.toUtf8(), QCryptographicHash::Sha256).toHex()));
// ssConfig.insert("timeout", 60);
// ssConfig.insert("method", amnezia::protocols::shadowsocks::ssEncryption());
// QString configData = QJsonDocument(ssConfig).toJson();
// QString sSConfigPath = "/opt/amneziavpn_data/ssConfig.json";
configData.replace("\"", "\\\"");
//qDebug().noquote() << configData;
// configData.replace("\"", "\\\"");
// //qDebug().noquote() << configData;
uploadTextFileToContainer(DockerContainer::ShadowSocks, credentials, configData, sSConfigPath);
// uploadTextFileToContainer(DockerContainer::ShadowSocks, credentials, configData, sSConfigPath);
// Start ss
QString script = QString("sudo docker exec -d %1 sh -c \"ss-server -c %2\"").
arg(getContainerName(DockerContainer::ShadowSocks)).arg(sSConfigPath);
// // Start ss
// QString script = QString("sudo docker exec -d %1 sh -c \"ss-server -c %2\"").
// arg(amnezia::server::getContainerName(DockerContainer::ShadowSocks)).arg(sSConfigPath);
e = runScript(genVarsForScript(credentials, DockerContainer::ShadowSocks), sshParams(credentials), script);
return e;
// e = runScript(genVarsForScript(credentials, DockerContainer::ShadowSocks), sshParams(credentials), script);
// return e;
}
QHash<QString, QString> ServerController::genVarsForScript(const ServerCredentials &credentials, DockerContainer container)
ServerController::Vars ServerController::genVarsForScript(const ServerCredentials &credentials, DockerContainer container)
{
QHash<QString, QString> vars;
Vars vars;
vars.insert("$CONTAINER_NAME", getContainerName(container));
vars.append(qMakePair<QString, QString>("$VPN_SUBNET_IP", amnezia::server::vpnDefaultSubnetIp()));
vars.append(qMakePair<QString, QString>("$VPN_SUBNET_MASK_VAL", amnezia::server::vpnDefaultSubnetMaskVal()));
vars.append(qMakePair<QString, QString>("$VPN_SUBNET_MASK", amnezia::server::vpnDefaultSubnetMask()));
vars.append(qMakePair<QString, QString>("$CONTAINER_NAME", amnezia::server::getContainerName(container)));
vars.append(qMakePair<QString, QString>("$DOCKERFILE_FOLDER", "/opt/amnezia/" + amnezia::server::getContainerName(container)));
QString serverIp = Utils::getIPAddress(credentials.hostName);
if (!serverIp.isEmpty()) {
vars.insert("$SERVER_IP_ADDRESS", serverIp);
vars.append(qMakePair<QString, QString>("$SERVER_IP_ADDRESS", serverIp));
}
else {
qWarning() << "ServerController::genVarsForScript unable to resolve address for credentials.hostName";
}
//
if (container == DockerContainer::OpenVpn) {
vars.append(qMakePair<QString, QString>("$SERVER_PORT", amnezia::protocols::openvpn::openvpnDefaultPort()));
}
else if (container == DockerContainer::OpenVpnOverCloak) {
vars.append(qMakePair<QString, QString>("$SERVER_PORT", amnezia::protocols::cloak::ckDefaultPort()));
vars.append(qMakePair<QString, QString>("$FAKE_WEB_SITE_ADDRESS", amnezia::protocols::cloak::ckDefaultRedirSite()));
}
else if (container == DockerContainer::ShadowSocks) {
vars.append(qMakePair<QString, QString>("$SERVER_PORT", "6789"));
}
return vars;
}
@ -488,9 +672,17 @@ SshConnection *ServerController::connectToHost(const SshConnectionParameters &ss
ErrorCode ServerController::setupServerFirewall(const ServerCredentials &credentials)
{
QFile file(":/server_scripts/setup_firewall.sh");
file.open(QIODevice::ReadOnly);
QString script = file.readAll();
return runScript(genVarsForScript(credentials, DockerContainer::OpenVpn), sshParams(credentials), script);
return runScript(sshParams(credentials),
replaceVars(amnezia::scriptData(SharedScriptType::setup_host_firewall),
genVarsForScript(credentials, DockerContainer::OpenVpnOverCloak)));
}
QString ServerController::replaceVars(const QString &script, const Vars &vars)
{
QString s = script;
for (const QPair<QString, QString> &var : vars) {
//qDebug() << "Replacing" << var << vars.value(var);
s.replace(var.first, var.second);
}
return s;
}