summaryrefslogtreecommitdiffstats
path: root/plugins/x2go
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 17:06:32 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 17:06:32 +0000
commit2dad5357405ad33cfa792f04b3ab62a5d188841e (patch)
treeb8f8893942060fe3cfb04ac374cda96fdfc8f453 /plugins/x2go
parentInitial commit. (diff)
downloadremmina-upstream/1.4.34+dfsg.tar.xz
remmina-upstream/1.4.34+dfsg.zip
Adding upstream version 1.4.34+dfsg.upstream/1.4.34+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plugins/x2go')
-rw-r--r--plugins/x2go/CMakeLists.txt66
-rw-r--r--plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg297
-rw-r--r--plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg297
-rw-r--r--plugins/x2go/x2go_plugin.c3463
-rw-r--r--plugins/x2go/x2go_plugin.h46
5 files changed, 4169 insertions, 0 deletions
diff --git a/plugins/x2go/CMakeLists.txt b/plugins/x2go/CMakeLists.txt
new file mode 100644
index 0000000..97df070
--- /dev/null
+++ b/plugins/x2go/CMakeLists.txt
@@ -0,0 +1,66 @@
+#
+# Project: Remmina Plugin X2Go
+# Description: Remmina protocol plugin to connect via X2Go using PyHoca
+# Based on Fabio Castelli Team Viewer Plugin
+# Copyright: 2013-2014 Fabio Castelli (Muflone)
+# Author: Antenore Gatta <antenore@simbiosi.org>
+# Copyright: 2015 Antenore Gatta
+# License: GPL-2+
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. * If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. * If you
+# do not wish to do so, delete this exception statement from your
+# version. * If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+set(REMMINA_PLUGIN_X2GO_SRCS
+ x2go_plugin.c
+ x2go_plugin.h
+ )
+
+add_library(remmina-plugin-x2go MODULE ${REMMINA_PLUGIN_X2GO_SRCS})
+set_target_properties(remmina-plugin-x2go PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-x2go PROPERTIES NO_SONAME 1)
+
+find_package(X11)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS}
+ ${XKBFILE_INCLUDE_DIRS} ${LIBSSH_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-x2go
+ ${REMMINA_COMMON_LIBRARIES}
+ ${XKBFILE_LIBRARIES}
+ ${LIBSSH_LIBRARIES}
+ ${X11_X11_LIB})
+
+install(TARGETS remmina-plugin-x2go DESTINATION ${REMMINA_PLUGINDIR})
+
+install(FILES
+ scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg
+ scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg
+ DESTINATION ${APPICONSCALE_EMBLEMS_DIR})
+
+if(WITH_ICON_CACHE)
+ gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor")
+endif()
diff --git a/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg
new file mode 100644
index 0000000..2cf9b92
--- /dev/null
+++ b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ width="128"
+ height="128"
+ sodipodi:docname="x2go-logo.svg"
+ sodipodi:docbase="/Users/h1/Desktop"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.0"
+ inkscape:export-filename="/Users/h1/Desktop/x2go-logo.png"
+ inkscape:export-xdpi="900"
+ inkscape:export-ydpi="900">
+ <metadata
+ id="metadata87">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>x2go Logo</dc:title>
+ <dc:date>12.06.2007</dc:date>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Heinz-M. Graesing</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:rights>
+ <cc:Agent>
+ <dc:title>obviously-nice</dc:title>
+ </cc:Agent>
+ </dc:rights>
+ <dc:publisher>
+ <cc:Agent>
+ <dc:title>obviously-nice</dc:title>
+ </cc:Agent>
+ </dc:publisher>
+ <dc:source>http://www.x2go.org/artwork</dc:source>
+ <dc:language>DE</dc:language>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>Logo</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ <cc:license
+ rdf:resource="http://creativecommons.org/licenses/by-nd/3.0/" />
+ </cc:Work>
+ <cc:License
+ rdf:about="http://creativecommons.org/licenses/by-nd/3.0/">
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Reproduction" />
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Distribution" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#Notice" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#Attribution" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs85">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 64 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="128 : 64 : 1"
+ inkscape:persp3d-origin="64 : 42.666667 : 1"
+ id="perspective46" />
+ </defs>
+ <sodipodi:namedview
+ inkscape:window-height="834"
+ inkscape:window-width="1295"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="true"
+ inkscape:object-nodes="true"
+ inkscape:grid-points="true"
+ inkscape:guide-points="true"
+ width="128px"
+ height="128px"
+ inkscape:zoom="3.9375"
+ inkscape:cx="64"
+ inkscape:cy="64"
+ inkscape:window-x="395"
+ inkscape:window-y="117"
+ inkscape:current-layer="svg2">
+ <inkscape:grid
+ id="GridFromPre046Settings"
+ type="xygrid"
+ originx="0px"
+ originy="0px"
+ spacingx="1px"
+ spacingy="1px"
+ color="#0000ff"
+ empcolor="#0000ff"
+ opacity="0.2"
+ empopacity="0.4"
+ empspacing="5" />
+ </sodipodi:namedview>
+ <g
+ style="fill:#4d4d4d"
+ id="g4"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 90,977.626 L 103.32,951.85 C 104.256,949.33 104.688,947.53 104.688,946.594 C 104.688,945.082 104.184,943.93 103.248,943.282 C 102.24,942.562 100.512,942.202 98.064,942.202 L 98.064,939.826 L 120.168,939.826 L 120.168,942.202 C 117.144,942.202 114.984,942.85 113.472,944.146 C 111.96,945.442 109.872,948.898 106.992,954.514 L 92.376,982.954 L 110.808,1024.498 C 112.68,1028.386 114.336,1031.122 115.776,1032.634 C 117.216,1034.074 118.944,1034.866 120.888,1034.866 L 120.888,1037.17 L 94.536,1037.17 L 94.536,1034.866 C 96.912,1034.866 98.568,1034.578 99.576,1033.93 C 100.584,1033.354 101.088,1032.346 101.088,1031.05 C 101.088,1029.826 100.368,1027.666 99.072,1024.498 L 86.112,995.482 L 71.28,1024.498 C 69.912,1026.946 69.192,1028.962 69.192,1030.474 C 69.192,1033.426 71.352,1034.866 75.672,1034.866 L 75.672,1037.17 L 55.728,1037.17 L 55.728,1034.866 C 57.672,1034.866 59.4,1034.218 60.84,1032.994 C 62.28,1031.77 63.792,1029.682 65.232,1026.586 L 83.736,990.01 L 67.248,952.498 C 65.52,948.466 63.936,945.802 62.424,944.362 C 60.84,942.922 58.608,942.202 55.728,942.202 L 55.728,939.826 L 84.024,939.826 L 84.024,942.202 C 79.632,942.202 77.4,943.714 77.4,946.738 C 77.4,948.25 77.832,949.978 78.696,951.85 L 90,977.626 z "
+ id="path6"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g8"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,940.474 L 50.976,939.25 L 50.976,941.698 L 6.984,940.474 z "
+ id="path10"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g12"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,946.45 L 50.976,945.298 L 50.976,946.954 L 56.16,947.026 L 56.52,947.026 L 56.808,947.026 L 57.096,947.098 L 57.384,947.098 L 57.672,947.17 L 57.888,947.242 L 58.104,947.314 L 58.32,947.386 L 58.464,947.458 L 58.608,947.53 L 58.752,947.602 L 58.896,947.674 L 58.968,947.746 L 59.112,947.818 L 59.112,947.89 L 59.184,947.89 L 6.984,946.45 z "
+ id="path14"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g16"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,952.498 L 61.272,950.986 L 61.488,951.346 L 61.776,951.922 L 62.064,952.498 L 62.352,953.146 L 62.64,953.794 L 62.784,954.01 L 6.984,952.498 z "
+ id="path18"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g20"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,958.474 L 63.936,956.89 L 65.376,960.058 L 6.984,958.474 z "
+ id="path22"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g24"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,964.45 L 66.528,962.866 L 67.968,966.106 L 6.984,964.45 z "
+ id="path26"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g28"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,970.498 L 69.12,968.77 L 70.632,972.226 L 6.984,970.498 z "
+ id="path30"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g32"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,976.474 L 71.712,974.674 L 73.296,978.274 L 6.984,976.474 z "
+ id="path34"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g36"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,982.45 L 74.304,980.65 L 75.96,984.322 L 6.984,982.45 z "
+ id="path38"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g40"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,988.426 L 76.896,986.554 L 78.408,989.866 L 78.192,990.37 L 6.984,988.426 z "
+ id="path42"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g44"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,994.402 L 77.112,992.458 L 75.168,996.274 L 6.984,994.402 z "
+ id="path46"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g48"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1000.378 L 74.016,998.578 L 72.216,1002.178 L 6.984,1000.378 z "
+ id="path50"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g52"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1006.354 L 70.92,1004.626 L 69.192,1008.082 L 6.984,1006.354 z "
+ id="path54"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g56"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1012.402 L 67.896,1010.746 L 66.168,1013.986 L 6.984,1012.402 z "
+ id="path58"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g60"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1036.378 L 50.976,1035.154 L 50.976,1037.602 L 6.984,1036.378 z "
+ id="path62"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g64"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1030.402 L 58.104,1028.962 L 58.032,1029.106 L 57.888,1029.178 L 57.744,1029.322 L 57.6,1029.394 L 57.528,1029.538 L 57.384,1029.61 L 57.312,1029.682 L 57.168,1029.682 L 57.024,1029.754 L 56.952,1029.826 L 56.808,1029.898 L 56.736,1029.898 L 56.592,1029.97 L 56.448,1029.97 L 56.376,1029.97 L 56.232,1030.042 L 56.088,1030.042 L 55.944,1030.042 L 50.976,1030.114 L 50.976,1031.554 L 6.984,1030.402 z "
+ id="path66"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g68"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1024.354 L 61.776,1022.914 L 60.984,1024.498 L 60.696,1025.002 L 60.48,1025.506 L 60.264,1025.866 L 6.984,1024.354 z "
+ id="path70"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g72"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1018.378 L 64.8,1016.794 L 63.288,1019.89 L 6.984,1018.378 z "
+ id="path74"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g76"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 63.936,924.49 L 118.872,924.49 C 123.84,924.49 127.872,928.522 127.872,933.49 L 127.872,1043.362 C 127.872,1048.33 123.84,1052.362 118.872,1052.362 L 63.936,1052.362 L 63.936,1050.418 L 117.864,1050.418 C 122.256,1050.418 125.856,1046.818 125.856,1042.426 L 125.856,934.498 C 125.856,930.106 122.256,926.506 117.864,926.506 L 63.936,926.506 L 63.936,924.49 z "
+ id="path78"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g80"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 9,924.49 L 63.936,924.49 L 63.936,926.506 L 9.936,926.506 C 5.544,926.506 1.944,930.106 1.944,934.498 L 1.944,1042.426 C 1.944,1046.818 5.544,1050.418 9.936,1050.418 L 63.936,1050.418 L 63.936,1052.362 L 9,1052.362 C 4.032,1052.362 0,1048.33 0,1043.362 L 0,933.49 C 0,928.522 4.032,924.49 9,924.49 z "
+ id="path82"
+ style="fill:#4d4d4d" />
+ </g>
+</svg>
diff --git a/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg
new file mode 100644
index 0000000..2cf9b92
--- /dev/null
+++ b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ width="128"
+ height="128"
+ sodipodi:docname="x2go-logo.svg"
+ sodipodi:docbase="/Users/h1/Desktop"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.0"
+ inkscape:export-filename="/Users/h1/Desktop/x2go-logo.png"
+ inkscape:export-xdpi="900"
+ inkscape:export-ydpi="900">
+ <metadata
+ id="metadata87">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>x2go Logo</dc:title>
+ <dc:date>12.06.2007</dc:date>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Heinz-M. Graesing</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:rights>
+ <cc:Agent>
+ <dc:title>obviously-nice</dc:title>
+ </cc:Agent>
+ </dc:rights>
+ <dc:publisher>
+ <cc:Agent>
+ <dc:title>obviously-nice</dc:title>
+ </cc:Agent>
+ </dc:publisher>
+ <dc:source>http://www.x2go.org/artwork</dc:source>
+ <dc:language>DE</dc:language>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>Logo</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ <cc:license
+ rdf:resource="http://creativecommons.org/licenses/by-nd/3.0/" />
+ </cc:Work>
+ <cc:License
+ rdf:about="http://creativecommons.org/licenses/by-nd/3.0/">
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Reproduction" />
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Distribution" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#Notice" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#Attribution" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs85">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 64 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="128 : 64 : 1"
+ inkscape:persp3d-origin="64 : 42.666667 : 1"
+ id="perspective46" />
+ </defs>
+ <sodipodi:namedview
+ inkscape:window-height="834"
+ inkscape:window-width="1295"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="true"
+ inkscape:object-nodes="true"
+ inkscape:grid-points="true"
+ inkscape:guide-points="true"
+ width="128px"
+ height="128px"
+ inkscape:zoom="3.9375"
+ inkscape:cx="64"
+ inkscape:cy="64"
+ inkscape:window-x="395"
+ inkscape:window-y="117"
+ inkscape:current-layer="svg2">
+ <inkscape:grid
+ id="GridFromPre046Settings"
+ type="xygrid"
+ originx="0px"
+ originy="0px"
+ spacingx="1px"
+ spacingy="1px"
+ color="#0000ff"
+ empcolor="#0000ff"
+ opacity="0.2"
+ empopacity="0.4"
+ empspacing="5" />
+ </sodipodi:namedview>
+ <g
+ style="fill:#4d4d4d"
+ id="g4"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 90,977.626 L 103.32,951.85 C 104.256,949.33 104.688,947.53 104.688,946.594 C 104.688,945.082 104.184,943.93 103.248,943.282 C 102.24,942.562 100.512,942.202 98.064,942.202 L 98.064,939.826 L 120.168,939.826 L 120.168,942.202 C 117.144,942.202 114.984,942.85 113.472,944.146 C 111.96,945.442 109.872,948.898 106.992,954.514 L 92.376,982.954 L 110.808,1024.498 C 112.68,1028.386 114.336,1031.122 115.776,1032.634 C 117.216,1034.074 118.944,1034.866 120.888,1034.866 L 120.888,1037.17 L 94.536,1037.17 L 94.536,1034.866 C 96.912,1034.866 98.568,1034.578 99.576,1033.93 C 100.584,1033.354 101.088,1032.346 101.088,1031.05 C 101.088,1029.826 100.368,1027.666 99.072,1024.498 L 86.112,995.482 L 71.28,1024.498 C 69.912,1026.946 69.192,1028.962 69.192,1030.474 C 69.192,1033.426 71.352,1034.866 75.672,1034.866 L 75.672,1037.17 L 55.728,1037.17 L 55.728,1034.866 C 57.672,1034.866 59.4,1034.218 60.84,1032.994 C 62.28,1031.77 63.792,1029.682 65.232,1026.586 L 83.736,990.01 L 67.248,952.498 C 65.52,948.466 63.936,945.802 62.424,944.362 C 60.84,942.922 58.608,942.202 55.728,942.202 L 55.728,939.826 L 84.024,939.826 L 84.024,942.202 C 79.632,942.202 77.4,943.714 77.4,946.738 C 77.4,948.25 77.832,949.978 78.696,951.85 L 90,977.626 z "
+ id="path6"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g8"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,940.474 L 50.976,939.25 L 50.976,941.698 L 6.984,940.474 z "
+ id="path10"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g12"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,946.45 L 50.976,945.298 L 50.976,946.954 L 56.16,947.026 L 56.52,947.026 L 56.808,947.026 L 57.096,947.098 L 57.384,947.098 L 57.672,947.17 L 57.888,947.242 L 58.104,947.314 L 58.32,947.386 L 58.464,947.458 L 58.608,947.53 L 58.752,947.602 L 58.896,947.674 L 58.968,947.746 L 59.112,947.818 L 59.112,947.89 L 59.184,947.89 L 6.984,946.45 z "
+ id="path14"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g16"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,952.498 L 61.272,950.986 L 61.488,951.346 L 61.776,951.922 L 62.064,952.498 L 62.352,953.146 L 62.64,953.794 L 62.784,954.01 L 6.984,952.498 z "
+ id="path18"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g20"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,958.474 L 63.936,956.89 L 65.376,960.058 L 6.984,958.474 z "
+ id="path22"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g24"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,964.45 L 66.528,962.866 L 67.968,966.106 L 6.984,964.45 z "
+ id="path26"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g28"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,970.498 L 69.12,968.77 L 70.632,972.226 L 6.984,970.498 z "
+ id="path30"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g32"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,976.474 L 71.712,974.674 L 73.296,978.274 L 6.984,976.474 z "
+ id="path34"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g36"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,982.45 L 74.304,980.65 L 75.96,984.322 L 6.984,982.45 z "
+ id="path38"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g40"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,988.426 L 76.896,986.554 L 78.408,989.866 L 78.192,990.37 L 6.984,988.426 z "
+ id="path42"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g44"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,994.402 L 77.112,992.458 L 75.168,996.274 L 6.984,994.402 z "
+ id="path46"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g48"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1000.378 L 74.016,998.578 L 72.216,1002.178 L 6.984,1000.378 z "
+ id="path50"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g52"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1006.354 L 70.92,1004.626 L 69.192,1008.082 L 6.984,1006.354 z "
+ id="path54"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g56"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1012.402 L 67.896,1010.746 L 66.168,1013.986 L 6.984,1012.402 z "
+ id="path58"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g60"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1036.378 L 50.976,1035.154 L 50.976,1037.602 L 6.984,1036.378 z "
+ id="path62"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g64"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1030.402 L 58.104,1028.962 L 58.032,1029.106 L 57.888,1029.178 L 57.744,1029.322 L 57.6,1029.394 L 57.528,1029.538 L 57.384,1029.61 L 57.312,1029.682 L 57.168,1029.682 L 57.024,1029.754 L 56.952,1029.826 L 56.808,1029.898 L 56.736,1029.898 L 56.592,1029.97 L 56.448,1029.97 L 56.376,1029.97 L 56.232,1030.042 L 56.088,1030.042 L 55.944,1030.042 L 50.976,1030.114 L 50.976,1031.554 L 6.984,1030.402 z "
+ id="path66"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g68"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1024.354 L 61.776,1022.914 L 60.984,1024.498 L 60.696,1025.002 L 60.48,1025.506 L 60.264,1025.866 L 6.984,1024.354 z "
+ id="path70"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g72"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1018.378 L 64.8,1016.794 L 63.288,1019.89 L 6.984,1018.378 z "
+ id="path74"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g76"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 63.936,924.49 L 118.872,924.49 C 123.84,924.49 127.872,928.522 127.872,933.49 L 127.872,1043.362 C 127.872,1048.33 123.84,1052.362 118.872,1052.362 L 63.936,1052.362 L 63.936,1050.418 L 117.864,1050.418 C 122.256,1050.418 125.856,1046.818 125.856,1042.426 L 125.856,934.498 C 125.856,930.106 122.256,926.506 117.864,926.506 L 63.936,926.506 L 63.936,924.49 z "
+ id="path78"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g80"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 9,924.49 L 63.936,924.49 L 63.936,926.506 L 9.936,926.506 C 5.544,926.506 1.944,930.106 1.944,934.498 L 1.944,1042.426 C 1.944,1046.818 5.544,1050.418 9.936,1050.418 L 63.936,1050.418 L 63.936,1052.362 L 9,1052.362 C 4.032,1052.362 0,1048.33 0,1043.362 L 0,933.49 C 0,928.522 4.032,924.49 9,924.49 z "
+ id="path82"
+ style="fill:#4d4d4d" />
+ </g>
+</svg>
diff --git a/plugins/x2go/x2go_plugin.c b/plugins/x2go/x2go_plugin.c
new file mode 100644
index 0000000..37ca272
--- /dev/null
+++ b/plugins/x2go/x2go_plugin.c
@@ -0,0 +1,3463 @@
+/*
+ * Project: Remmina Plugin X2Go
+ * Description: Remmina protocol plugin to connect via X2Go using PyHocaCLI
+ * Author: Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
+ * Antenore Gatta <antenore@simbiosi.org>
+ * Copyright: 2010-2011 Vic Lee
+ * 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * 2015 Antenore Gatta
+ * 2016-2018 Antenore Gatta, Giovanni Panozzo
+ * 2019 Mike Gabriel
+ * 2021 Daniel Teichmann
+ * License: GPL-2+
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "x2go_plugin.h"
+#include "common/remmina_plugin.h"
+
+#include <gtk/gtkx.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#elif defined(GDK_WINDOWING_WAYLAND)
+#include <gdk/gdkwayland.h>
+#endif
+
+#include <X11/Xlib.h>
+#include <X11/XKBlib.h>
+#include <X11/extensions/XKBrules.h>
+
+#include <sys/types.h>
+#include <signal.h>
+#include <time.h>
+#include <ctype.h>
+
+#define FEATURE_AVAILABLE(gpdata, feature) \
+ gpdata->available_features ? (g_list_find_custom( \
+ gpdata->available_features, \
+ feature, \
+ (GCompareFunc) g_strcmp0 \
+ ) ? TRUE : FALSE) : FALSE
+
+#define FEATURE_NOT_AVAIL_STR(feature) \
+ g_strdup_printf(_("The command-line feature '%s' is not available! Attempting " \
+ "to start PyHoca-CLI without using this feature…"), feature)
+
+#define GET_PLUGIN_DATA(gp) \
+ (RemminaPluginX2GoData*) g_object_get_data(G_OBJECT(gp), "plugin-data")
+
+// --------- SESSIONS ------------
+#define SET_RESUME_SESSION(gp, resume_data) \
+ g_object_set_data_full(G_OBJECT(gp), "resume-session-data", \
+ resume_data, \
+ g_free)
+
+#define GET_RESUME_SESSION(gp) \
+ (gchar*) g_object_get_data(G_OBJECT(gp), "resume-session-data")
+
+// A session is selected if the returning value is something other than 0.
+#define IS_SESSION_SELECTED(gp) \
+ g_object_get_data(G_OBJECT(gp), "session-selected") ? TRUE : FALSE
+
+// We don't use the function as a real pointer but rather as a boolean value.
+#define SET_SESSION_SELECTED(gp, is_session_selected) \
+ g_object_set_data_full(G_OBJECT(gp), "session-selected", \
+ is_session_selected, \
+ NULL)
+// -------------------
+
+#define REMMINA_PLUGIN_INFO(fmt, ...) \
+ rm_plugin_service->_remmina_info("[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_MESSAGE(fmt, ...) \
+ rm_plugin_service->_remmina_message("[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) \
+ rm_plugin_service->_remmina_debug(__func__, "[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_WARNING(fmt, ...) \
+ rm_plugin_service->_remmina_warning(__func__, "[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_AUDIT(fmt, ...) \
+ rm_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_ERROR(fmt, ...) \
+ rm_plugin_service->_remmina_error(__func__, "[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_CRITICAL(fmt, ...) \
+ rm_plugin_service->_remmina_critical(__func__, "[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define GET_PLUGIN_STRING(value) \
+ g_strdup(rm_plugin_service->file_get_string(remminafile, value))
+
+#define GET_PLUGIN_PASSWORD(value) \
+ GET_PLUGIN_STRING(value)
+
+#define GET_PLUGIN_INT(value, default_value) \
+ rm_plugin_service->file_get_int(remminafile, value, default_value)
+
+#define GET_PLUGIN_BOOLEAN(value) \
+ rm_plugin_service->file_get_int(remminafile, value, FALSE)
+
+static RemminaPluginService *rm_plugin_service = NULL;
+
+typedef struct _RemminaPluginX2GoData {
+ GtkWidget *socket;
+ gint socket_id;
+
+ pthread_t thread;
+
+ Display *display;
+ Window window_id;
+ int (*orig_handler)(Display *, XErrorEvent *);
+
+ GPid pidx2go;
+
+ gboolean disconnected;
+
+ GList* available_features;
+} RemminaPluginX2GoData;
+
+/**
+ * @brief Can be used to pass custom user data between functions and threads.
+ * *AND* pass the useful RemminaProtocolWidget with it along.
+ */
+typedef struct _X2GoCustomUserData {
+ RemminaProtocolWidget* gp;
+ gpointer dialog_data;
+ gpointer connect_data;
+ gpointer opt1;
+ gpointer opt2;
+} X2GoCustomUserData;
+
+/**
+ * @brief Used for the session chooser dialog (GtkListStore)
+ * See the example at: https://docs.gtk.org/gtk3/class.ListStore.html
+ * The order is the exact same as the user sees in the dialog.
+ * SESSION_NUM_PROPERTIES is used to keep count of the properties
+ * and it must be the last object.
+ */
+enum SESSION_PROPERTIES {
+ SESSION_DISPLAY = 0,
+ SESSION_STATUS,
+ SESSION_SESSION_ID,
+ SESSION_SUSPENDED_SINCE,
+ SESSION_CREATE_DATE,
+ SESSION_AGENT_PID,
+ SESSION_USERNAME,
+ SESSION_HOSTNAME,
+ SESSION_COOKIE,
+ SESSION_GRAPHIC_PORT,
+ SESSION_SND_PORT,
+ SESSION_SSHFS_PORT,
+ SESSION_DIALOG_IS_VISIBLE,
+ SESSION_NUM_PROPERTIES // Must be last. Counts all enum elements.
+};
+
+// Following str2int code was adapted from Stackoverflow:
+// https://stackoverflow.com/questions/7021725/how-to-convert-a-string-to-integer-in-c
+typedef enum _str2int_errno {
+ STR2INT_SUCCESS,
+ STR2INT_OVERFLOW,
+ STR2INT_UNDERFLOW,
+ STR2INT_INCONVERTIBLE,
+ STR2INT_INVALID_DATA
+} str2int_errno;
+
+/**
+ * @brief Convert string s to int out.
+ *
+ * @param out The converted int. Cannot be NULL.
+ *
+ * @param s Input string to be converted. \n
+ * The format is the same as strtol,
+ * except that the following are inconvertible: \n
+ * * empty string \n
+ * * leading whitespace \n
+ * * or any trailing characters that are not part of the number \n
+ * Cannot be NULL.
+ * @param base Base to interpret string in. Same range as strtol (2 to 36).
+ *
+ * @return Indicates if the operation succeeded, or why it failed with str2int_errno enum.
+ */
+str2int_errno str2int(gint *out, gchar *s, gint base)
+{
+ gchar *end;
+
+ if (!s || !out || base <= 0) return STR2INT_INVALID_DATA;
+
+ if (s[0] == '\0' || isspace(s[0])) return STR2INT_INCONVERTIBLE;
+
+ errno = 0;
+ glong l = strtol(s, &end, base);
+
+ /* Both checks are needed because INT_MAX == LONG_MAX is possible. */
+ if (l > INT_MAX || (errno == ERANGE && l == LONG_MAX)) return STR2INT_OVERFLOW;
+ if (l < INT_MIN || (errno == ERANGE && l == LONG_MIN)) return STR2INT_UNDERFLOW;
+ if (*end != '\0') return STR2INT_INCONVERTIBLE;
+
+ *out = l;
+ return STR2INT_SUCCESS;
+}
+
+/**
+ * DialogData:
+ * @param flags see GtkDialogFlags
+ * @param type see GtkMessageType
+ * @param buttons see GtkButtonsType
+ * @param title Title of the Dialog
+ * @param message Message of the Dialog
+ * @param callbackfunc A GCallback function which will be executed on the dialogs
+ * 'response' signal. Allowed to be NULL. \n
+ * The callback function is obliged to destroy the dialog widget. \n
+ * @param dialog_factory A user-defined callback function that is called when it is time
+ * to build the actual GtkDialog. \n
+ * Can be used to build custom dialogs. Allowed to be NULL.
+ *
+ *
+ * The `DialogData` structure contains all info needed to open a GTK dialog with
+ * rmplugin_x2go_open_dialog()
+ *
+ * Quick example of a callback function: \n
+ * static gboolean rmplugin_x2go_test_callback(RemminaProtocolWidget *gp, gint response_id, \n
+ * GtkDialog *self) { \n
+ * REMMINA_PLUGIN_DEBUG("response: %i", response_id); \n
+ * if (response_id == GTK_RESPONSE_OK) { \n
+ * REMMINA_PLUGIN_DEBUG("OK!"); \n
+ * } \n
+ * gtk_widget_destroy(self); \n
+ * return G_SOURCE_REMOVE; \n
+ * }
+ *
+ */
+struct _DialogData
+{
+ GtkWindow *parent;
+ GtkDialogFlags flags;
+ GtkMessageType type;
+ GtkButtonsType buttons;
+ gchar *title;
+ gchar *message;
+ GCallback callbackfunc;
+
+ // If the dialog needs to be custom.
+ GCallback dialog_factory_func;
+ gpointer dialog_factory_data;
+};
+
+/**
+ * @param custom_data X2GoCustomUserData structure with the following: \n
+ * gp -> gp (RemminaProtocolWidget*) \n
+ * dialog_data -> dialog data (struct _DialogData*)
+ * @returns: FALSE. This source should be removed from main loop.
+ * #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable
+ * names for the return value.
+ */
+static gboolean rmplugin_x2go_open_dialog(X2GoCustomUserData *custom_data)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!custom_data || !custom_data->gp || !custom_data->dialog_data) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Parameter 'custom_data' is not initialized!")
+ ));
+
+ return G_SOURCE_REMOVE;
+ }
+
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget*) custom_data->gp;
+ struct _DialogData *ddata = (struct _DialogData*) custom_data->dialog_data;
+
+ if (ddata) {
+ // Can't check type, flags or buttons
+ // because they are enums and '0' is a valid value
+ if (!ddata->title || !ddata->message) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Broken `DialogData`! Aborting…"));
+ return G_SOURCE_REMOVE;
+ }
+ } else {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Can't retrieve `DialogData`! Aborting…"));
+ return G_SOURCE_REMOVE;
+ }
+
+ REMMINA_PLUGIN_DEBUG("`DialogData` checks passed. Now showing dialog…");
+
+ GtkWidget* widget_gtk_dialog = NULL;
+
+ if (ddata->dialog_factory_func != NULL) {
+ REMMINA_PLUGIN_DEBUG("Calling *custom* dialog factory function…");
+ GCallback dialog_factory_func = G_CALLBACK(ddata->dialog_factory_func);
+ gpointer dialog_factory_data = ddata->dialog_factory_data;
+
+ // Calling dialog_factory_func(custom_data, dialog_factory_data);
+ widget_gtk_dialog = ((GtkWidget* (*)(X2GoCustomUserData*, gpointer))
+ dialog_factory_func)(custom_data, dialog_factory_data);
+ } else {
+ widget_gtk_dialog = gtk_message_dialog_new(ddata->parent,
+ ddata->flags,
+ ddata->type,
+ ddata->buttons,
+ "%s", ddata->title);
+
+ gtk_message_dialog_format_secondary_text(
+ GTK_MESSAGE_DIALOG(widget_gtk_dialog), "%s", ddata->message);
+ }
+
+ if (!widget_gtk_dialog) {
+ REMMINA_PLUGIN_CRITICAL("Error! Aborting.");
+ return G_SOURCE_REMOVE;
+ }
+
+ if (ddata->callbackfunc) {
+ g_signal_connect_swapped(G_OBJECT(widget_gtk_dialog), "response",
+ G_CALLBACK(ddata->callbackfunc),
+ custom_data);
+ } else {
+ g_signal_connect(G_OBJECT(widget_gtk_dialog), "response",
+ G_CALLBACK(gtk_widget_destroy),
+ NULL);
+ }
+
+ gtk_widget_show_all(widget_gtk_dialog);
+
+ // Delete ddata object and reference 'dialog-data' in gp.
+ g_object_set_data(G_OBJECT(gp), "dialog-data", NULL);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * @brief These define the responses of session-chooser-dialog's buttons.
+ */
+enum SESSION_CHOOSER_RESPONSE_TYPE {
+ SESSION_CHOOSER_RESPONSE_NEW = 0,
+ SESSION_CHOOSER_RESPONSE_CHOOSE,
+ SESSION_CHOOSER_RESPONSE_TERMINATE,
+};
+
+/**
+ * @brief Finds a child GtkWidget of a parent GtkWidget.
+ * Copied from https://stackoverflow.com/a/23497087 ;)
+ *
+ * @param parent Parent GtkWidget*
+ * @param name Name string of child. (Must be set before, er else it will be a
+ * default string)
+ * @return GtkWidget*
+ */
+static GtkWidget* rmplugin_x2go_find_child(GtkWidget* parent, const gchar* name)
+{
+ const gchar* parent_name = gtk_widget_get_name((GtkWidget*) parent);
+ if (g_ascii_strcasecmp(parent_name, (gchar*) name) == 0) {
+ return parent;
+ }
+
+ if (GTK_IS_BIN(parent)) {
+ GtkWidget *child = gtk_bin_get_child(GTK_BIN(parent));
+ return rmplugin_x2go_find_child(child, name);
+ }
+
+ if (GTK_IS_CONTAINER(parent)) {
+ GList *children = gtk_container_get_children(GTK_CONTAINER(parent));
+ while (children != NULL) {
+ GtkWidget *widget = rmplugin_x2go_find_child(children->data, name);
+ if (widget != NULL) {
+ return widget;
+ }
+
+ children = g_list_next(children);
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * @brief Gets executed on "row-activated" signal. It is emitted when the method when
+ * the user double clicks a treeview row. It is also emitted when a non-editable
+ * row is selected and one of the keys: Space, Shift+Space, Return or Enter is
+ * pressed.
+ *
+ * @param custom_data X2GoCustomUserData structure with the following: \n
+ * gp -> gp (RemminaProtocolWidget*) \n
+ * opt1 -> dialog widget (GtkWidget*)
+ */
+static gboolean rmplugin_x2go_session_chooser_row_activated(GtkTreeView *treeview,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column,
+ X2GoCustomUserData *custom_data)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!custom_data || !custom_data->gp || !custom_data->opt1) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Parameter 'custom_data' is not initialized!")
+ ));
+
+ return G_SOURCE_REMOVE;
+ }
+
+ RemminaProtocolWidget* gp = (RemminaProtocolWidget*) custom_data->gp;
+ // dialog_data (unused)
+ // connect_data (unused)
+ GtkWidget* dialog = GTK_WIDGET(custom_data->opt1);
+
+ gchar *session_id;
+ GtkTreeIter iter;
+ GtkTreeModel *model = gtk_tree_view_get_model(treeview);
+
+ if (gtk_tree_model_get_iter(model, &iter, path)) {
+ gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
+ SESSION_SESSION_ID, &session_id, -1);
+
+ // Silent bail out.
+ if (!session_id || strlen(session_id) <= 0) return G_SOURCE_REMOVE;
+
+ SET_RESUME_SESSION(gp, session_id);
+
+ // Unstucking main process. Telling it that a session has been selected.
+ // We use a trick here. As long as there is something other than 0
+ // stored, a session is selected. So we use the gpointer as a gboolean.
+ SET_SESSION_SELECTED(gp, (gpointer) TRUE);
+ gtk_widget_hide(GTK_WIDGET(dialog));
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * @brief Translates a session property (described by SESSION_PROPERTIES enum) to a
+ * string containing it's display name.
+ *
+ * @param session_property A session property. (as described by SESSION_PROPERTIES enum)
+ * @return gchar* Translated display name. (Can be NULL, if session_property is invalid!)
+ */
+static gchar *rmplugin_x2go_session_property_to_string(guint session_property) {
+ gchar* return_char = NULL;
+
+ switch (session_property) {
+ // I think we can close one eye here regarding max line-length.
+ case SESSION_DISPLAY: return_char = g_strdup(_("X Display")); break;
+ case SESSION_STATUS: return_char = g_strdup(_("Status")); break;
+ case SESSION_SESSION_ID: return_char = g_strdup(_("Session ID")); break;
+ case SESSION_CREATE_DATE: return_char = g_strdup(_("Create date")); break;
+ case SESSION_SUSPENDED_SINCE: return_char = g_strdup(_("Suspended since")); break;
+ case SESSION_AGENT_PID: return_char = g_strdup(_("Agent PID")); break;
+ case SESSION_USERNAME: return_char = g_strdup(_("Username")); break;
+ case SESSION_HOSTNAME: return_char = g_strdup(_("Hostname")); break;
+ case SESSION_COOKIE: return_char = g_strdup(_("Cookie")); break;
+ case SESSION_GRAPHIC_PORT: return_char = g_strdup(_("Graphic port")); break;
+ case SESSION_SND_PORT: return_char = g_strdup(_("SND port")); break;
+ case SESSION_SSHFS_PORT: return_char = g_strdup(_("SSHFS port")); break;
+ case SESSION_DIALOG_IS_VISIBLE: return_char = g_strdup(_("Visible")); break;
+ }
+
+ return return_char;
+}
+
+/**
+ * @brief Builds a dialog which contains all found X2Go-Sessions of the remote server.
+ * The dialog gives the user the option to choose between resuming or terminating
+ * an existing session or to create a new one.
+ *
+ * @param custom_data X2GoCustomUserData structure with the following: \n
+ * gp -> gp (RemminaProtocolWidget*) \n
+ * dialog_data -> dialog data (struct _DialogData*) \n
+ * connect_data -> connection data (struct _ConnectionData*)
+ * @param sessions_list The GList* Should contain all found X2Go-Sessions.
+ * Sessions are string arrays of properties.
+ * The type of the GList is gchar**.
+ *
+ * @returns GtkWidget* custom dialog.
+ */
+static GtkWidget* rmplugin_x2go_choose_session_dialog_factory(X2GoCustomUserData *custom_data,
+ GList *sessions_list)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!custom_data || !custom_data->gp ||
+ !custom_data->dialog_data || !custom_data->connect_data) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Parameter 'custom_data' is not initialized!")
+ ));
+
+ return NULL;
+ }
+
+ struct _DialogData* ddata = (struct _DialogData*) custom_data->dialog_data;
+
+ if (!ddata || !sessions_list || !ddata->title) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Could not retrieve valid `DialogData` or "
+ "`sessions_list`! Aborting…"));
+ return NULL;
+ }
+
+ GtkWidget *widget_gtk_dialog = NULL;
+ widget_gtk_dialog = gtk_dialog_new_with_buttons(ddata->title, ddata->parent,
+ ddata->flags,
+ // TRANSLATORS: Stick to x2goclient's translation for terminate.
+ _("_Terminate"),
+ SESSION_CHOOSER_RESPONSE_TERMINATE,
+ // TRANSLATORS: Stick to x2goclient's translation for resume.
+ _("_Resume"),
+ SESSION_CHOOSER_RESPONSE_CHOOSE,
+ _("_New"),
+ SESSION_CHOOSER_RESPONSE_NEW,
+ NULL);
+
+ GtkWidget *button = gtk_dialog_get_widget_for_response(
+ GTK_DIALOG(widget_gtk_dialog),
+ SESSION_CHOOSER_RESPONSE_TERMINATE);
+ // TRANSLATORS: Tooltip for terminating button inside Session-Chooser-Dialog.
+ // TRANSLATORS: Please stick to X2GoClient's way of translating.
+ gtk_widget_set_tooltip_text(button, _("Terminating X2Go sessions can take a moment."));
+
+ #define DEFAULT_DIALOG_WIDTH 720
+ #define DEFAULT_DIALOG_HEIGHT (DEFAULT_DIALOG_WIDTH * 9) / 16
+
+ gtk_widget_set_size_request(GTK_WIDGET(widget_gtk_dialog),
+ DEFAULT_DIALOG_WIDTH, DEFAULT_DIALOG_HEIGHT);
+ gtk_window_set_default_size(GTK_WINDOW(widget_gtk_dialog),
+ DEFAULT_DIALOG_WIDTH, DEFAULT_DIALOG_HEIGHT);
+
+ gtk_window_set_resizable(GTK_WINDOW(widget_gtk_dialog), TRUE);
+
+ GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL);
+ //gtk_widget_show(scrolled_window);
+
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(
+ GTK_DIALOG(widget_gtk_dialog))
+ ), GTK_WIDGET(scrolled_window), TRUE, TRUE, 5);
+
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolled_window),
+ GTK_POLICY_AUTOMATIC,
+ GTK_POLICY_AUTOMATIC);
+
+
+ GType types[SESSION_NUM_PROPERTIES];
+
+ // First to last in SESSION_PROPERTIES.
+ for (gint i = 0; i < SESSION_NUM_PROPERTIES; ++i) {
+ // Everything is a String. (Except IS_VISIBLE flag)
+ // If that changes one day, you could extent the if statement here.
+ // But you would propably need a *lot* of refactoring.
+ // Especially in the session parser.
+ if (i == SESSION_DIALOG_IS_VISIBLE) {
+ types[i] = G_TYPE_BOOLEAN;
+ } else {
+ types[i] = G_TYPE_STRING;
+ }
+ }
+
+ // create tree view
+ GtkListStore *store = gtk_list_store_newv(SESSION_NUM_PROPERTIES, types);
+
+ GtkTreeModelFilter *filter = GTK_TREE_MODEL_FILTER(
+ gtk_tree_model_filter_new(GTK_TREE_MODEL(store),
+ NULL)
+ );
+ gtk_tree_model_filter_set_visible_column(filter, SESSION_DIALOG_IS_VISIBLE);
+
+ GtkWidget *tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(filter));
+ g_object_unref (G_OBJECT (store)); // tree now holds reference
+ gtk_widget_set_size_request(tree_view, -1, 300);
+
+ // Gets name to be findable by rmplugin_x2go_find_child()
+ gtk_widget_set_name(GTK_WIDGET(tree_view), "session_chooser_treeview");
+
+ // create list view columns
+ gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree_view), TRUE);
+ gtk_tree_view_set_headers_clickable (GTK_TREE_VIEW(tree_view), FALSE);
+ gtk_tree_view_set_enable_search(GTK_TREE_VIEW(tree_view), TRUE);
+ gtk_widget_show (tree_view);
+ gtk_container_add (GTK_CONTAINER(scrolled_window), tree_view);
+
+ GtkTreeViewColumn *tree_view_col = NULL;
+ GtkCellRenderer *cell_renderer = NULL;
+ gchar *header_title = NULL;
+
+ // First to last in SESSION_PROPERTIES.
+ for (guint i = 0; i < SESSION_NUM_PROPERTIES; ++i) {
+ // Do not display SESSION_DIALOG_IS_VISIBLE.
+ if (i == SESSION_DIALOG_IS_VISIBLE) continue;
+
+ header_title = rmplugin_x2go_session_property_to_string(i);
+ if (!header_title) {
+ REMMINA_PLUGIN_WARNING("%s", g_strdup_printf(
+ _("Internal error: %s"), g_strdup_printf(
+ _("Unknown property '%i'"), i
+ )));
+ header_title = g_strdup_printf(_("Unknown property '%i'"), i);
+ }
+
+ tree_view_col = gtk_tree_view_column_new();
+ gtk_tree_view_column_set_title(tree_view_col, header_title);
+ gtk_tree_view_column_set_clickable(tree_view_col, FALSE);
+ gtk_tree_view_column_set_sizing (tree_view_col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
+ gtk_tree_view_column_set_resizable(tree_view_col, TRUE);
+
+ cell_renderer = gtk_cell_renderer_text_new();
+ gtk_tree_view_column_pack_start(tree_view_col, cell_renderer, TRUE);
+ gtk_tree_view_column_add_attribute(tree_view_col, cell_renderer, "text", i);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), tree_view_col);
+ }
+
+ GList *elem = NULL;
+ GtkTreeIter iter;
+
+ for (elem = sessions_list; elem; elem = elem->next) {
+ gchar** session = (gchar**) elem->data;
+ g_assert(session != NULL);
+
+ gtk_list_store_append(store, &iter);
+
+ for (gint i = 0; i < SESSION_NUM_PROPERTIES; i++) {
+ gchar* property = session[i];
+ GValue a = G_VALUE_INIT;
+
+ // Everything here is a string (except SESSION_DIALOG_IS_VISIBLE)
+
+ if (i == SESSION_DIALOG_IS_VISIBLE) {
+ g_value_init(&a, G_TYPE_BOOLEAN);
+ g_assert(G_VALUE_HOLDS_BOOLEAN(&a) && "GValue does not "
+ "hold a boolean!");
+ // Default is to show every new session.
+ g_value_set_boolean(&a, TRUE);
+ } else {
+ g_value_init(&a, G_TYPE_STRING);
+ g_assert(G_VALUE_HOLDS_STRING(&a) && "GValue does not "
+ "hold a string!");
+ g_value_set_static_string (&a, property);
+ }
+
+ gtk_list_store_set_value(store, &iter, i, &a);
+ }
+ }
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ * connect_data -> connection data (struct _ConnectionData*)
+ * opt1 -> dialog widget (GtkWidget*)
+ */
+ // everything else is already initialized.
+ custom_data->opt1 = widget_gtk_dialog;
+
+ g_signal_connect(tree_view, "row-activated",
+ G_CALLBACK(rmplugin_x2go_session_chooser_row_activated),
+ custom_data);
+
+ return widget_gtk_dialog;
+}
+
+/**
+ * @brief Uses either 'dialog' or 'treeview' to return the GtkTreeModel of the
+ * Session-Chooser-Dialog. Directly giving 'treeview' as a parameter is faster.
+ * Only *one* parameter has to be given. The other one can be NULL.
+ * Error messages are all handled already.
+ *
+ * @param dialog The Session-Chooser-Dialog itself. (Slower) Can be NULL.
+ * @param treeview The GtkTreeView of the Session-Chooser-Dialog. (faster) Can be NULL.
+ * @return GtkTreeModelFilter* (Does not contains all rows, only the visible ones!!!) \n
+ * But you can get all rows with: \n
+ * gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(return_model));
+ */
+static GtkTreeModelFilter* rmplugin_x2go_session_chooser_get_filter_model(GtkWidget *dialog,
+ GtkTreeView* treeview)
+{
+ //REMMINA_PLUGIN_DEBUG("Function entry.");
+ GtkTreeModel *return_model = NULL;
+
+ if (!treeview && dialog) {
+ GtkWidget *treeview_new = rmplugin_x2go_find_child(GTK_WIDGET(dialog),
+ "session_chooser_treeview");
+
+ if (!treeview_new) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Could not find child GtkTreeView of "
+ "session chooser dialog.")
+ ));
+ return NULL;
+ }
+
+ return_model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview_new));
+ } else if (treeview) {
+ return_model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
+ } else {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Neither the 'dialog' nor 'treeview' parameters are initialized! "
+ "At least one of them must be given.")
+ ));
+ return NULL;
+ }
+
+ if (!return_model || !GTK_TREE_MODEL_FILTER(return_model)) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Could not obtain \"GtkTreeModelFilter*\" of the session chooser dialog, "
+ "for unknown reason.")
+ ));
+ }
+
+ return GTK_TREE_MODEL_FILTER(return_model);
+}
+
+/**
+ * @brief Gets the selected row of the Session-Chooser-Dialog.
+ * The path gets converted with gtk_tree_model_filter_convert_child_path_to_path()
+ * before it gets returned. So path describes a row of 'filter' and *not* its
+ * child GtkTreeModel.
+ *
+ * @param dialog The Session-Chooser-Dialog.
+ * @return GtkTreePath* describing the path to the row.
+ */
+static GtkTreePath* rmplugin_x2go_session_chooser_get_selected_row(GtkWidget *dialog)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ GtkWidget *treeview = rmplugin_x2go_find_child(GTK_WIDGET(dialog),
+ "session_chooser_treeview");
+ if (!treeview) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Could not find child GtkTreeView of session chooser dialog.")
+ ));
+ return NULL;
+ }
+
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
+ if (!selection) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Could not get currently selected row (session)!")
+ ));
+ return NULL;
+ }
+
+ GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(
+ NULL, GTK_TREE_VIEW(treeview));
+ GtkTreeModel *model = gtk_tree_model_filter_get_model(filter);
+ if (!model) return NULL; // error message was already handled.
+
+ GtkTreeModel *filter_model = GTK_TREE_MODEL(filter);
+ g_assert(filter_model && "Could not cast 'filter' to a GtkTreeModel!");
+ GList *selected_rows = gtk_tree_selection_get_selected_rows(selection, &filter_model);
+
+ // We only support single selection.
+ gint selected_rows_num = gtk_tree_selection_count_selected_rows(selection);
+ if (selected_rows_num != 1) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"), g_strdup_printf(
+ _("Exactly one session should be selectable but '%i' rows "
+ "(sessions) are selected."),
+ selected_rows_num
+ )));
+ return NULL;
+ }
+
+ // This would be very dangerous (we didn't check for NULL) if we hadn't just
+ // checked that only one row is selected.
+ GtkTreePath *path = selected_rows->data;
+
+ // Convert to be path of GtkTreeModelFilter and *not* its child GtkTreeModel.
+ path = gtk_tree_model_filter_convert_child_path_to_path(filter, path);
+
+ return path;
+}
+
+/**
+ * @brief Finds the GtkTreeView inside of the session chooser dialog,
+ * determines the selected row and extracts a property.
+ *
+ * @param dialog GtkWidget* the dialog itself.
+ * @param property_index Index of property.
+ * @param row A specific row to get the property of. (Can be NULL)
+ *
+ * @return GValue The value of property. (Can be non-initialized!)
+ */
+static GValue rmplugin_x2go_session_chooser_get_property(GtkWidget *dialog,
+ gint property_index,
+ GtkTreePath *row)
+{
+ //REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ GValue ret_value = G_VALUE_INIT;
+
+ if (!row) {
+ GtkTreePath *selected_row = rmplugin_x2go_session_chooser_get_selected_row(dialog);
+ if (!selected_row) return ret_value; // error message was already handled.
+ row = selected_row;
+ }
+
+ GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(dialog, NULL);
+ GtkTreeModel *model = gtk_tree_model_filter_get_model(filter);
+ if (!model) return ret_value; // error message was already handled.
+
+ GtkTreeIter iter;
+ gboolean success = gtk_tree_model_get_iter(model, &iter, row);
+ if (!success) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Failed to fill 'GtkTreeIter'.")
+ ));
+
+ return ret_value;
+ }
+
+ GValue property = G_VALUE_INIT;
+ gtk_tree_model_get_value(model, &iter, property_index, &property);
+
+ return property;
+}
+
+/**
+ * @brief This function dumps all properties of a session to the console.
+ * It can/should be used with: \n
+ * gtk_tree_model_foreach(GTK_TREE_MODEL(model), (GtkTreeModelForeachFunc) \n
+ * rmplugin_x2go_dump_session_properties, \n
+ * dialog);
+ */
+/*static void rmplugin_x2go_dump_session_properties(GtkTreeModel *model, GtkTreePath *path,
+ GtkTreeIter *iter, GtkWidget *dialog)
+{
+ //REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ g_debug(_("Properties for session with path '%s':"), gtk_tree_path_to_string(path));
+ for (guint i = 0; i < SESSION_NUM_PROPERTIES; i++) {
+ GValue property = G_VALUE_INIT;
+ property = rmplugin_x2go_session_chooser_get_property(dialog, i, path);
+
+ gchar* display_name = rmplugin_x2go_session_property_to_string(i);
+ g_assert(display_name && "Could not get display name for a property!");
+
+ if (i == SESSION_DIALOG_IS_VISIBLE) {
+ g_assert(G_VALUE_HOLDS_BOOLEAN(&property) && "GValue does not "
+ "hold a boolean!");
+ g_debug("\t%s: '%s'", display_name,
+ g_value_get_boolean(&property) ? "TRUE" : "FALSE");
+ } else {
+ g_assert(G_VALUE_HOLDS_STRING(&property) && "GValue does not "
+ "hold a string!");
+ g_debug("\t%s: '%s'", display_name, g_value_get_string(&property));
+ }
+ }
+}*/
+
+/**
+ * @brief This function synchronously spawns a pyhoca-cli process with argv as arguments.
+ * @param argc Number of arguments.
+ * @param argv Arguments as string array. \n
+ * Last elements has to be NULL. \n
+ * Strings will get freed automatically.
+ * @param error Will be filled with an error message on fail.
+ * @param env String array of enviroment variables. \n
+ * The list is NULL terminated and each item in
+ * the list is of the form `NAME=VALUE`.
+ *
+ * @returns Returns either standard output string or NULL if it failed.
+ */
+static gchar* rmplugin_x2go_spawn_pyhoca_process(guint argc, gchar* argv[],
+ GError** error, gchar** env)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!argv) {
+ gchar* errmsg = g_strdup_printf(
+ _("Internal error: %s"),
+ _("parameter 'argv' is 'NULL'.")
+ );
+ REMMINA_PLUGIN_CRITICAL("%s", errmsg);
+ g_set_error(error, 1, 1, "%s", errmsg);
+ return NULL;
+ }
+
+ if (!error) {
+ // Can't report error message back since 'error' is NULL.
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("parameter 'error' is 'NULL'.")
+ ));
+ return NULL;
+ }
+
+ if (!env || !env[0]) {
+ gchar* errmsg = g_strdup_printf(
+ _("Internal error: %s"),
+ _("parameter 'env' is either invalid or uninitialized.")
+ );
+ REMMINA_PLUGIN_CRITICAL("%s", errmsg);
+ g_set_error(error, 1, 1, "%s", errmsg);
+ return NULL;
+ }
+
+ gint exit_code = 0;
+ gchar *standard_out;
+ // Just supresses pyhoca-cli's help message when pyhoca-cli's version is too old.
+ gchar *standard_err;
+
+ gboolean success_ret = g_spawn_sync(NULL, argv, env, G_SPAWN_SEARCH_PATH, NULL,
+ NULL, &standard_out, &standard_err,
+ &exit_code, error);
+
+ REMMINA_PLUGIN_INFO("%s", _("Started PyHoca-CLI with the following arguments:"));
+ // Print every argument except passwords. Free all arg strings.
+ for (gint i = 0; i < argc - 1; i++) {
+ gchar* curr_arg = argv[i];
+
+ if (g_str_equal(curr_arg, "--password") ||
+ g_str_equal(curr_arg, "--ssh-passphrase")) {
+ g_printf("%s ", curr_arg);
+ g_printf("XXXXXX ");
+ g_free(curr_arg);
+ g_free(argv[++i]);
+ continue;
+ } else {
+ g_printf("%s ", curr_arg);
+ g_free(curr_arg);
+ }
+ }
+ g_printf("\n");
+
+ /* TOO VERBOSE: */
+ /*
+ REMMINA_PLUGIN_DEBUG("%s", _("Started PyHoca-CLI with the "
+ "following environment variables:"));
+ REMMINA_PLUGIN_DEBUG("%s", g_strjoinv("\n", env));
+ */
+
+ if (standard_err && strlen(standard_err) > 0) {
+ if (g_str_has_prefix(standard_err, "pyhoca-cli: error: a socket error "
+ "occured while establishing the connection:")) {
+ // Log error into GUI.
+ gchar* errmsg = g_strdup_printf(
+ _("The necessary PyHoca-CLI process has encountered a "
+ "internet connection problem.")
+ );
+
+ // Log error into debug window and stdout
+ REMMINA_PLUGIN_CRITICAL("%s:\n%s", errmsg, standard_err);
+ g_set_error(error, 1, 1, "%s", errmsg);
+ return NULL;
+ } else {
+ gchar* errmsg = g_strdup_printf(
+ _("Could not start "
+ "PyHoca-CLI:\n%s"),
+ standard_err
+ );
+ REMMINA_PLUGIN_CRITICAL("%s", errmsg);
+ g_set_error(error, 1, 1, "%s", errmsg);
+ return NULL;
+ }
+ } else if (!success_ret || (*error) || strlen(standard_out) <= 0 || exit_code) {
+ if (!(*error)) {
+ gchar* errmsg = g_strdup_printf(
+ _("An unknown error occured while trying "
+ "to start PyHoca-CLI. Exit code: %i"),
+ exit_code);
+ REMMINA_PLUGIN_WARNING("%s", errmsg);
+ g_set_error(error, 1, 1, "%s", errmsg);
+ } else {
+ gchar* errmsg = g_strdup_printf(
+ _("An unknown error occured while trying to start "
+ "PyHoca-CLI. Exit code: %i. Error: '%s'"),
+ exit_code, (*error)->message);
+ REMMINA_PLUGIN_WARNING("%s", errmsg);
+ }
+
+ return NULL;
+ }
+
+ return standard_out;
+}
+
+/**
+ * @brief Stores all necessary information needed for retrieving sessions from
+ * a X2Go server.
+ */
+struct _ConnectionData {
+ gchar* host;
+ gchar* username;
+ gchar* password;
+ gchar* ssh_privatekey;
+ gchar* ssh_passphrase;
+};
+
+/**
+ * @brief Either sets a specific row visible or invisible.
+ * Also handles 'terminate' and 'resume' buttons of session-chooser-dialog.
+ * If there are no sessions available anymore, disable all buttons which are not 'new'
+ * and if a session is available again, enable them.
+ *
+ * @param path Describes which row. (GtkTreePath*) \n
+ * Should be from GtkTreeModelFilter's perspective!
+ * @param value TRUE = row is visible & FALSE = row is invisible (gboolean)
+ * @param dialog Session-Chooser-Dialog (GtkDialog*)
+ * @return Returns TRUE if successful. (gboolean)
+ */
+static gboolean rmplugin_x2go_session_chooser_set_row_visible(GtkTreePath *path,
+ gboolean value,
+ GtkDialog *dialog) {
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!path || !dialog) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Neither the 'path' nor 'dialog' parameters are initialized.")
+ ));
+ return FALSE;
+ }
+
+ GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(
+ GTK_WIDGET(dialog), NULL);
+ GtkTreeModel *model = gtk_tree_model_filter_get_model(filter);
+
+ // error message was already handled.
+ if (!model) return FALSE;
+
+ GtkTreeIter iter;
+ if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path)) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("GtkTreePath 'path' describes a non-existing row!")
+ ));
+ return FALSE;
+ }
+
+
+ // Make session either visible or invisible.
+ gtk_list_store_set(GTK_LIST_STORE(model), &iter,
+ SESSION_DIALOG_IS_VISIBLE, value, -1);
+
+ // Update row.
+ gtk_tree_model_row_changed(GTK_TREE_MODEL(model), path, &iter);
+
+ /* Get IS_VISIBLE flag of a session. */
+ // GValue ret_value = G_VALUE_INIT;
+ // ret_value = rmplugin_x2go_session_chooser_get_property(GTK_WIDGET(dialog),
+ // SESSION_DIALOG_IS_VISIBLE,
+ // path);
+ // g_debug("Is visible: %s", g_value_get_boolean(&ret_value) ? "TRUE" : "FALSE");
+
+
+ GtkWidget *term_button = gtk_dialog_get_widget_for_response(
+ GTK_DIALOG(dialog),
+ SESSION_CHOOSER_RESPONSE_TERMINATE);
+ GtkWidget *resume_button = gtk_dialog_get_widget_for_response(
+ GTK_DIALOG(dialog),
+ SESSION_CHOOSER_RESPONSE_CHOOSE);
+
+ // If no (visible) row is left to terminate disable terminate and resume buttons.
+ gint rows_amount = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(filter), NULL);
+ if (rows_amount <= 0) {
+ gtk_widget_set_sensitive(term_button, FALSE);
+ gtk_widget_set_sensitive(resume_button, FALSE);
+ } else {
+ gtk_widget_set_sensitive(term_button, TRUE);
+ gtk_widget_set_sensitive(resume_button, TRUE);
+ }
+
+ // Success, yay!
+ return TRUE;
+}
+
+static gboolean rmplugin_x2go_verify_connection_data(struct _ConnectionData *connect_data) {
+ /* Check connect_data. */
+ if (!connect_data ||
+ !connect_data->host ||
+ !connect_data->username ||
+ strlen(connect_data->host) <= 0 ||
+ strlen(connect_data->username) <= 0)
+ {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("'Invalid connection data.'")
+ ));
+
+ return FALSE;
+ }
+
+ if (!connect_data->password && (!connect_data->ssh_privatekey ||
+ strlen(connect_data->ssh_privatekey) <= 0)) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("'Invalid connection data.'")
+ ));
+ } else {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * @brief Terminates a specific X2Go session using pyhoca-cli.
+ *
+ * @param custom_data X2GoCustomUserData structure with the following: \n
+ * gp -> gp (RemminaProtocolWidget*) \n
+ * dialog_data -> dialog data (struct _DialogData*) \n
+ * connect_data -> connection data (struct _ConnectionData*) \n
+ * opt1 -> selected row (GtkTreePath*) \n
+ * opt2 -> session-selection-dialog (GtkDialog*)
+ *
+ * @return G_SOURCE_REMOVE (FALSE), #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more
+ * memorable names for the return value. See GLib docs. \n
+ * https://docs.gtk.org/glib/const.SOURCE_REMOVE.html
+ */
+static gboolean rmplugin_x2go_pyhoca_terminate_session(X2GoCustomUserData *custom_data)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!custom_data || !custom_data->gp ||
+ !custom_data->dialog_data || !custom_data->connect_data ||
+ !custom_data->opt1 || !custom_data->opt2) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Parameter 'custom_data' is not fully initialized!")
+ ));
+
+ return G_SOURCE_REMOVE;
+ }
+
+ // Extract data passed by X2GoCustomUserData *custom_data.
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(custom_data->gp);
+ //struct _DialogData *ddata = (struct _DialogData*) custom_data->dialog_data;
+ struct _ConnectionData *connect_data = (struct _ConnectionData*) custom_data->connect_data;
+ GtkTreePath* selected_row = (GtkTreePath*) custom_data->opt1;
+ GtkDialog *dialog = GTK_DIALOG(custom_data->opt2);
+
+ gchar *host = NULL;
+ gchar *username = NULL;
+ gchar *password = NULL;
+ gchar *ssh_privatekey = NULL;
+ gchar *ssh_passphrase = NULL;
+ gboolean valid = rmplugin_x2go_verify_connection_data(connect_data);
+ if (valid) {
+ if (connect_data->password) password = connect_data->password;
+ if (connect_data->ssh_privatekey) {
+ ssh_privatekey = connect_data->ssh_privatekey;
+
+ if (connect_data->ssh_passphrase) {
+ ssh_passphrase = connect_data->ssh_passphrase;
+ }
+ }
+
+ host = connect_data->host;
+ username = connect_data->username;
+ } else {
+ return G_SOURCE_REMOVE;
+ }
+
+ GValue value = rmplugin_x2go_session_chooser_get_property(GTK_WIDGET(dialog),
+ SESSION_SESSION_ID,
+ selected_row);
+ // error message was handled already.
+ if (!G_VALUE_HOLDS_STRING(&value)) return G_SOURCE_REMOVE;
+ const gchar *session_id = g_value_get_string(&value);
+
+ // We will now start pyhoca-cli with only the '--terminate $SESSION_ID' option.
+ // (and of course auth related stuff)
+ gchar *argv[50];
+ gint argc = 0;
+
+ argv[argc++] = g_strdup("pyhoca-cli");
+
+ argv[argc++] = g_strdup("--server"); // Not listed as feature.
+ argv[argc++] = g_strdup_printf("%s", host);
+
+ if (FEATURE_AVAILABLE(gpdata, "USERNAME")) {
+ argv[argc++] = g_strdup("-u");
+ if (username) {
+ argv[argc++] = g_strdup_printf("%s", username);
+ } else {
+ argv[argc++] = g_strdup_printf("%s", g_get_user_name());
+ }
+ } else {
+ REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
+ return G_SOURCE_REMOVE;
+ }
+
+ if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) {
+ if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) {
+ argv[argc++] = g_strdup("--auth-attempts");
+ argv[argc++] = g_strdup_printf ("%i", 0);
+ } else {
+ REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS"));
+ }
+ if (strlen(password) > 0) {
+ argv[argc++] = g_strdup("--force-password");
+ argv[argc++] = g_strdup("--password");
+ argv[argc++] = g_strdup_printf("%s", password);
+ }
+ } else if (!password) {
+ REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
+ return G_SOURCE_REMOVE;
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "TERMINATE")) {
+ argv[argc++] = g_strdup("--terminate");
+ argv[argc++] = g_strdup_printf("%s", session_id);
+ } else {
+ REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("TERMINATE"));
+ return G_SOURCE_REMOVE;
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) {
+ argv[argc++] = g_strdup("--non-interactive");
+ } else {
+ REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) {
+ if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) {
+ argv[argc++] = g_strdup("--ssh-privkey");
+ argv[argc++] = g_strdup_printf("%s", ssh_privatekey);
+
+ if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) {
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) {
+ argv[argc++] = g_strdup("--ssh-passphrase");
+ argv[argc++] = g_strdup_printf("%s", ssh_passphrase);
+ } else {
+ REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE"));
+ }
+ }
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY"));
+ }
+
+ argv[argc++] = NULL;
+
+ GError* error = NULL;
+ gchar** envp = g_get_environ();
+ rmplugin_x2go_spawn_pyhoca_process(argc, argv, &error, envp);
+ g_strfreev(envp);
+
+ if (error) {
+ gchar *err_msg = g_strdup_printf(
+ _("Could not terminate X2Go session '%s':\n%s"),
+ session_id,
+ error->message
+ );
+
+ REMMINA_PLUGIN_CRITICAL("%s", err_msg);
+
+ struct _DialogData *err_ddata = g_new0(struct _DialogData, 1);
+ err_ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(dialog)));
+ err_ddata->flags = GTK_DIALOG_MODAL;
+ err_ddata->type = GTK_MESSAGE_ERROR;
+ err_ddata->buttons = GTK_BUTTONS_OK;
+ err_ddata->title = _("An error occured.");
+ err_ddata->message = err_msg;
+ // We don't need the response.
+ err_ddata->callbackfunc = NULL;
+ // We don't need a custom dialog either.
+ err_ddata->dialog_factory_func = NULL;
+ err_ddata->dialog_factory_data = NULL;
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ */
+ custom_data->gp = custom_data->gp;
+ custom_data->dialog_data = err_ddata;
+ custom_data->connect_data = NULL;
+ custom_data->opt1 = NULL;
+ custom_data->opt2 = NULL;
+
+ IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data);
+
+ // Too verbose:
+ // GtkTreeModel *model = gtk_tree_model_filter_get_model(
+ // GTK_TREE_MODEL_FILTER(filter));
+ // gtk_tree_model_foreach(GTK_TREE_MODEL(model), (GtkTreeModelForeachFunc)
+ // rmplugin_x2go_dump_session_properties, dialog);
+
+ // Set row visible again since we could not terminate the session.
+ if (!rmplugin_x2go_session_chooser_set_row_visible(selected_row, TRUE,
+ dialog)) {
+ // error message was already handled.
+ return G_SOURCE_REMOVE;
+ }
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * @brief Gets executed on dialog's 'response' signal
+ *
+ * @param custom_data X2GoCustomUserData*: \n
+ * gp -> gp (RemminaProtocolWidget*) \n
+ * dialog_data -> dialog data (struct _DialogData*) \n
+ * connect_data -> connection data (struct _ConnectionData*)
+ * @param response_id See GTK 'response' signal.
+ * @param self The dialog itself.
+ *
+ * @return gboolean, #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable
+ * names for the return value. See GLib docs. \n
+ * https://docs.gtk.org/glib/const.SOURCE_REMOVE.html
+ */
+static gboolean rmplugin_x2go_session_chooser_callback(X2GoCustomUserData* custom_data,
+ gint response_id,
+ GtkDialog *self)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!custom_data || !custom_data->gp || !custom_data->dialog_data ||
+ !custom_data->connect_data) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Parameter 'custom_data' is not initialized!")
+ ));
+
+ return G_SOURCE_REMOVE;
+ }
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget*) custom_data->gp;
+
+ // Don't need to run other stuff, if the user just wants a new session.
+ // Also it can happen, that no session is there anymore which can be selected!
+ if (response_id == SESSION_CHOOSER_RESPONSE_NEW) {
+ REMMINA_PLUGIN_DEBUG("The user explicitly requested a new session. "
+ "Creating a new session…");
+ SET_RESUME_SESSION(gp, NULL);
+
+ // Unstucking main process. Telling it that a session has been selected.
+ // We use a trick here. As long as there is something other
+ // than 0 stored, a session is selected. So we use the gpointer as a gboolean.
+ SET_SESSION_SELECTED(gp, (gpointer) TRUE);
+
+ gtk_widget_destroy(GTK_WIDGET(self));
+
+ return G_SOURCE_REMOVE;
+ }
+
+ // This assumes that there are sessions which can be selected!
+ GValue value = rmplugin_x2go_session_chooser_get_property(
+ GTK_WIDGET(self),
+ SESSION_SESSION_ID,
+ NULL // Let the function search for the selected row.
+ );
+
+ // error message was handled already.
+ if (!G_VALUE_HOLDS_STRING(&value)) return G_SOURCE_REMOVE;
+
+ gchar *session_id = (gchar*) g_value_get_string(&value);
+
+ if (response_id == SESSION_CHOOSER_RESPONSE_CHOOSE) {
+ if (!session_id || strlen(session_id) <= 0) {
+ REMMINA_PLUGIN_DEBUG(
+ "%s",
+ _("Could not get session ID from session chooser dialog.")
+ );
+ SET_RESUME_SESSION(gp, NULL);
+ } else {
+ SET_RESUME_SESSION(gp, session_id);
+
+ REMMINA_PLUGIN_MESSAGE("%s", g_strdup_printf(
+ _("Resuming session: '%s'"),
+ session_id
+ ));
+ }
+ } else if (response_id == SESSION_CHOOSER_RESPONSE_TERMINATE) {
+ if (!session_id || strlen(session_id) <= 0) {
+ REMMINA_PLUGIN_DEBUG(
+ "%s",
+ _("Could not get session ID from session chooser dialog.")
+ );
+ SET_RESUME_SESSION(gp, NULL);
+ } else {
+ SET_RESUME_SESSION(gp, session_id);
+
+ REMMINA_PLUGIN_MESSAGE("%s", g_strdup_printf(
+ _("Terminating session: '%s'"),
+ session_id
+ ));
+ }
+
+ GtkTreePath *path = rmplugin_x2go_session_chooser_get_selected_row(
+ GTK_WIDGET(self));
+ // error message was already handled.
+ if (!path) return G_SOURCE_REMOVE;
+
+ // Actually set row invisible.
+ if (!rmplugin_x2go_session_chooser_set_row_visible(path, FALSE, self)) {
+ // error message was already handled.
+ return G_SOURCE_REMOVE;
+ }
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ * connect_data -> connection data (struct _ConnectionData*)
+ * opt1 -> selected row (GtkTreePath*)
+ * opt2 -> session selection dialog (GtkDialog*)
+ */
+ // everything else is already initialized.
+ custom_data->opt1 = path;
+ custom_data->opt2 = self;
+
+ // Actually start pyhoca-cli process with --terminate $session_id.
+ g_thread_new("terminate-session-thread",
+ (GThreadFunc) rmplugin_x2go_pyhoca_terminate_session,
+ custom_data);
+
+ // Dialog should stay open.
+ return G_SOURCE_CONTINUE;
+ } else {
+ REMMINA_PLUGIN_DEBUG("User clicked dialog away. "
+ "Creating a new session then.");
+ SET_RESUME_SESSION(gp, NULL);
+ }
+
+ // Unstucking main process. Telling it that a session has been selected.
+ // We use a trick here. As long as there is something other
+ // than 0 stored, a session is selected. So we use the gpointer as a gboolean.
+ SET_SESSION_SELECTED(gp, (gpointer) TRUE);
+
+ gtk_widget_destroy(GTK_WIDGET(self));
+
+ return G_SOURCE_REMOVE;
+}
+
+#define RMPLUGIN_X2GO_FEATURE_GTKSOCKET 1
+
+/* Forward declaration */
+static RemminaProtocolPlugin rmplugin_x2go;
+
+/* When more than one NX sessions is connecting in progress, we need this mutex and array
+ * to prevent them from stealing the same window ID.
+ */
+static pthread_mutex_t remmina_x2go_init_mutex;
+static GArray *remmina_x2go_window_id_array;
+
+/* ------------- Support for execution on main thread of GTK functions ------------- */
+struct onMainThread_cb_data
+{
+ enum { FUNC_GTK_SOCKET_ADD_ID } func;
+
+ GtkSocket* sk;
+ Window w;
+
+ /* Mutex for thread synchronization */
+ pthread_mutex_t mu;
+ /* Flag to catch cancellations */
+ gboolean cancelled;
+};
+
+static gboolean onMainThread_cb(struct onMainThread_cb_data *d)
+{
+ TRACE_CALL(__func__);
+ if (!d->cancelled) {
+ switch (d->func) {
+ case FUNC_GTK_SOCKET_ADD_ID:
+ gtk_socket_add_id(d->sk, d->w);
+ break;
+ }
+ pthread_mutex_unlock(&d->mu);
+ } else {
+ /* thread has been cancelled, so we must free d memory here */
+ g_free(d);
+ }
+ return G_SOURCE_REMOVE;
+}
+
+
+static void onMainThread_cleanup_handler(gpointer data)
+{
+ TRACE_CALL(__func__);
+ struct onMainThread_cb_data *d = data;
+ d->cancelled = TRUE;
+}
+
+static void onMainThread_schedule_callback_and_wait(struct onMainThread_cb_data *d)
+{
+ TRACE_CALL(__func__);
+ d->cancelled = FALSE;
+ pthread_cleanup_push(onMainThread_cleanup_handler, d);
+ pthread_mutex_init(&d->mu, NULL);
+ pthread_mutex_lock(&d->mu);
+ gdk_threads_add_idle((GSourceFunc)onMainThread_cb, (gpointer) d);
+
+ pthread_mutex_lock(&d->mu);
+
+ pthread_cleanup_pop(0);
+ pthread_mutex_unlock(&d->mu);
+ pthread_mutex_destroy(&d->mu);
+}
+
+static void onMainThread_gtk_socket_add_id(GtkSocket* sk, Window w)
+{
+ TRACE_CALL(__func__);
+
+ struct onMainThread_cb_data *d;
+
+ d = g_new0(struct onMainThread_cb_data, 1);
+ d->func = FUNC_GTK_SOCKET_ADD_ID;
+ d->sk = sk;
+ d->w = w;
+
+ onMainThread_schedule_callback_and_wait(d);
+ g_free(d);
+}
+/* /-/-/-/-/-/-/ Support for execution on main thread of GTK functions /-/-/-/-/-/-/ */
+
+static void rmplugin_x2go_remove_window_id (Window window_id)
+{
+ gint i;
+ gboolean already_seen = FALSE;
+
+ pthread_mutex_lock(&remmina_x2go_init_mutex);
+ for (i = 0; i < remmina_x2go_window_id_array->len; i++) {
+ if (g_array_index(remmina_x2go_window_id_array, Window, i) == window_id) {
+ already_seen = TRUE;
+ REMMINA_PLUGIN_DEBUG("Window of X2Go Agent with ID [0x%lx] seen already.",
+ window_id);
+ break;
+ }
+ }
+
+ if (already_seen) {
+ g_array_remove_index_fast(remmina_x2go_window_id_array, i);
+ REMMINA_PLUGIN_DEBUG("Forgetting about window of X2Go Agent with ID [0x%lx]…",
+ window_id);
+ }
+
+ pthread_mutex_unlock(&remmina_x2go_init_mutex);
+}
+
+/**
+ * @returns: FALSE. This source should be removed from main loop.
+ * #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable
+ * names for the return value.
+ */
+static gboolean rmplugin_x2go_cleanup(RemminaProtocolWidget *gp)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ gchar *server;
+ gint port;
+
+ RemminaFile *remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
+ rm_plugin_service->get_server_port(rm_plugin_service->file_get_string(remminafile, "server"),
+ 22,
+ &server,
+ &port);
+
+ REMMINA_PLUGIN_AUDIT(_("Disconnected from %s:%d via X2Go"), server, port);
+ g_free(server), server = NULL;
+
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+ if (gpdata == NULL) {
+ REMMINA_PLUGIN_DEBUG("Exiting since gpdata is already 'NULL'…");
+ return G_SOURCE_REMOVE;
+ }
+
+ if (gpdata->thread) {
+ pthread_cancel(gpdata->thread);
+ if (gpdata->thread) pthread_join(gpdata->thread, NULL);
+ }
+
+ if (gpdata->window_id) {
+ rmplugin_x2go_remove_window_id(gpdata->window_id);
+ }
+
+ if (gpdata->pidx2go) {
+ kill(gpdata->pidx2go, SIGTERM);
+ g_spawn_close_pid(gpdata->pidx2go);
+ gpdata->pidx2go = 0;
+ }
+
+ if (gpdata->display) {
+ XSetErrorHandler(gpdata->orig_handler);
+ XCloseDisplay(gpdata->display);
+ gpdata->display = NULL;
+ }
+
+ g_object_steal_data(G_OBJECT(gp), "plugin-data");
+ rm_plugin_service->protocol_plugin_signal_connection_closed(gp);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean rmplugin_x2go_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (gpdata->disconnected) {
+ REMMINA_PLUGIN_DEBUG("Doing nothing since the plugin is already disconnected.");
+ return G_SOURCE_REMOVE;
+ }
+
+ rmplugin_x2go_cleanup(gp);
+
+ // Try again.
+ return G_SOURCE_CONTINUE;
+}
+
+static void rmplugin_x2go_pyhoca_cli_exited(GPid pid,
+ gint status,
+ RemminaProtocolWidget *gp)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+ if (!gpdata) {
+ REMMINA_PLUGIN_DEBUG("Doing nothing as the disconnection "
+ "has already been handled.");
+ return;
+ }
+
+ if (gpdata->pidx2go <= 0) {
+ REMMINA_PLUGIN_DEBUG("Doing nothing since pyhoca-cli was expected to stop.");
+ return;
+ }
+
+ REMMINA_PLUGIN_CRITICAL("%s", _("PyHoca-CLI exited unexpectedly. "
+ "This connection will now be closed."));
+
+ struct _DialogData *ddata = g_new0(struct _DialogData, 1);
+ ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp)));
+ ddata->flags = GTK_DIALOG_MODAL;
+ ddata->type = GTK_MESSAGE_ERROR;
+ ddata->buttons = GTK_BUTTONS_OK;
+ ddata->title = _("An error occured.");
+ ddata->message = _("The necessary child process 'pyhoca-cli' stopped unexpectedly.\n"
+ "Please check your profile settings and PyHoca-CLI's output for "
+ "possible errors. Also ensure the remote server is "
+ "reachable and you're using the right credentials.");
+ // We don't need the response.
+ ddata->callbackfunc = NULL;
+ // We don't need a custom dialog either.
+ ddata->dialog_factory_func = NULL;
+ ddata->dialog_factory_data = NULL;
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ */
+ X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1);
+ g_assert(custom_data && "custom_data could not be initialized.");
+
+ custom_data->gp = gp;
+ custom_data->dialog_data = ddata;
+ custom_data->connect_data = NULL;
+ custom_data->opt1 = NULL;
+
+ IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data);
+
+ // 1 Second. Give `Dialog` chance to open.
+ usleep(1000 * 1000);
+
+ rmplugin_x2go_close_connection(gp);
+}
+
+/**
+ * @brief Saves s_password and s_username if set.
+ * @returns either TRUE or FALSE. If FALSE gets returned, `errmsg` is set.
+ */
+static gboolean rmplugin_x2go_save_credentials(RemminaFile* remminafile,
+ gchar* s_username, gchar* s_password,
+ gchar* errmsg)
+{
+ // User has requested to save credentials. We put all the new credentials
+ // into remminafile->settings. They will be saved later, on successful
+ // connection, by rcw.c
+ if (s_password && s_username) {
+ if (g_strcmp0(s_username, "") == 0) {
+ g_strlcpy(errmsg, _("Can't save empty username!"), 512);
+ //REMMINA_PLUGIN_CRITICAL("%s", errmsg); // No need.
+ return FALSE;
+ }
+
+ // We allow the possibility to set an empty password because a X2Go
+ // session can be still made using keyfiles or similar.
+ rm_plugin_service->file_set_string(remminafile, "password",
+ s_password);
+ rm_plugin_service->file_set_string(remminafile, "username",
+ s_username);
+ } else {
+ g_strlcpy(errmsg, g_strdup_printf(
+ _("Internal error: %s"),
+ _("Could not save new credentials.")
+ ), 512);
+
+ REMMINA_PLUGIN_CRITICAL("%s", _("Could not save "
+ "new credentials: 's_password' or "
+ "'s_username' strings were not set."));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+
+/**
+ * @brief Asks the user for a username and password.
+ *
+ * @param errmsg Pointer to error message string (set if function failed).
+ * @param passphrase gchar** Passphrase which will be used to unlock SSH key.
+ *
+ * @returns FALSE if auth failed and TRUE on success.
+ */
+static gboolean rmplugin_x2go_get_ssh_passphrase(RemminaProtocolWidget *gp, gchar *errmsg,
+ gchar **passphrase)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ g_assert(errmsg != NULL);
+ g_assert(gp != NULL);
+
+ if ((*passphrase) == NULL) {
+ // Just setting NULL password to empty password.
+ (*passphrase) = g_strdup("");
+ }
+
+ gint ret = rm_plugin_service->protocol_plugin_init_auth(
+ gp, 0, _("Enter password to unlock the SSH key:"),
+ NULL, *passphrase, NULL, NULL
+ );
+
+ if (ret == GTK_RESPONSE_OK) {
+ gchar *s_passphrase = rm_plugin_service->protocol_plugin_init_get_password(gp);
+ if (s_passphrase) {
+ (*passphrase) = g_strdup(s_passphrase);
+ g_free(s_passphrase);
+ }
+ } else {
+ g_strlcpy(errmsg, _("Password input cancelled. Aborting…"), 512);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+
+/**
+ * @brief Asks the user for a username and password.
+ *
+ * @param errmsg Pointer to error message string (set if function failed).
+ * @param username Pointer to default username. Gets set to new username on success.
+ * @param password Pointer to default password. Gets set to new password on success.
+ *
+ * @returns FALSE if auth failed and TRUE on success.
+ */
+static gboolean rmplugin_x2go_get_auth(RemminaProtocolWidget *gp, gchar* errmsg,
+ gchar** default_username, gchar** default_password)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ g_assert(errmsg != NULL);
+ g_assert(gp != NULL);
+ g_assert(default_username != NULL);
+ g_assert(default_password != NULL);
+
+ // default_username is probably NULL because the user didn't configure any
+ // username in the profile settings.
+ if ((*default_username) == NULL) {
+ gchar* l_errmsg = g_strdup_printf(
+ _("Tip: Check the 'Save password' checkbox or manually input your "
+ "X2Go username and password in the profile settings to store "
+ "them for faster logins.")
+ );
+ REMMINA_PLUGIN_MESSAGE("%s", l_errmsg);
+ (*default_username) = g_strdup("");
+ }
+
+ // default_password is probably NULL because something did go wrong at the
+ // secret-plugin. For example: The user didn't input a password for keyring or
+ // the user simply didn't configure a password in the profile settings.
+ if ((*default_password) == NULL) {
+ (*default_password) = g_strdup("");
+ }
+
+ gchar *s_username, *s_password;
+ gint ret;
+ gboolean save;
+ gboolean disable_password_storing;
+ RemminaFile *remminafile;
+
+ remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
+
+ disable_password_storing = rm_plugin_service->file_get_int(
+ remminafile, "disablepasswordstoring", FALSE
+ );
+
+ ret = rm_plugin_service->protocol_plugin_init_auth(
+ gp, (disable_password_storing ? 0 :
+ REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD |
+ REMMINA_MESSAGE_PANEL_FLAG_USERNAME),
+ _("Enter X2Go credentials"),
+ (*default_username), (*default_password), NULL, NULL
+ );
+
+ if (ret == GTK_RESPONSE_OK) {
+ s_username = rm_plugin_service->protocol_plugin_init_get_username(gp);
+ s_password = rm_plugin_service->protocol_plugin_init_get_password(gp);
+ if (rm_plugin_service->protocol_plugin_init_get_savepassword(gp))
+ rm_plugin_service->file_set_string(
+ remminafile, "password", s_password
+ );
+
+ // Should be renamed to protocol_plugin_init_get_savecredentials()?!
+ save = rm_plugin_service->protocol_plugin_init_get_savepassword(gp);
+ if (save) {
+ if (!rmplugin_x2go_save_credentials(remminafile, s_username,
+ s_password, errmsg)) {
+ return FALSE;
+ }
+ }
+ if (s_username) {
+ (*default_username) = g_strdup(s_username);
+ g_free(s_username);
+ }
+ if (s_password) {
+ (*default_password) = g_strdup(s_password);
+ g_free(s_password);
+ }
+ } else {
+ g_strlcpy(errmsg, _("Authentication cancelled. Aborting…"), 512);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * @brief Executes 'pyhoca-cli --list-sessions' for username@host.
+ *
+ * @param gp RemminaProtocolWidget* is used to get the x2go-plugin data.
+ * @param error This is where a error message will be when NULL gets returned.
+ * @param connect_data struct _ConnectionData* which stores all necessary information
+ * needed for retrieving sessions from a X2Go server.
+ *
+ * @returns Standard output of pyhoca-cli command.
+ * If NULL then errmsg is set to user-friendly error message.
+ */
+static gchar* rmplugin_x2go_get_pyhoca_sessions(RemminaProtocolWidget* gp, GError **error,
+ struct _ConnectionData* connect_data)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+ RemminaPluginX2GoData* gpdata = GET_PLUGIN_DATA(gp);
+
+ gchar *host = NULL;
+ gchar *username = NULL;
+ gchar *password = NULL;
+ gchar *ssh_privatekey = NULL;
+ gchar *ssh_passphrase = NULL;
+ gboolean valid = rmplugin_x2go_verify_connection_data(connect_data);
+
+ if (valid) {
+ if (connect_data->password) password = connect_data->password;
+ if (connect_data->ssh_privatekey) {
+ ssh_privatekey = connect_data->ssh_privatekey;
+
+ if (connect_data->ssh_passphrase) {
+ ssh_passphrase = connect_data->ssh_passphrase;
+ }
+ }
+
+ host = connect_data->host;
+ username = connect_data->username;
+ } else {
+ return G_SOURCE_REMOVE;
+ }
+
+ // We will now start pyhoca-cli with only the '--list-sessions' option.
+
+ gchar *argv[50];
+ gint argc = 0;
+
+ argv[argc++] = g_strdup("pyhoca-cli");
+ argv[argc++] = g_strdup("--list-sessions");
+
+ argv[argc++] = g_strdup("--server"); // Not listed as feature.
+ argv[argc++] = g_strdup_printf("%s", host);
+
+ if (FEATURE_AVAILABLE(gpdata, "USERNAME")) {
+ argv[argc++] = g_strdup("-u");
+ if (username) {
+ argv[argc++] = g_strdup_printf("%s", username);
+ } else {
+ argv[argc++] = g_strdup_printf("%s", g_get_user_name());
+ }
+ } else {
+ g_set_error(error, 1, 1, "%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
+ REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
+ return NULL;
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) {
+ argv[argc++] = g_strdup("--non-interactive");
+ } else {
+ REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE"));
+ }
+
+ if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) {
+ if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) {
+ argv[argc++] = g_strdup("--auth-attempts");
+ argv[argc++] = g_strdup_printf ("%i", 0);
+ } else {
+ REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS"));
+ }
+ if (strlen(password) > 0) {
+ argv[argc++] = g_strdup("--force-password");
+ argv[argc++] = g_strdup("--password");
+ argv[argc++] = g_strdup_printf("%s", password);
+ }
+ } else if (!password) {
+ g_set_error(error, 1, 1, "%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
+ REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
+ return NULL;
+ }
+
+ // No need to catch feature-not-available error.
+ // `--quiet` is not that important.
+ if (FEATURE_AVAILABLE(gpdata, "QUIET")) {
+ argv[argc++] = g_strdup("--quiet");
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) {
+ if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) {
+ argv[argc++] = g_strdup("--ssh-privkey");
+ argv[argc++] = g_strdup_printf("%s", ssh_privatekey);
+
+ if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) {
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) {
+ argv[argc++] = g_strdup("--ssh-passphrase");
+ argv[argc++] = g_strdup_printf("%s", ssh_passphrase);
+ } else {
+ REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE"));
+ }
+ }
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY"));
+ }
+
+
+ argv[argc++] = NULL;
+
+ //#ifndef GLIB_AVAILABLE_IN_2_68
+ gchar** envp = g_get_environ();
+ gchar* envp_splitted = g_strjoinv(";", envp);
+ envp_splitted = g_strconcat(envp_splitted, ";LANG=C", (void*) NULL);
+ envp = g_strsplit(envp_splitted, ";", 0);
+ /*
+ * #else
+ * // Only available after glib version 2.68.
+ * // TODO: FIXME: NOT TESTED!
+ * GStrvBuilder* builder = g_strv_builder_new();
+ * g_strv_builder_add(builder, "LANG=C");
+ * GStrv envp = g_strv_builder_end(builder);
+ * #endif
+ */
+
+ gchar* std_out = rmplugin_x2go_spawn_pyhoca_process(argc, argv, error, envp);
+ g_strfreev(envp);
+
+ if (!std_out || *error) {
+ // If no error is set but std_out is NULL
+ // then something is not right at all.
+ // Most likely the developer forgot to add an error message. Crash.
+ g_assert((*error) != NULL);
+ return NULL;
+ }
+
+ return std_out;
+}
+
+/**
+ * @brief This function is used to parse the output of
+ * rmplugin_x2go_get_pyhoca_sessions().
+ *
+ * @param gp RemminaProtocolWidget* is used to get the x2go-plugin data.
+ * @param error This is where a error message will be when NULL gets returned.
+ * @param connect_data struct _ConnectionData* which stores all necessary information
+ * needed for retrieving sessions from a X2Go server.
+ *
+ * @returns Returns either a GList containing the IDs of every already existing session
+ * found or if the function failes, NULL.
+ *
+ * TODO: If pyhoca-cli (python-x2go) implements `--json` or similar option -> Replace
+ * entire function with JSON parsing.
+ */
+static GList* rmplugin_x2go_parse_pyhoca_sessions(RemminaProtocolWidget* gp,
+ GError **error,
+ struct _ConnectionData* connect_data)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ gchar *pyhoca_output = NULL;
+
+ pyhoca_output = rmplugin_x2go_get_pyhoca_sessions(gp, error, connect_data);
+ if (!pyhoca_output || *error) {
+ // If no error is set but pyhoca_output is NULL
+ // then something is not right at all.
+ // Most likely the developer forgot to add an error message. Crash.
+ g_assert((*error) != NULL);
+
+ return NULL;
+ }
+
+ gchar **lines_list = g_strsplit(pyhoca_output, "\n", -1);
+ // Assume at least two lines of output.
+ if (lines_list == NULL || lines_list[0] == NULL || lines_list[1] == NULL) {
+ g_set_error(error, 1, 1, "%s", _("Could not parse the output of PyHoca-CLI's "
+ "--list-sessions option. Creating a new "
+ "session now."));
+ return NULL;
+ }
+
+ gboolean found_session = FALSE;
+ GList* sessions = NULL;
+ gchar** session = NULL;
+
+ for (guint i = 0; lines_list[i] != NULL; i++) {
+ gchar* current_line = lines_list[i];
+
+ // TOO VERBOSE:
+ //REMMINA_PLUGIN_DEBUG("pyhoca-cli: %s", current_line);
+
+ // Hardcoded string "Session Name: " comes from python-x2go.
+ if (!g_str_has_prefix(current_line, "Session Name: ") && !found_session) {
+ // Doesn't begin with "Session Name: " and
+ // the current line doesn't come after that either. Skipping.
+ continue;
+ }
+
+ if (g_str_has_prefix(current_line, "Session Name: ")) {
+ gchar* session_id = NULL;
+ gchar** line_list = g_strsplit(current_line, ": ", 0);
+
+ if (line_list == NULL ||
+ line_list[0] == NULL ||
+ line_list[1] == NULL ||
+ strlen(line_list[0]) <= 0 ||
+ strlen(line_list[1]) <= 0)
+ {
+ found_session = FALSE;
+ continue;
+ }
+
+ session = malloc(sizeof(gchar*) * (SESSION_NUM_PROPERTIES+1));
+ if (!session) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Could not allocate "
+ "enough memory!"));
+ }
+ session[SESSION_NUM_PROPERTIES] = NULL;
+ sessions = g_list_append(sessions, session);
+
+ session_id = line_list[1];
+ session[SESSION_SESSION_ID] = session_id;
+
+ REMMINA_PLUGIN_INFO("%s", g_strdup_printf(
+ _("Found already existing X2Go session with ID: '%s'"),
+ session[SESSION_SESSION_ID])
+ );
+
+ found_session = TRUE;
+ continue;
+ }
+
+ if (!found_session) {
+ continue;
+ }
+
+ if (g_strcmp0(current_line, "-------------") == 0) {
+ continue;
+ }
+
+ gchar* value = NULL;
+ gchar** line_list = g_strsplit(current_line, ": ", 0);
+
+ if (line_list == NULL ||
+ line_list[0] == NULL ||
+ line_list[1] == NULL ||
+ strlen(line_list[0]) <= 0 ||
+ strlen(line_list[1]) <= 0)
+ {
+ // Probably the empty line at the end of every session.
+ found_session = FALSE;
+ continue;
+ }
+ value = line_list[1];
+
+ if (g_str_has_prefix(current_line, "cookie: ")) {
+ REMMINA_PLUGIN_DEBUG("cookie:\t'%s'", value);
+ session[SESSION_COOKIE] = value;
+ } else if (g_str_has_prefix(current_line, "agent PID: ")) {
+ REMMINA_PLUGIN_DEBUG("agent PID:\t'%s'", value);
+ session[SESSION_AGENT_PID] = value;
+ } else if (g_str_has_prefix(current_line, "display: ")) {
+ REMMINA_PLUGIN_DEBUG("display:\t'%s'", value);
+ session[SESSION_DISPLAY] = value;
+ } else if (g_str_has_prefix(current_line, "status: ")) {
+ if (g_strcmp0(value, "S") == 0) {
+ // TRANSLATORS: Please stick to X2GoClient's translation.
+ value = _("Suspended");
+ } else if (g_strcmp0(value, "R") == 0) {
+ // TRANSLATORS: Please stick to X2GoClient's translation.
+ value = _("Running");
+ } else if (g_strcmp0(value, "T") == 0) {
+ // TRANSLATORS: Please stick to X2GoClient's translation.
+ value = _("Terminated");
+ }
+ REMMINA_PLUGIN_DEBUG("status:\t'%s'", value);
+ session[SESSION_STATUS] = value;
+ } else if (g_str_has_prefix(current_line, "graphic port: ")) {
+ REMMINA_PLUGIN_DEBUG("graphic port:\t'%s'", value);
+ session[SESSION_GRAPHIC_PORT] = value;
+ } else if (g_str_has_prefix(current_line, "snd port: ")) {
+ REMMINA_PLUGIN_DEBUG("snd port:\t'%s'", value);
+ session[SESSION_SND_PORT] = value;
+ } else if (g_str_has_prefix(current_line, "sshfs port: ")) {
+ REMMINA_PLUGIN_DEBUG("sshfs port:\t'%s'", value);
+ session[SESSION_SSHFS_PORT] = value;
+ } else if (g_str_has_prefix(current_line, "username: ")) {
+ REMMINA_PLUGIN_DEBUG("username:\t'%s'", value);
+ session[SESSION_USERNAME] = value;
+ } else if (g_str_has_prefix(current_line, "hostname: ")) {
+ REMMINA_PLUGIN_DEBUG("hostname:\t'%s'", value);
+ session[SESSION_HOSTNAME] = value;
+ } else if (g_str_has_prefix(current_line, "create date: ")) {
+ REMMINA_PLUGIN_DEBUG("create date:\t'%s'", value);
+ session[SESSION_CREATE_DATE] = value;
+ } else if (g_str_has_prefix(current_line, "suspended since: ")) {
+ REMMINA_PLUGIN_DEBUG("suspended since:\t'%s'", value);
+ session[SESSION_SUSPENDED_SINCE] = value;
+ } else {
+ REMMINA_PLUGIN_DEBUG("Not supported:\t'%s'", value);
+ found_session = FALSE;
+ }
+ }
+
+ if (!sessions) {
+ g_set_error(error, 1, 1,
+ "%s", _("Could not find any sessions on remote machine. Creating a new "
+ "session now.")
+ );
+
+ // returning NULL with `error` set.
+ }
+
+ return sessions;
+}
+
+/**
+ * @brief Asks the user, with the help of a dialog, to continue an already existing
+ * session, terminate or create a new one.
+ *
+ * @param error Is set if there is something to tell the user. \n
+ * Not necessarily an *error* message.
+ * @param connect_data Stores all necessary information needed for
+ * etrieving sessions from a X2Go server.
+ * @return gchar* ID of session. Can be 'NULL' but then 'error' is set.
+ */
+static gchar* rmplugin_x2go_ask_session(RemminaProtocolWidget *gp, GError **error,
+ struct _ConnectionData* connect_data)
+{
+ if (!connect_data ||
+ !connect_data->host ||
+ !connect_data->username ||
+ !connect_data->password ||
+ strlen(connect_data->host) <= 0 ||
+ strlen(connect_data->username) <= 0)
+ // Allow empty passwords. Maybe the user wants to connect via public key?
+ {
+ g_set_error(error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("'Invalid connection data.'")
+ ));
+ return NULL;
+ }
+
+ GList *sessions_list = NULL;
+ sessions_list = rmplugin_x2go_parse_pyhoca_sessions(gp, error, connect_data);
+
+ if (!sessions_list || *error) {
+ // If no error is set but sessions_list is NULL
+ // then something is not right at all.
+ // Most likely the developer forgot to add an error message. Crash.
+ g_assert(*error != NULL);
+ return NULL;
+ }
+
+ // Prep new DialogData struct.
+ struct _DialogData *ddata = g_new0(struct _DialogData, 1);
+ ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp)));
+ ddata->flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT;
+ //ddata->type = GTK_MESSAGE_QUESTION;
+ //ddata->buttons = GTK_BUTTONS_OK; // Doesn't get used in our custom factory.
+ ddata->title = _("Choose a session to resume:");
+ ddata->message = "";
+
+ // gboolean factory(X2GoCustomUserData*, gpointer)
+ // X2GoCustomUserData*:
+ // gp -> gp (RemminaProtocolWidget*)
+ // dialog_data -> dialog data (struct _DialogData*)
+ // connect_data -> connection data (struct _ConnectionData*)
+ // gpointer: dialog_factory_data
+ ddata->callbackfunc = G_CALLBACK(rmplugin_x2go_session_chooser_callback);
+
+ // gboolean factory(X2GoCustomUserData*, gpointer)
+ // X2GoCustomUserData*:
+ // gp -> gp (RemminaProtocolWidget*)
+ // dialog_data -> dialog data (struct _DialogData*)
+ // connect_data -> connection data (struct _ConnectionData*)
+ // gpointer: dialog_factory_data
+ ddata->dialog_factory_data = sessions_list;
+ ddata->dialog_factory_func = G_CALLBACK(rmplugin_x2go_choose_session_dialog_factory);
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ */
+ X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1);
+ g_assert(custom_data && "custom_data could not be initialized.");
+
+ custom_data->gp = gp;
+ custom_data->dialog_data = ddata;
+ custom_data->connect_data = connect_data;
+ custom_data->opt1 = NULL;
+
+ // Open dialog here. Dialog rmplugin_x2go_session_chooser_callback (callbackfunc)
+ // should set SET_RESUME_SESSION.
+ IDLE_ADD((GSourceFunc)rmplugin_x2go_open_dialog, custom_data);
+
+ guint counter = 0;
+ while (!IS_SESSION_SELECTED(gp)) {
+ // 0.5 Seconds. Give dialog chance to open.
+ usleep(500 * 1000);
+
+ // Every 5 seconds
+ if (counter % 10 == 0 || counter == 0) {
+ REMMINA_PLUGIN_MESSAGE("%s", _("Waiting for user to select a session…"));
+ }
+ counter++;
+ }
+
+ gchar* chosen_resume_session = GET_RESUME_SESSION(gp);
+
+ if (!chosen_resume_session || strlen(chosen_resume_session) <= 0) {
+ g_set_error(error, 1, 1, "%s", _("No session was selected. Creating a new one."));
+ return NULL;
+ }
+
+ return chosen_resume_session;
+}
+
+static gboolean rmplugin_x2go_exec_x2go(gchar *host,
+ gint sshport,
+ gchar *username,
+ gchar *password,
+ gchar *command,
+ gchar *kbdlayout,
+ gchar *kbdtype,
+ gchar *audio,
+ gchar *clipboard,
+ gint dpi,
+ gchar *resolution,
+ gchar *ssh_privatekey,
+ RemminaProtocolWidget *gp,
+ gchar *errmsg)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+
+ gchar *argv[50];
+ gint argc = 0;
+
+ // We don't want to save any SSH passphrases on hard drive!
+ // Thats why we will always ask if needed.
+ gchar *ssh_passphrase = NULL;
+
+ if (!username || strlen(username) <= 0) {
+ // Sets `username` and `password`.
+ if (!rmplugin_x2go_get_auth(gp, errmsg, &username, &password)) {
+ return FALSE;
+ }
+ }
+
+ // Password can be *empty* but not NULL.
+ if (!password) {
+ password = g_strdup("");
+ }
+
+ if (ssh_privatekey && strlen(ssh_privatekey) > 0) {
+ // FIXME: Check if file exists and is legit private key.
+ // See: https://security.stackexchange.com/a/245767
+
+ // Get ssh_privatekey now via dialog.
+ if (!rmplugin_x2go_get_ssh_passphrase(gp, errmsg, &ssh_passphrase)) {
+ return FALSE;
+ }
+ }
+
+ struct _ConnectionData* connect_data = g_new0(struct _ConnectionData, 1);
+ connect_data->host = host;
+ connect_data->username = username;
+ connect_data->password = password;
+ connect_data->ssh_privatekey = ssh_privatekey;
+ connect_data->ssh_passphrase = ssh_passphrase;
+
+ GError *session_error = NULL;
+ gchar* resume_session_id = rmplugin_x2go_ask_session(gp, &session_error,
+ connect_data);
+
+ if (!resume_session_id || session_error || strlen(resume_session_id) <= 0) {
+ // If no error is set but session_id is NULL
+ // then something is not right at all.
+ // Most likely the developer forgot to add an error message. Crash.
+ g_assert(session_error != NULL);
+
+ REMMINA_PLUGIN_WARNING("%s", g_strdup_printf(
+ _("A non-critical error happened: %s"),
+ session_error->message
+ ));
+ } else {
+ REMMINA_PLUGIN_INFO("%s", g_strdup_printf(
+ _("User chose to resume session with ID: '%s'"),
+ resume_session_id
+ ));
+ }
+
+ argc = 0;
+ argv[argc++] = g_strdup("pyhoca-cli");
+
+ argv[argc++] = g_strdup("--server"); // Not listed as feature.
+ argv[argc++] = g_strdup_printf ("%s", host);
+
+ if (FEATURE_AVAILABLE(gpdata, "REMOTE_SSH_PORT")) {
+ argv[argc++] = g_strdup("-p");
+ argv[argc++] = g_strdup_printf ("%d", sshport);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("REMOTE_SSH_PORT"));
+ }
+
+ if (resume_session_id && strlen(resume_session_id) > 0) {
+ REMMINA_PLUGIN_INFO("%s", g_strdup_printf(
+ // TRANSLATORS: Please stick to X2GoClient's way of translating.
+ _("Resuming session '%s'…"),
+ resume_session_id
+ ));
+
+ if (FEATURE_AVAILABLE(gpdata, "RESUME")) {
+ argv[argc++] = g_strdup("--resume");
+ argv[argc++] = g_strdup_printf("%s", resume_session_id);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("RESUME"));
+ }
+ }
+
+ // Deprecated. The user either wants to continue a
+ // session or just not. No inbetween.
+ // if (!resume_session_id) {
+ // if (FEATURE_AVAILABLE(gpdata, "TRY_RESUME")) {
+ // argv[argc++] = g_strdup("--try-resume");
+ // } else {
+ // REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("TRY_RESUME"));
+ // }
+ // }
+
+ if (FEATURE_AVAILABLE(gpdata, "USERNAME")) {
+ argv[argc++] = g_strdup("-u");
+ if (username){
+ argv[argc++] = g_strdup_printf ("%s", username);
+ } else {
+ argv[argc++] = g_strdup_printf ("%s", g_get_user_name());
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
+ }
+
+ if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) {
+ if (strlen(password) > 0) {
+ argv[argc++] = g_strdup("--force-password");
+ argv[argc++] = g_strdup("--password");
+ argv[argc++] = g_strdup_printf ("%s", password);
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) {
+ argv[argc++] = g_strdup("--auth-attempts");
+ argv[argc++] = g_strdup_printf ("%i", 0);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) {
+ argv[argc++] = g_strdup("--non-interactive");
+ } else {
+ REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "COMMAND")) {
+ argv[argc++] = g_strdup("-c");
+ // FIXME: pyhoca-cli is picky about multiple quotes around
+ // the command string...
+ // argv[argc++] = g_strdup_printf ("%s", g_shell_quote(command));
+ argv[argc++] = g_strdup(command);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("COMMAND"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "KBD_LAYOUT")) {
+ if (kbdlayout) {
+ argv[argc++] = g_strdup("--kbd-layout");
+ argv[argc++] = g_strdup_printf ("%s", kbdlayout);
+ } else {
+ argv[argc++] = g_strdup("--kbd-layout");
+ argv[argc++] = g_strdup("auto");
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("KBD_LAYOUT"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "KBD_TYPE")) {
+ if (kbdtype) {
+ argv[argc++] = g_strdup("--kbd-type");
+ argv[argc++] = g_strdup_printf ("%s", kbdtype);
+ } else {
+ argv[argc++] = g_strdup("--kbd-type");
+ argv[argc++] = g_strdup("auto");
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("KBD_TYPE"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "GEOMETRY")) {
+ if (!resolution)
+ resolution = "800x600";
+ argv[argc++] = g_strdup("-g");
+ argv[argc++] = g_strdup_printf ("%s", resolution);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("GEOMETRY"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "TERMINATE_ON_CTRL_C")) {
+ argv[argc++] = g_strdup("--terminate-on-ctrl-c");
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("TERMINATE_ON_CTRL_C"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "SOUND")) {
+ if (audio) {
+ argv[argc++] = g_strdup("--sound");
+ argv[argc++] = g_strdup_printf ("%s", audio);
+ } else {
+ argv[argc++] = g_strdup("--sound");
+ argv[argc++] = g_strdup("none");
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SOUND"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "CLIPBOARD_MODE")) {
+ if (clipboard) {
+ argv[argc++] = g_strdup("--clipboard-mode");
+ argv[argc++] = g_strdup_printf("%s", clipboard);
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("CLIPBOARD_MODE"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "DPI")) {
+ // Even though we validate the users input in the Remmina Editor,
+ // manipulating profile files is still very possible…
+ // Values are extracted from pyhoca-cli.
+ if (dpi < 20 || dpi > 400) {
+ g_strlcpy(errmsg, _("DPI setting is out of bounds. Please adjust "
+ "it in profile settings."), 512);
+ // No need, start_session() will handle output.
+ //REMMINA_PLUGIN_CRITICAL("%s", errmsg);
+ return FALSE;
+ }
+ argv[argc++] = g_strdup("--dpi");
+ argv[argc++] = g_strdup_printf ("%i", dpi);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("DPI"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) {
+ if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) {
+ argv[argc++] = g_strdup("--ssh-privkey");
+ argv[argc++] = g_strdup_printf("%s", ssh_privatekey);
+
+ if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) {
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) {
+ argv[argc++] = g_strdup("--ssh-passphrase");
+ argv[argc++] = g_strdup_printf("%s", ssh_passphrase);
+ } else {
+ REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE"));
+ }
+ }
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY"));
+ }
+
+ argv[argc++] = NULL;
+
+ GError *error = NULL;
+ gchar **envp = g_get_environ();
+ gboolean success = g_spawn_async_with_pipes (NULL, argv, envp,
+ (G_SPAWN_DO_NOT_REAP_CHILD |
+ G_SPAWN_SEARCH_PATH), NULL,
+ NULL, &gpdata->pidx2go,
+ NULL, NULL, NULL, &error);
+
+ REMMINA_PLUGIN_INFO("%s", _("Started PyHoca-CLI with the following arguments:"));
+ // Print every argument except passwords. Free all arg strings.
+ for (gint i = 0; i < argc - 1; i++) {
+ gchar* curr_arg = argv[i];
+
+ if (g_str_equal(curr_arg, "--password") ||
+ g_str_equal(curr_arg, "--ssh-passphrase")) {
+ g_printf("%s ", curr_arg);
+ g_printf("XXXXXX ");
+ g_free(curr_arg);
+ g_free(argv[++i]);
+ continue;
+ } else {
+ g_printf("%s ", curr_arg);
+ g_free(curr_arg);
+ }
+ }
+ g_printf("\n");
+
+ if (!success || error) {
+ // TRANSLATORS: Meta-error. Shouldn't be visible.
+ if (!error) error = g_error_new(0, 0, _("Internal error."));
+
+ gchar *error_title = _("Could not start X2Go session…");
+
+ struct _DialogData* ddata = g_new0(struct _DialogData, 1);
+ ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp)));
+ ddata->flags = GTK_DIALOG_MODAL;
+ ddata->type = GTK_MESSAGE_ERROR;
+ ddata->buttons = GTK_BUTTONS_OK;
+ ddata->title = _("Could not start X2Go session.");
+ ddata->message = g_strdup_printf(_("Could not start PyHoca-CLI (%i): '%s'"),
+ error->code,
+ error->message);
+ // We don't need the response.
+ ddata->callbackfunc = NULL;
+ // We don't need a custom dialog either.
+ ddata->dialog_factory_func = NULL;
+ ddata->dialog_factory_data = NULL;
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ */
+ X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1);
+ g_assert(custom_data && "Could not initialise Custom_data.");
+
+ custom_data->gp = gp;
+ custom_data->dialog_data = ddata;
+ custom_data->connect_data = NULL;
+ custom_data->opt1 = NULL;
+
+ IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data);
+
+ g_strlcpy(errmsg, error_title, 512);
+
+ // No need to output here. rmplugin_x2go_start_session will do this.
+
+ g_error_free(error);
+
+ return FALSE;
+ }
+
+ // Prevent a race condition where pyhoca-cli is not
+ // started yet (pidx2go == 0) but a watcher is added.
+
+ struct timespec ts;
+ // 0.001 seconds.
+ ts.tv_nsec = 1 * 1000 * 1000;
+ ts.tv_sec = 0;
+ while (gpdata->pidx2go == 0) {
+ nanosleep(&ts, NULL);
+ REMMINA_PLUGIN_DEBUG("Waiting for PyHoca-CLI to start…");
+ };
+
+ REMMINA_PLUGIN_DEBUG("Watching child 'pyhoca-cli' process now…");
+ g_child_watch_add(gpdata->pidx2go,
+ (GChildWatchFunc) rmplugin_x2go_pyhoca_cli_exited,
+ gp);
+
+ return TRUE;
+}
+
+/**
+ * @returns a GList* with all features which pyhoca-cli had before the feature system.
+ */
+static GList* rmplugin_x2go_old_pyhoca_features()
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ #define AMOUNT_FEATURES 43
+ gchar* features[AMOUNT_FEATURES] = {
+ "ADD_TO_KNOWN_HOSTS", "AUTH_ATTEMPTS", "BROKER_PASSWORD", "BROKER_URL",
+ "CLEAN_SESSIONS", "COMMAND", "DEBUG", "FORCE_PASSWORD", "FORWARD_SSHAGENT",
+ "GEOMETRY", "KBD_LAYOUT", "KBD_TYPE", "LIBDEBUG", "LIBDEBUG_SFTPXFER", "LINK",
+ "LIST_CLIENT_FEATURES", "LIST_DESKTOPS", "LIST_SESSIONS", "NEW", "PACK",
+ "PASSWORD", "PDFVIEW_CMD", "PRINTER", "PRINTING", "PRINT_ACTION", "PRINT_CMD",
+ "QUIET", "REMOTE_SSH_PORT", "RESUME", "SAVE_TO_FOLDER", "SESSION_PROFILE",
+ "SESSION_TYPE", "SHARE_DESKTOP", "SHARE_LOCAL_FOLDERS", "SHARE_MODE", "SOUND",
+ "SSH_PRIVKEY", "SUSPEND", "TERMINATE", "TERMINATE_ON_CTRL_C", "TRY_RESUME",
+ "USERNAME", "XINERAMA"
+ };
+
+ GList *features_list = NULL;
+ for (int i = 0; i < AMOUNT_FEATURES; i++) {
+ features_list = g_list_append(features_list, features[i]);
+ }
+
+ return features_list;
+}
+
+/**
+ * @returns a GList* which includes all pyhoca-cli command line features we can use.
+ */
+static GList* rmplugin_x2go_populate_available_features_list()
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ GList* returning_glist = NULL;
+
+ // We will now start pyhoca-cli with only the '--list-cmdline-features' option
+ // and depending on the exit code and standard output we will determine if some
+ // features are available or not.
+
+ gchar* argv[50];
+ gint argc = 0;
+
+ argv[argc++] = g_strdup("pyhoca-cli");
+ argv[argc++] = g_strdup("--list-cmdline-features");
+ argv[argc++] = NULL;
+
+ GError* error = NULL; // Won't be actually used.
+
+ // Querying pyhoca-cli's command line features.
+ gchar** envp = g_get_environ();
+ gchar* features_string = rmplugin_x2go_spawn_pyhoca_process(argc, argv,
+ &error, envp);
+ g_strfreev(envp);
+
+ if (!features_string || error) {
+ // We added the '--list-cmdline-features' on commit 17d1be1319ba6 of
+ // pyhoca-cli. In order to protect setups which don't have the newest
+ // version of pyhoca-cli available yet we artificially create a list
+ // of an old limited set of features.
+
+ REMMINA_PLUGIN_WARNING("%s",
+ _("Could not get PyHoca-CLI's command-line features. This "
+ "indicates it is either too old, or not installed. "
+ "An old limited set of features will be used for now."));
+
+ return rmplugin_x2go_old_pyhoca_features();
+ } else {
+ gchar **features_list = g_strsplit(features_string, "\n", 0);
+
+ if (features_list == NULL) {
+ gchar *error_msg = _("Could not parse PyHoca-CLI's command-line "
+ "features. Using a limited feature-set for now.");
+ REMMINA_PLUGIN_WARNING("%s", error_msg);
+ return rmplugin_x2go_old_pyhoca_features();
+ }
+
+ REMMINA_PLUGIN_INFO("%s", _("Retrieved the following PyHoca-CLI "
+ "command-line features:"));
+
+ for(int k = 0; features_list[k] != NULL; k++) {
+ // Filter out empty strings
+ if (strlen(features_list[k]) <= 0) continue;
+
+ REMMINA_PLUGIN_INFO("%s",
+ g_strdup_printf(_("Available feature[%i]: '%s'"),
+ k+1, features_list[k]));
+ returning_glist = g_list_append(returning_glist, features_list[k]);
+ }
+ return returning_glist;
+ }
+}
+
+static void rmplugin_x2go_on_plug_added(GtkSocket *socket, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gchar *server;
+ gint port;
+
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+ REMMINA_PLUGIN_DEBUG("Socket %d", gpdata->socket_id);
+ rm_plugin_service->protocol_plugin_signal_connection_opened(gp);
+
+ RemminaFile *remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
+ rm_plugin_service->get_server_port(rm_plugin_service->file_get_string(remminafile, "server"),
+ 22,
+ &server,
+ &port);
+
+ REMMINA_PLUGIN_AUDIT(_("Connected to %s:%d via X2Go"), server, port);
+ g_free(server), server = NULL;
+
+ return;
+}
+
+static gboolean rmplugin_x2go_on_plug_removed(GtkSocket *socket, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+ rmplugin_x2go_close_connection(gp);
+ return G_SOURCE_CONTINUE;
+}
+
+static void rmplugin_x2go_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Function entry.", PLUGIN_NAME);
+ RemminaPluginX2GoData *gpdata;
+
+ gpdata = g_new0(RemminaPluginX2GoData, 1);
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
+
+ if (!rm_plugin_service->gtksocket_available()) {
+ /* report this in open_connection, not reportable here... */
+ return;
+ }
+
+ GList* available_features = rmplugin_x2go_populate_available_features_list();
+
+ // available_features can't be NULL cause if it fails, it gets populated with an
+ // old standard feature set.
+ gpdata->available_features = available_features;
+
+ gpdata->socket_id = 0;
+ gpdata->thread = 0;
+
+ gpdata->display = NULL;
+ gpdata->window_id = 0;
+ gpdata->pidx2go = 0;
+ gpdata->orig_handler = NULL;
+
+ gpdata->socket = gtk_socket_new();
+ rm_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->socket);
+ gtk_widget_show(gpdata->socket);
+
+ g_signal_connect(G_OBJECT(gpdata->socket), "plug-added",
+ G_CALLBACK(rmplugin_x2go_on_plug_added), gp);
+ g_signal_connect(G_OBJECT(gpdata->socket), "plug-removed",
+ G_CALLBACK(rmplugin_x2go_on_plug_removed), gp);
+ gtk_container_add(GTK_CONTAINER(gp), gpdata->socket);
+}
+
+static gboolean rmplugin_x2go_try_window_id(Window window_id)
+{
+ TRACE_CALL(__func__);
+ gint i;
+ gboolean already_seen = FALSE;
+
+ REMMINA_PLUGIN_DEBUG("Check if the window of X2Go Agent with ID [0x%lx] is already known or if "
+ "it needs registration", window_id);
+
+ pthread_mutex_lock(&remmina_x2go_init_mutex);
+ for (i = 0; i < remmina_x2go_window_id_array->len; i++) {
+ if (g_array_index(remmina_x2go_window_id_array, Window, i) == window_id) {
+ already_seen = TRUE;
+ REMMINA_PLUGIN_DEBUG("Window of X2Go Agent with ID [0x%lx] "
+ "already seen.", window_id);
+ break;
+ }
+ }
+ if (!already_seen) {
+ g_array_append_val(remmina_x2go_window_id_array, window_id);
+ REMMINA_PLUGIN_DEBUG("Registered new window for X2Go Agent with "
+ "ID [0x%lx].", window_id);
+ }
+ pthread_mutex_unlock(&remmina_x2go_init_mutex);
+
+ return (!already_seen);
+}
+
+static int rmplugin_x2go_dummy_handler(Display *dsp, XErrorEvent *err)
+{
+ TRACE_CALL(__func__);
+ return 0;
+}
+
+static gboolean rmplugin_x2go_start_create_notify(RemminaProtocolWidget *gp, gchar *errmsg)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+
+ gpdata->display = XOpenDisplay(gdk_display_get_name(gdk_display_get_default()));
+ if (gpdata->display == NULL) {
+ g_strlcpy(errmsg, _("Could not open X11 DISPLAY."), 512);
+ return FALSE;
+ }
+
+ gpdata->orig_handler = XSetErrorHandler(rmplugin_x2go_dummy_handler);
+
+ XSelectInput(gpdata->display,
+ XDefaultRootWindow(gpdata->display),
+ SubstructureNotifyMask);
+
+ REMMINA_PLUGIN_DEBUG("X11 event-watcher created.");
+
+ return TRUE;
+}
+
+static gboolean rmplugin_x2go_monitor_create_notify(RemminaProtocolWidget *gp,
+ const gchar *cmd,
+ gchar *errmsg)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata;
+
+ gboolean agent_window_found = FALSE;
+ Atom atom;
+ XEvent xev;
+ Window w;
+ Atom type;
+ int format;
+ unsigned long nitems, rest;
+ unsigned char *data = NULL;
+
+ guint16 non_createnotify_count = 0;
+
+ struct timespec ts;
+ // wait_amount * ts.tv_nsec = 20s
+ // 100 * 0.2s = 20s
+ int wait_amount = 100;
+
+ CANCEL_DEFER
+
+ REMMINA_PLUGIN_DEBUG("%s", _("Waiting for window of X2Go Agent to appear…"));
+
+ gpdata = GET_PLUGIN_DATA(gp);
+ atom = XInternAtom(gpdata->display, "WM_COMMAND", True);
+ if (atom == None) {
+ CANCEL_ASYNC
+ return FALSE;
+ }
+
+ ts.tv_sec = 0;
+ // 0.2s = 200000000ns
+ ts.tv_nsec = 200000000;
+
+ while (wait_amount > 0) {
+ pthread_testcancel();
+ if (!(gpdata->pidx2go > 0)) {
+ nanosleep(&ts, NULL);
+ REMMINA_PLUGIN_DEBUG("Waiting for X2Go session to start…");
+ continue;
+ }
+
+ while (!XPending(gpdata->display)) {
+ nanosleep(&ts, NULL);
+ wait_amount--;
+ // Don't spam the console. Print every second though.
+ if (wait_amount % 5 == 0) {
+ REMMINA_PLUGIN_INFO("%s", _("Waiting for PyHoca-CLI to "
+ "show the session's window…"));
+ }
+ continue;
+ }
+
+ XNextEvent(gpdata->display, &xev);
+ // Just ignore non CreatNotify events.
+ if (xev.type != CreateNotify) {
+ non_createnotify_count++;
+ if (non_createnotify_count % 5 == 0) {
+ REMMINA_PLUGIN_DEBUG("Saw '%i' X11 events, which weren't "
+ "CreateNotify.", non_createnotify_count);
+ }
+ continue;
+ }
+
+ w = xev.xcreatewindow.window;
+ if (XGetWindowProperty(gpdata->display, w, atom, 0, 255, False,
+ AnyPropertyType, &type, &format, &nitems, &rest,
+ &data) != Success) {
+ REMMINA_PLUGIN_DEBUG("Could not get WM_COMMAND property from X11 "
+ "window ID [0x%lx].", w);
+ continue;
+ }
+
+ if (data) {
+ REMMINA_PLUGIN_DEBUG("Saw '%i' X11 events, which weren't "
+ "CreateNotify.", non_createnotify_count);
+ REMMINA_PLUGIN_DEBUG("Found X11 window with WM_COMMAND set "
+ "to '%s', the window ID is [0x%lx].",
+ (char*)data, w);
+ }
+ if (data && g_strrstr((gchar*)data, cmd) &&
+ rmplugin_x2go_try_window_id(w)) {
+ gpdata->window_id = w;
+ agent_window_found = TRUE;
+ XFree(data);
+ break;
+ }
+ if (data)
+ XFree(data);
+ }
+
+ XSetErrorHandler(gpdata->orig_handler);
+ XCloseDisplay(gpdata->display);
+ gpdata->display = NULL;
+
+ CANCEL_ASYNC
+
+ if (!agent_window_found) {
+ g_strlcpy(errmsg, _("No X2Go session window appeared. "
+ "Something went wrong…"), 512);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean rmplugin_x2go_start_session(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);;
+ RemminaFile *remminafile;
+ const gchar errmsg[512] = {0};
+ gboolean ret = TRUE;
+
+ gchar *servstr, *host, *username, *password, *command, *kbdlayout, *kbdtype,
+ *audio, *clipboard, *res, *ssh_privatekey;
+ gint sshport, dpi;
+ GdkDisplay *default_dsp;
+ gint width, height;
+
+ // We save the X Display name (:0) as we will need to synchronize the clipboards
+ default_dsp = gdk_display_get_default();
+ const gchar *default_dsp_name = gdk_display_get_name(default_dsp);
+ REMMINA_PLUGIN_DEBUG("Default display is '%s'.", default_dsp_name);
+
+ remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
+
+ servstr = GET_PLUGIN_STRING("server");
+ if (servstr) {
+ rm_plugin_service->get_server_port(servstr, 22, &host, &sshport);
+ } else {
+ return FALSE;
+ }
+
+ if (!sshport) sshport=22;
+
+ username = GET_PLUGIN_STRING("username");
+ password = GET_PLUGIN_PASSWORD("password");
+
+ command = GET_PLUGIN_STRING("command");
+ if (!command) command = "TERMINAL";
+
+ kbdlayout = GET_PLUGIN_STRING("kbdlayout");
+ kbdtype = GET_PLUGIN_STRING("kbdtype");
+
+ audio = GET_PLUGIN_STRING("audio");
+
+ clipboard = GET_PLUGIN_STRING("clipboard");
+
+ dpi = GET_PLUGIN_INT("dpi", 80);
+
+ ssh_privatekey = GET_PLUGIN_STRING("ssh_privatekey");
+
+ // If empty set to NULL
+ if(ssh_privatekey && g_str_equal(ssh_privatekey, "")) {
+ ssh_privatekey = NULL;
+ }
+
+ width = rm_plugin_service->get_profile_remote_width(gp);
+ height = rm_plugin_service->get_profile_remote_height(gp);
+ /* multiple of 4 */
+ width = (width + 3) & ~0x3;
+ height = (height + 3) & ~0x3;
+ if ((width > 0) && (height > 0)) {
+ res = g_strdup_printf ("%dx%d", width, height);
+ } else {
+ res = "800x600";
+ }
+ REMMINA_PLUGIN_DEBUG("Resolution set by user: '%s'.", res);
+
+ REMMINA_PLUGIN_DEBUG("Attached window to socket '%d'.", gpdata->socket_id);
+
+ /* register for notifications of window creation events */
+ if (ret) ret = rmplugin_x2go_start_create_notify(gp, (gchar*)&errmsg);
+
+ /* trigger the session start, session window should appear soon after this */
+ if (ret) ret = rmplugin_x2go_exec_x2go(host, sshport, username, password, command,
+ kbdlayout, kbdtype, audio, clipboard, dpi,
+ res, ssh_privatekey, gp,
+ (gchar*)&errmsg);
+
+ /* get the window ID of the remote x2goagent */
+ if (ret) ret = rmplugin_x2go_monitor_create_notify(gp, "x2goagent",
+ (gchar*)&errmsg);
+
+ if (!ret) {
+ REMMINA_PLUGIN_CRITICAL("%s", errmsg);
+ rm_plugin_service->protocol_plugin_set_error(gp, "%s", &errmsg);
+ return FALSE;
+ }
+
+ /* embed it */
+ onMainThread_gtk_socket_add_id(GTK_SOCKET(gpdata->socket), gpdata->window_id);
+
+ return TRUE;
+}
+
+static gboolean rmplugin_x2go_main(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+ gboolean ret = FALSE;
+
+ ret = rmplugin_x2go_start_session(gp);
+
+ gpdata->thread = 0;
+ return ret;
+}
+
+static gpointer rmplugin_x2go_main_thread(RemminaProtocolWidget* gp)
+{
+ TRACE_CALL(__func__);
+ if (!gp) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("RemminaProtocolWidget* gp is 'NULL'!")
+ ));
+ return NULL;
+ }
+
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+
+ CANCEL_ASYNC
+ if (!rmplugin_x2go_main(gp)) {
+ IDLE_ADD((GSourceFunc) rmplugin_x2go_cleanup, gp);
+ }
+
+ return NULL;
+}
+
+static gboolean rmplugin_x2go_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (!rm_plugin_service->gtksocket_available()) {
+ rm_plugin_service->protocol_plugin_set_error(gp, _("The %s protocol is "
+ "unavailable because GtkSocket only works under X.org"),
+ PLUGIN_NAME);
+ return FALSE;
+ }
+
+ gpdata->socket_id = gtk_socket_get_id(GTK_SOCKET(gpdata->socket));
+ // casting to void* is allowed since return type 'gpointer' is actually void*.
+ if (pthread_create(&gpdata->thread, NULL, (void*) rmplugin_x2go_main_thread, gp)) {
+ rm_plugin_service->protocol_plugin_set_error(gp, _("Could not initialize "
+ "pthread. Falling back to non-threaded mode…"));
+ gpdata->thread = 0;
+ return FALSE;
+ } else {
+ return TRUE;
+ }
+}
+
+static gboolean rmplugin_x2go_query_feature(RemminaProtocolWidget* gp,
+ const RemminaProtocolFeature* feature)
+{
+ TRACE_CALL(__func__);
+ return TRUE;
+}
+
+static const RemminaProtocolFeature rmplugin_x2go_features[] = {
+ {REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET, RMPLUGIN_X2GO_FEATURE_GTKSOCKET, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL}
+};
+
+
+/**
+ * @brief This function builds a string like: "'value1', 'value2' and 'value3'" \n
+ * To be used in a loop. \n
+ * See rmplugin_x2go_string_setting_validator() for an example.
+ *
+ * @param max_elements Number of maximum elements.
+ * @param element_to_add Next element to add to the string
+ * @param current_element Which element is element_to_add?
+ * @param string The string to which `element_to_add` will be added.
+ */
+static gchar* rmplugin_x2go_enumeration_prettifier(const guint max_elements,
+ const guint current_element,
+ gchar* element_to_add,
+ gchar* string)
+{
+ if (max_elements > 2) {
+ if (current_element == max_elements - 1) {
+ // TRANSLATORS: Presumably you just want to translate 'and' into
+ // your language.
+ // (Except your listing-grammar differs from english.)
+ // 'value1', 'value2', 'valueN-1' and 'valueN'
+ return g_strdup_printf(_("%sand '%s'"), string, element_to_add);
+ } else if (current_element == max_elements - 2) {
+ // TRANSLATORS: Presumably you just want to leave it english.
+ // (Except your listing-grammar differs from english.)
+ // 'value1', 'value2', 'valueN-1' and 'valueN'
+ return g_strdup_printf(_("%s'%s' "), string, element_to_add);
+ } else {
+ // TRANSLATORS: Presumably you just want to leave it english.
+ // (Except your listing-grammar differs from english.)
+ // 'value1', 'value2', 'valueN-1' and 'valueN'
+ return g_strdup_printf(_("%s'%s', "), string, element_to_add);
+ }
+ } else if (max_elements == 2) {
+ if (current_element == max_elements - 1) {
+ // TRANSLATORS: Presumably you just want to translate 'and' into
+ // your language.
+ // (Except your listing-grammar differs from english.)
+ // 'value1' and 'value2'
+ return g_strdup_printf(_("%sand '%s'"), string, element_to_add);
+ } else {
+ // TRANSLATORS: Presumably you just want to leave it english.
+ // (Except your listing-grammar differs from english.)
+ // 'value1' and 'value2'
+ return g_strdup_printf(_("%s'%s' "), string, element_to_add);
+ }
+ } else {
+ return g_strdup(element_to_add);
+ }
+}
+
+/**
+ * @brief Validator-functions are getting executed when the user wants to save profile
+ * settings. It uses the given data (See RemminaProtocolSetting array) to determine
+ * which strings are allowed and returns a end-user friendly error message.
+ *
+ * @param key Key is the setting's name.
+ * @param value Value to validate.
+ * @param data Data needed for validation process. See RemminaProtocolSetting array.
+ *
+ * @returns End-user friendly and translated error message, explaining why the given
+ * value is invalid. If the given value is error-free then NULL gets returned.
+ *
+ */
+static GError* rmplugin_x2go_string_setting_validator(gchar* key, gchar* value,
+ gchar* data)
+{
+ GError *error = NULL;
+
+ if (!data) {
+ gchar *error_msg = _("Invalid validation data in ProtocolSettings array!");
+ REMMINA_PLUGIN_CRITICAL("%s", error_msg);
+ g_set_error(&error, 1, 1, "%s", error_msg);
+ return error;
+ }
+
+ gchar **elements_list = g_strsplit(data, ",", 0);
+
+ guint elements_amount = 0;
+ elements_amount = g_strv_length(elements_list);
+
+ if (elements_list == NULL ||
+ elements_list[0] == NULL ||
+ strlen(elements_list[0]) <= 0)
+ {
+ gchar *error_msg = _("Validation data in ProtocolSettings array is invalid!");
+ REMMINA_PLUGIN_CRITICAL("%s", error_msg);
+ g_set_error(&error, 1, 1, "%s", error_msg);
+ return error;
+ }
+
+ gchar *data_str = "";
+
+ if (!key || !value) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Parameters 'key' or 'value' are 'NULL'!"));
+ g_set_error(&error, 1, 1, "%s", _("Internal error."));
+ return error;
+ }
+
+ for (guint i = 0; elements_list[i] != NULL; i++) {
+ // Don't wanna crash if elements_list[i] is NULL.
+ gchar* element = elements_list[i] ? elements_list[i] : "";
+ if (g_strcmp0(value, element) == 0) {
+ // We found value in elements_list. Value passed validation.
+ return NULL;
+ }
+
+ data_str = rmplugin_x2go_enumeration_prettifier(elements_amount, i,
+ element, data_str);
+ }
+
+ if (elements_amount > 1) {
+ g_set_error(&error, 1, 1, _("Allowed values are %s."), data_str);
+ } else {
+ g_set_error(&error, 1, 1, _("The only allowed value is '%s'."), data_str);
+ }
+
+ g_free(data_str);
+ g_strfreev(elements_list);
+
+ return error;
+}
+
+/**
+ * @brief Validator-functions are getting executed when the user wants to save profile
+ * settings. It uses the given data (See RemminaProtocolSetting array) to determine
+ * if the given value is a valid integer is in range and returns a end-user
+ * friendly error message.
+ *
+ * @param key Key is the setting's name.
+ * @param value Value to validate.
+ * @param data Data needed for validation process. See RemminaProtocolSetting array.
+ *
+ * @returns End-user friendly and translated error message, explaining why the given
+ * value is invalid. If the given value is error-free then NULL gets returned.
+ *
+ */
+static GError* rmplugin_x2go_int_setting_validator(gchar* key, gpointer value,
+ gchar* data)
+{
+ GError *error = NULL;
+
+ gchar **integer_list = g_strsplit(data, ";", 0);
+
+ if (integer_list == NULL ||
+ integer_list[0] == NULL ||
+ integer_list[1] == NULL ||
+ strlen(integer_list[0]) <= 0 ||
+ strlen(integer_list[1]) <= 0)
+ {
+ gchar *error_msg = _("Validation data in ProtocolSettings array is invalid!");
+ REMMINA_PLUGIN_CRITICAL("%s", error_msg);
+ g_set_error(&error, 1, 1, "%s", error_msg);
+ return error;
+ }
+
+ gint minimum;
+ str2int_errno err = str2int(&minimum, integer_list[0], 10);
+ if (err == STR2INT_INCONVERTIBLE) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The lower limit is not a valid integer!")
+ ));
+ } else if (err == STR2INT_OVERFLOW) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The lower limit is too high!")
+ ));
+ } else if (err == STR2INT_UNDERFLOW) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The lower limit is too low!")
+ ));
+ } else if (err == STR2INT_INVALID_DATA) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Something unknown went wrong.")
+ ));
+ }
+
+ if (error) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Please check the RemminaProtocolSetting "
+ "array for possible errors."));
+ return error;
+ }
+
+ gint maximum;
+ err = str2int(&maximum, integer_list[1], 10);
+ if (err == STR2INT_INCONVERTIBLE) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The upper limit is not a valid integer!")
+ ));
+ } else if (err == STR2INT_OVERFLOW) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The upper limit is too high!")
+ ));
+ } else if (err == STR2INT_UNDERFLOW) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The upper limit is too low!")
+ ));
+ } else if (err == STR2INT_INVALID_DATA) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Something unknown went wrong.")
+ ));
+ }
+
+ if (error) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Please check the RemminaProtocolSetting "
+ "array for possible errors."));
+ return error;
+ }
+
+ gint int_value;
+ err = str2int(&int_value, value, 10);
+ if (err == STR2INT_INCONVERTIBLE) {
+ // non-numerical characters are can't be entered but, the user can
+ // input an empty string.
+ g_set_error(&error, 1, 1, "%s", _("The input is not a valid integer!"));
+ } else if (err == STR2INT_OVERFLOW || err == STR2INT_UNDERFLOW) {
+ g_set_error(&error, 1, 1, _("Input must be a number between %i and %i."),
+ minimum, maximum);
+ } else if (err == STR2INT_INVALID_DATA) {
+ g_set_error(&error, 1, 1, "%s", _("Something unknown went wrong."));
+ }
+
+ if (error) {
+ return error;
+ }
+
+ /*REMMINA_PLUGIN_DEBUG("Key: \t%s", (gchar*) key);
+ REMMINA_PLUGIN_DEBUG("Value:\t%s", (gchar*) value);
+ REMMINA_PLUGIN_DEBUG("Data: \t%s", data);
+ REMMINA_PLUGIN_DEBUG("Min: %i, Max: %i", minimum, maximum);
+ REMMINA_PLUGIN_DEBUG("Value converted:\t%i", int_value);*/
+
+ if (err == STR2INT_SUCCESS && (minimum > int_value || int_value > maximum)) {
+ g_set_error(&error, 1, 1, _("Input must be a number between %i and %i."),
+ minimum, maximum);
+ }
+
+ // Should be NULL.
+ return error;
+}
+
+/* Array of RemminaProtocolSetting for basic settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting tooltip
+ * g) Validation data pointer, will be passed to the validation callback method.
+ * h) Validation callback method (Can be NULL. Every entry will be valid then.)
+ * use following prototype:
+ * gboolean mysetting_validator_method(gpointer key, gpointer value,
+ * gpointer validator_data);
+ * gpointer key is a gchar* containing the setting's name,
+ * gpointer value contains the value which should be validated,
+ * gpointer validator_data contains your passed data.
+ */
+static const RemminaProtocolSetting rmplugin_x2go_basic_settings[] = {
+ {REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("Password"), FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "command", N_("Startup program"), FALSE,
+ /* SELECT & COMBO Values */ "MATE,KDE,XFCE,LXDE,TERMINAL",
+ /* Tooltip */ N_("Which command should be executed after creating the X2Go session?"), NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION, "resolution", NULL, FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "kbdlayout", N_("Keyboard Layout (auto)"), FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "kbdtype", N_("Keyboard type (auto)"), FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "audio", N_("Audio support"), FALSE,
+ /* SELECT & COMBO Values */ "pulse,esd,none",
+ /* Tooltip */ N_("The sound system of the X2Go server (default: 'pulse')."),
+ /* Validation data */ "pulse,esd,none",
+ /* Validation method */ G_CALLBACK(rmplugin_x2go_string_setting_validator)},
+ {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "clipboard", N_("Clipboard direction"), FALSE,
+ /* SELECT & COMBO Values */ "none,server,client,both",
+ /* Tooltip */ N_("Which direction should clipboard content be copied? "
+ "(default: 'both')."),
+ /* Validation data */ "none,server,client,both",
+ /* Validation method */ G_CALLBACK(rmplugin_x2go_string_setting_validator)},
+ {REMMINA_PROTOCOL_SETTING_TYPE_INT, "dpi", N_("DPI resolution"), FALSE, NULL,
+ /* Tooltip */ N_("Launch session with a specific resolution (in dots per inch). "
+ "Must be between 20 and 400."),
+ /* Validation data */ "20;400", // "<min>;<max>;"
+ /* Validation method */ G_CALLBACK(rmplugin_x2go_int_setting_validator)},
+ {REMMINA_PROTOCOL_SETTING_TYPE_FILE, "ssh_privatekey", N_("SSH identity file"), FALSE, NULL, N_("Your private key"), NULL, NULL },
+ {REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL}};
+
+/* Protocol plugin definition and features */
+static RemminaProtocolPlugin rmplugin_x2go = {
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ PLUGIN_NAME, // Name
+ PLUGIN_DESCRIPTION, // Description
+ GETTEXT_PACKAGE, // Translation domain
+ PLUGIN_VERSION, // Version number
+ PLUGIN_APPICON, // Icon for normal connection
+ PLUGIN_SSH_APPICON, // Icon for SSH connection
+ rmplugin_x2go_basic_settings, // Array for basic settings
+ NULL, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type
+ rmplugin_x2go_features, // Array for available features
+ rmplugin_x2go_init, // Plugin initialization method
+ rmplugin_x2go_open_connection, // Plugin open connection method
+ rmplugin_x2go_close_connection, // Plugin connection-method closure
+ rmplugin_x2go_query_feature, // Query for available features
+ NULL, // Call a feature
+ NULL, // Send a keystroke
+ NULL, // Screenshot
+};
+
+G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL("remmina_plugin_entry");
+ rm_plugin_service = service;
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+ if (!service->register_plugin((RemminaPlugin *) &rmplugin_x2go)) {
+ return FALSE;
+ }
+
+ pthread_mutex_init(&remmina_x2go_init_mutex, NULL);
+ remmina_x2go_window_id_array = g_array_new(FALSE, TRUE, sizeof(Window));
+
+ REMMINA_PLUGIN_MESSAGE("%s", _("X2Go plugin loaded."));
+
+ return TRUE;
+}
diff --git a/plugins/x2go/x2go_plugin.h b/plugins/x2go/x2go_plugin.h
new file mode 100644
index 0000000..5fc53a6
--- /dev/null
+++ b/plugins/x2go/x2go_plugin.h
@@ -0,0 +1,46 @@
+/*
+ * Project: Remmina Plugin X2Go
+ * Description: Remmina protocol plugin to connect via X2Go using PyHoca
+ * Based on Fabio Castelli Team Viewer Plugin
+ * Copyright: 2013-2014 Fabio Castelli (Muflone)
+ * Author: Antenore Gatta <antenore@simbiosi.org>
+ * Copyright: 2015 Antenore Gatta
+ * License: GPL-2+
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#define PLUGIN_NAME "X2GO"
+#define PLUGIN_DESCRIPTION N_("X2Go - Launch an X2Go session")
+#define PLUGIN_VERSION "2.0.0"
+#define PLUGIN_APPICON "org.remmina.Remmina-x2go-symbolic"
+#define PLUGIN_SSH_APPICON "org.remmina.Remmina-x2go-ssh-symbolic"