[X2Go-Commits] [x2goclient] 01/01: Interaction with SSH server (for example for changing expired password). Fixes: #592.

git-admin at x2go.org git-admin at x2go.org
Wed May 10 15:23:10 CEST 2017


This is an automated email from the git hooks/post-receive script.

x2go pushed a commit to branch master
in repository x2goclient.

commit 68bbf328132125eaad5c53b0ac82490bf818e42e
Author: Oleksandr Shneyder <o.shneyder at phoca-gmbh.de>
Date:   Wed May 10 15:22:11 2017 +0200

    Interaction with SSH server (for example for changing expired password). Fixes: #592.
---
 debian/changelog            |   2 +
 src/InteractionDialog.cpp   | 132 ++++++++++++++++++++++++++++++++++++
 src/InteractionDialog.h     |  59 ++++++++++++++++
 src/onmainwindow.cpp        |  59 ++++++++++++++--
 src/onmainwindow.h          |   6 ++
 src/onmainwindow_privat.h   |   1 +
 src/sshmasterconnection.cpp | 160 +++++++++++++++++++++++++++++++++++++++++++-
 src/sshmasterconnection.h   |  11 +++
 src/x2goclient.cpp          |   1 +
 x2goclient.pro              |   2 +
 10 files changed, 428 insertions(+), 5 deletions(-)

diff --git a/debian/changelog b/debian/changelog
index 57e48d8..40e262f 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -155,6 +155,8 @@ x2goclient (4.1.0.1-0x2go1) UNRELEASED; urgency=medium
     - Disable sound button on direct RDP and XDMCP sessions.
       Set for direct XDMCP session autologin=true.
       Set for direct XDMCP session username=XDM.
+    - Interaction with SSH server (for example for changing
+      expired password). Fixes: #592.
 
   [ Robert Parts ]
   * New upstream version (4.1.0.1):
diff --git a/src/InteractionDialog.cpp b/src/InteractionDialog.cpp
new file mode 100644
index 0000000..eac7a23
--- /dev/null
+++ b/src/InteractionDialog.cpp
@@ -0,0 +1,132 @@
+/**************************************************************************
+*   Copyright (C) 2005-2017 by Oleksandr Shneyder                         *
+*   o.shneyder at phoca-gmbh.de                                              *
+*                                                                         *
+*   This program is free software; you can redistribute it and/or modify  *
+*   it under the terms of the GNU General Public License as published by  *
+*   the Free Software Foundation; either version 2 of the License, or     *
+*   (at your option) any later version.                                   *
+*   This program is distributed in the hope that it will be useful,       *
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
+*   GNU General Public License for more details.                          *
+*                                                                         *
+*   You should have received a copy of the GNU General Public License     *
+*   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
+***************************************************************************/
+
+#include "InteractionDialog.h"
+#include "x2goclientconfig.h"
+#include "onmainwindow.h"
+#include <QTextEdit>
+#include <QVBoxLayout>
+#include <QPushButton>
+#include <QLabel>
+#include <QLineEdit>
+
+InteractionDialog::InteractionDialog(QWidget* parent): SVGFrame(":/img/svg/passform.svg",
+            false,parent )
+{
+    mw=(ONMainWindow*)parent;
+    mw->setWidgetStyle(this);
+
+    if ( !mw->retMiniMode() )
+        setFixedSize ( this->sizeHint().width(),this->sizeHint().height()*1.5 );
+    else
+        setFixedSize ( 310,280 );
+
+
+    QPalette pal=this->palette();
+    pal.setBrush ( QPalette::Window, QColor ( 255,255,255,0 ) );
+    pal.setColor ( QPalette::Active, QPalette::WindowText, QPalette::Mid );
+    pal.setColor ( QPalette::Active, QPalette::ButtonText, QPalette::Mid );
+    pal.setColor ( QPalette::Active, QPalette::Text, QPalette::Mid );
+    pal.setColor ( QPalette::Inactive, QPalette::WindowText, QPalette::Mid );
+    pal.setColor ( QPalette::Inactive, QPalette::ButtonText, QPalette::Mid );
+    pal.setColor ( QPalette::Inactive, QPalette::Text, QPalette::Mid );
+
+    this->setPalette ( pal );
+
+    pal.setColor ( QPalette::Button, QColor ( 255,255,255,0 ) );
+    pal.setColor ( QPalette::Window, QColor ( 255,255,255,255 ) );
+    pal.setColor ( QPalette::Base, QColor ( 255,255,255,255 ) );
+
+    QFont fnt=this->font();
+    if ( mw->retMiniMode() )
+#ifdef Q_WS_HILDON
+        fnt.setPointSize ( 10 );
+#else
+        fnt.setPointSize ( 9 );
+#endif
+    this->setFont ( fnt );
+    this->hide();
+
+    textEdit=new QTextEdit(this);
+    QVBoxLayout* lay=new QVBoxLayout(this);
+    lay->addWidget(new QLabel(tr("Terminal output:")));
+    lay->addWidget(textEdit);
+
+    textEntry=new QLineEdit(this);
+    textEntry->setEchoMode(QLineEdit::NoEcho);
+    lay->addWidget(textEntry);
+    mw->setWidgetStyle(textEntry);
+
+    cancelButton=new QPushButton(tr("Cancel"),this);
+    lay->addWidget(cancelButton);
+    mw->setWidgetStyle(textEdit);
+    textEdit->setReadOnly(true);
+    mw->setWidgetStyle(textEdit->viewport());
+    mw->setWidgetStyle((QWidget*)textEdit->verticalScrollBar());
+    mw->setWidgetStyle(cancelButton);
+    connect(textEntry,SIGNAL(returnPressed()),this,SLOT(slotTextEntered()));
+    connect(cancelButton, SIGNAL(clicked(bool)),this,SLOT(slotButtonPressed()));
+}
+
+InteractionDialog::~InteractionDialog()
+{
+//     qDebug()<<"Iter dlg destruct\n";
+}
+
+void InteractionDialog::appendText(QString txt)
+{
+    textEntry->setEnabled(true);
+    textEdit->append(txt);
+    textEntry->setFocus();
+    interrupted=false;
+    display=false;
+    cancelButton->setText(tr("Cancel"));
+}
+
+void InteractionDialog::reset()
+{
+    textEdit->clear();
+}
+
+void InteractionDialog::slotTextEntered()
+{
+    QString text=textEntry->text()+"\n";
+    textEntry->clear();
+    emit textEntered(text);
+}
+
+void InteractionDialog::slotButtonPressed()
+{
+    if(!display)
+    {
+        emit interrupt();
+        interrupted=true;
+    }
+    else
+    {
+        qDebug()<<"reconnect";
+        emit closeInterractionDialog();
+    }
+}
+
+void InteractionDialog::setDisplayMode()
+{
+    cancelButton->setText(tr("Reconnect"));
+    textEntry->setEnabled(false);
+    display=true;
+}
+
diff --git a/src/InteractionDialog.h b/src/InteractionDialog.h
new file mode 100644
index 0000000..e2ddf43
--- /dev/null
+++ b/src/InteractionDialog.h
@@ -0,0 +1,59 @@
+/**************************************************************************
+*   Copyright (C) 2005-2017 by Oleksandr Shneyder                         *
+*   o.shneyder at phoca-gmbh.de                                              *
+*                                                                         *
+*   This program is free software; you can redistribute it and/or modify  *
+*   it under the terms of the GNU General Public License as published by  *
+*   the Free Software Foundation; either version 2 of the License, or     *
+*   (at your option) any later version.                                   *
+*   This program is distributed in the hope that it will be useful,       *
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
+*   GNU General Public License for more details.                          *
+*                                                                         *
+*   You should have received a copy of the GNU General Public License     *
+*   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
+***************************************************************************/
+
+#ifndef INTERACTIONDIALOG_H
+#define INTERACTIONDIALOG_H
+#include "x2goclientconfig.h"
+
+
+#include "SVGFrame.h"
+
+class ONMainWindow;
+class QTextEdit;
+class QLineEdit;
+class QPushButton;
+class InteractionDialog: public SVGFrame
+{
+
+    Q_OBJECT
+public:
+    InteractionDialog ( QWidget* parent=0);
+    virtual ~InteractionDialog();
+    void reset();
+    void appendText(QString txt);
+    bool isInterrupted() {
+        return interrupted;
+    }
+    void setDisplayMode();
+private:
+    ONMainWindow* mw;
+    QTextEdit* textEdit;
+    QPushButton* cancelButton;
+    QLineEdit* textEntry;
+    bool interrupted;
+    bool display;
+private slots:
+    void slotTextEntered();
+    void slotButtonPressed();
+signals:
+    void textEntered(QString text);
+    void interrupt();
+    void closeInterractionDialog();
+};
+
+#endif
+
diff --git a/src/onmainwindow.cpp b/src/onmainwindow.cpp
index 6107c76..d346af3 100644
--- a/src/onmainwindow.cpp
+++ b/src/onmainwindow.cpp
@@ -446,6 +446,9 @@ ONMainWindow::ONMainWindow ( QWidget *parent ) :QMainWindow ( parent )
     initPassDlg();
     initSelectSessDlg();
     initStatusDlg();
+    interDlg=new InteractionDialog(bgFrame);
+    connect(interDlg, SIGNAL(closeInterractionDialog()), this, SLOT(slotCloseInteractionDialog()));
+    username->addWidget ( interDlg );
 
 #if defined(CFGPLUGIN) && defined(Q_OS_LINUX)
 
@@ -2920,6 +2923,13 @@ SshMasterConnection* ONMainWindow::startSshConnection ( QString host, QString po
     connect ( con, SIGNAL ( userAuthError ( QString ) ),this,SLOT ( slotSshUserAuthError ( QString ) ) );
     connect ( con, SIGNAL ( connectionError ( QString,QString ) ), this,
               SLOT ( slotSshConnectionError ( QString,QString ) ) );
+    connect ( con, SIGNAL(startInteraction(SshMasterConnection*,QString)),this,
+               SLOT(slotSshInteractionStart(SshMasterConnection*,QString)) );
+    connect ( con, SIGNAL(updateInteraction(SshMasterConnection*,QString)),this,
+              SLOT(slotSshInteractionUpdate(SshMasterConnection*,QString)) );
+    connect (con, SIGNAL(finishInteraction(SshMasterConnection*)),this, SLOT(slotSshInteractionFinish(SshMasterConnection*)));
+    connect ( interDlg, SIGNAL(textEntered(QString)), con, SLOT(interactionTextEnter(QString)));
+    connect ( interDlg, SIGNAL(interrupt()), con, SLOT(interactionInterruptSlot()));
     con->start();
     return con;
 }
@@ -3008,6 +3018,44 @@ void ONMainWindow::slotServSshConnectionOk(QString server)
     con->executeCommand( "export HOSTNAME && x2golistsessions", this, SLOT (slotListAllSessions ( bool,QString,int ) ));
 }
 
+void ONMainWindow::slotSshInteractionFinish(SshMasterConnection* connection)
+{
+    if(interDlg->isInterrupted())
+    {
+         slotCloseInteractionDialog();
+    }
+    else
+    {
+         interDlg->setDisplayMode();
+    }
+}
+
+void ONMainWindow::slotCloseInteractionDialog()
+{
+         slotSshUserAuthError("NO_ERROR");
+}
+
+
+
+void ONMainWindow::slotSshInteractionStart(SshMasterConnection* connection, QString prompt)
+{
+    sessionStatusDlg->hide();
+    interDlg->show();
+    interDlg->reset();
+    interDlg->appendText(prompt);
+
+    setEnabled(true);
+    interDlg->setEnabled(true);
+    x2goDebug<<"SSH Session prompt:"<<prompt;
+
+}
+
+void ONMainWindow::slotSshInteractionUpdate(SshMasterConnection* connection, QString output)
+{
+    interDlg->appendText(output);
+    x2goDebug<<"SSH Interaction update:"<<output;
+}
+
 void ONMainWindow::slotSshServerAuthPassphrase(SshMasterConnection* connection, bool verificationCode)
 {
     bool ok;
@@ -3069,6 +3117,7 @@ void ONMainWindow::slotSshServerAuthChallengeResponse(SshMasterConnection* conne
 
 void ONMainWindow::slotSshServerAuthError ( int error, QString sshMessage, SshMasterConnection* connection )
 {
+    interDlg->hide();
     if ( startHidden )
     {
         startHidden=false;
@@ -3179,6 +3228,7 @@ void ONMainWindow::slotSshServerAuthError ( int error, QString sshMessage, SshMa
 
 void ONMainWindow::slotSshUserAuthError ( QString error )
 {
+    interDlg->hide();
     if ( sshConnection )
     {
         sshConnection->wait();
@@ -3201,9 +3251,10 @@ void ONMainWindow::slotSshUserAuthError ( QString error )
         trayQuit();
     }
 
-    QMessageBox::critical (0l, tr ("Authentication failed."),
-                           error, QMessageBox::Ok,
-                           QMessageBox::NoButton);
+    if(error != "NO_ERROR")
+        QMessageBox::critical (0l, tr ("Authentication failed."),
+                               error, QMessageBox::Ok,
+                               QMessageBox::NoButton);
     setEnabled ( true );
     passForm->setEnabled ( true );
     slotShowPassForm();
@@ -3752,6 +3803,7 @@ bool ONMainWindow::startSession ( const QString& sid )
 void ONMainWindow::slotListSessions ( bool result,QString output,
                                       int  )
 {
+    interDlg->hide();
     x2goDebug<<output;
     if ( result==false )
     {
@@ -12318,7 +12370,6 @@ void ONMainWindow::initSelectSessDlg()
 }
 
 
-
 void ONMainWindow::printSshDError_startupFailure()
 {
     if ( closeEventSent )
diff --git a/src/onmainwindow.h b/src/onmainwindow.h
index d2e5399..c38e50c 100644
--- a/src/onmainwindow.h
+++ b/src/onmainwindow.h
@@ -88,6 +88,7 @@ class QStandardItemModel;
 class HttpBrokerClient;
 class QMenu;
 class QComboBox;
+class InteractionDialog;
 
 class SessionExplorer;
 struct user
@@ -577,6 +578,7 @@ public:
 
 
 private:
+    InteractionDialog* interDlg;
     QString m_x2goconfig;
     QStringList _internApplicationsNames;
     QStringList _transApplicationsNames;
@@ -1022,7 +1024,11 @@ private slots:
     void slotSshConnectionError ( QString message, QString lastSessionError );
     void slotSshServerAuthError ( int error, QString sshMessage, SshMasterConnection* connection );
     void slotSshServerAuthPassphrase ( SshMasterConnection* connection, bool verificationCode );
+    void slotSshInteractionStart ( SshMasterConnection* connection, QString prompt );
+    void slotSshInteractionUpdate ( SshMasterConnection* connection, QString output );
+    void slotSshInteractionFinish ( SshMasterConnection* connection);
     void slotSshServerAuthChallengeResponse( SshMasterConnection* connection, QString Challenge );
+    void slotCloseInteractionDialog();
     void slotSshUserAuthError ( QString error );
     void slotSshConnectionOk();
     void slotServSshConnectionOk(QString server);
diff --git a/src/onmainwindow_privat.h b/src/onmainwindow_privat.h
index 817ee0b..92a3c36 100644
--- a/src/onmainwindow_privat.h
+++ b/src/onmainwindow_privat.h
@@ -33,6 +33,7 @@
 #include "printprocess.h"
 #include "helpdialog.h"
 #include "appdialog.h"
+#include "InteractionDialog.h"
 #include <QDesktopServices>
 #include <QApplication>
 #include <QScrollBar>
diff --git a/src/sshmasterconnection.cpp b/src/sshmasterconnection.cpp
index f2db295..e3ef249 100644
--- a/src/sshmasterconnection.cpp
+++ b/src/sshmasterconnection.cpp
@@ -724,7 +724,23 @@ void SshMasterConnection::run()
 #ifdef DEBUG
         x2goDebug<<"User authentication OK.";
 #endif
-        emit connectionOk(host);
+	if(checkLogin())
+	{
+	    x2goDebug<<"Login Check - OK";
+            emit connectionOk(host);
+	}
+	else
+	{
+	    x2goDebug<<"Login Check - Failed";
+// 	    if(!interactionInterrupt)
+	    {
+	      emit finishInteraction(this);
+	    }
+	    ssh_disconnect ( my_ssh_session );
+            ssh_free ( my_ssh_session );
+	    quit();
+	    return;
+	}
     }
     else
     {
@@ -1502,6 +1518,145 @@ bool SshMasterConnection::userAuthKrb()
 }
 
 
+void SshMasterConnection::interactionTextEnter(QString text)
+{
+    interactionInputMutex.lock();
+    interactionInputText=text;
+    interactionInputMutex.unlock();
+}
+
+void SshMasterConnection::interactionInterruptSlot()
+{
+    interactionInputMutex.lock();
+    interactionInterrupt=true;
+    interactionInputMutex.unlock();
+}
+
+bool SshMasterConnection::checkLogin()
+{
+    interactionInterrupt=false;
+    interactionInputText=QString::null;
+
+
+    ssh_channel channel = ssh_channel_new ( my_ssh_session );
+
+    if (!channel) {
+        QString err = ssh_get_error (my_ssh_session);
+        QString error_msg = tr ("%1 failed.").arg ("ssh_channel_new");
+
+#ifdef DEBUG
+        x2goDebug << error_msg.left (error_msg.size () - 1) << ": " << err << endl;
+#endif
+        return false;
+    }
+    if ( ssh_channel_open_session ( channel ) !=SSH_OK )
+    {
+        QString err=ssh_get_error ( my_ssh_session );
+        QString errorMsg=tr ( "%1 failed." ).arg ("ssh_channel_open_session");
+#ifdef DEBUG
+        x2goDebug<<errorMsg.left (errorMsg.size () - 1)<<": "<<err<<endl;
+#endif
+        return false;
+    }
+    if (ssh_channel_request_pty(channel)!=SSH_OK)
+    {
+        QString err=ssh_get_error ( my_ssh_session );
+        QString errorMsg=tr ( "%1 failed." ).arg ("ssh_channel_request_pty");
+#ifdef DEBUG
+        x2goDebug<<errorMsg.left (errorMsg.size () - 1)<<": "<<err<<endl;
+#endif
+        return false;
+    }
+    if(ssh_channel_change_pty_size(channel, 80, 24)!=SSH_OK)
+    {
+        QString err=ssh_get_error ( my_ssh_session );
+        QString errorMsg=tr ( "%1 failed." ).arg ("ssh_channel_change_pty_size");
+#ifdef DEBUG
+        x2goDebug<<errorMsg.left (errorMsg.size () - 1)<<": "<<err<<endl;
+#endif
+        return false;
+    }
+    if ( ssh_channel_request_exec ( channel, "echo \"LOGIN OK\"" ) != SSH_OK )
+    {
+        QString err=ssh_get_error ( my_ssh_session );
+        QString errorMsg=tr ( "%1 failed." ).arg ("ssh_channel_request_exec");
+#ifdef DEBUG
+        x2goDebug<<errorMsg.left (errorMsg.size () - 1)<<": "<<err<<endl;
+#endif
+    }
+    else
+    {
+        char buffer[1024*512]; //512K buffer
+        bool hasInterraction=true;
+	bool interactionStarted=false;
+//         x2goDebug<<"CHECK LOGIN channel created."<<endl;
+        while (ssh_channel_is_open(channel) &&
+                !ssh_channel_is_eof(channel))
+        {
+            int nbytes = ssh_channel_read_nonblocking(channel, buffer, sizeof(buffer), 0);
+            if (nbytes < 0)
+                return false;
+            if (nbytes > 0)
+            {
+                QString inf=QByteArray ( buffer,nbytes );
+                x2goDebug<<"LOGIN CHECK:"<<inf;
+                if(inf.indexOf("LOGIN OK")!=-1)
+                {
+		    x2goDebug<<"don't have interaction";
+                    hasInterraction=false;
+		    break;
+                }
+                else
+		{
+		  if(!interactionStarted)
+		  {
+		     interactionStarted=true;
+		     emit startInteraction(this, inf);
+		  }
+		  else
+		     emit updateInteraction(this,inf);
+		}
+            }
+            bool interrupt;
+            interactionInputMutex.lock();
+	    interrupt=interactionInterrupt;
+	    QString textToSend=interactionInputText;
+	    interactionInputText=QString::null;
+	    interactionInputMutex.unlock();
+	    if(textToSend.length()>0)
+	    {
+// 	        x2goDebug<<"SEND Input to SERVER";
+	        ssh_channel_write(channel, textToSend.toLocal8Bit().constData(), textToSend.length());
+	    }
+	    if(interrupt)
+	    {
+	        break;
+	    }
+            this->usleep(30);
+        }
+
+        x2goDebug<<"LOOP FINISHED";
+	bool retVal=false;
+        if(!hasInterraction)
+	{
+	    x2goDebug<<"No interaction needed, continue session";
+            retVal=true;
+	}
+	else
+        {
+            sshProcErrString=tr("Reconnect session");
+            x2goDebug<<"Reconnect session";
+        }
+        ssh_channel_close(channel);
+        ssh_channel_send_eof(channel);
+        ssh_channel_free(channel);
+	return retVal;
+
+    }
+    return false;
+
+}
+
 bool SshMasterConnection::userAuth()
 {
     if (kerberos)
@@ -2059,3 +2214,6 @@ void SshMasterConnection::finalize ( int item )
     emit channelClosed ( proc, uuid );
 }
 
+
+
+
diff --git a/src/sshmasterconnection.h b/src/sshmasterconnection.h
index 13499e6..f95f653 100644
--- a/src/sshmasterconnection.h
+++ b/src/sshmasterconnection.h
@@ -122,6 +122,7 @@ private:
     bool userAuthAuto();
     bool userAuthWithKey();
     bool userChallengeAuth();
+    bool checkLogin();
     bool userAuth();
     bool userAuthKrb();
     void channelLoop();
@@ -147,6 +148,9 @@ private slots:
     void slotSshProxyTunnelOk(int);
     void slotSshProxyTunnelFailed(bool result,  QString output,
                                   int);
+public slots:
+    void interactionTextEnter(QString text);
+    void interactionInterruptSlot();
 
 private:
     ssh_session my_ssh_session;
@@ -158,6 +162,9 @@ private:
     QMutex disconnectFlagMutex;
     QMutex writeHostKeyMutex;
     QMutex reverseTunnelRequestMutex;
+    QMutex interactionInputMutex;
+    QString interactionInputText;
+    bool interactionInterrupt;
     bool writeHostKey;
     bool writeHostKeyReady;
     int nextPid;
@@ -219,7 +226,11 @@ signals:
 
     void needPassPhrase(SshMasterConnection*, bool verificationCode);
     void needChallengeResponse(SshMasterConnection*, QString Challenge);
+    void startInteraction(SshMasterConnection*, QString prompt);
+    void finishInteraction(SshMasterConnection*);
+    void updateInteraction(SshMasterConnection*, QString output);
 };
 
 
 #endif // SSHMASTERCONNECTION_H
+
diff --git a/src/x2goclient.cpp b/src/x2goclient.cpp
index 9879142..0aea78f 100644
--- a/src/x2goclient.cpp
+++ b/src/x2goclient.cpp
@@ -92,6 +92,7 @@ int fork_helper (int argc, char **argv) {
 #endif /* defined (Q_OS_UNIX) */
 
 int main (int argc, char **argv) {
+   return (x2goMain(argc, argv));
 #ifdef Q_OS_UNIX
   /* Scan program arguments for --unixhelper flag. */
   bool unix_helper_request = 0;
diff --git a/x2goclient.pro b/x2goclient.pro
index 6cc8fd5..a1174f2 100644
--- a/x2goclient.pro
+++ b/x2goclient.pro
@@ -47,6 +47,7 @@ HEADERS += src/configdialog.h \
            src/sshmasterconnection.h \
            src/sshprocess.h \
            src/SVGFrame.h \
+           src/InteractionDialog.h \
            src/userbutton.h \
            src/x2goclientconfig.h \
            src/x2gologdebug.h \
@@ -101,6 +102,7 @@ SOURCES += src/sharewidget.cpp \
            src/sshmasterconnection.cpp \
            src/sshprocess.cpp \
            src/SVGFrame.cpp \
+           src/InteractionDialog.cpp \
            src/userbutton.cpp \
            src/x2gologdebug.cpp \
            src/printprocess.cpp \

--
Alioth's /srv/git/code.x2go.org/x2goclient.git//..//_hooks_/post-receive-email on /srv/git/code.x2go.org/x2goclient.git


More information about the x2go-commits mailing list