diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 17:06:32 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 17:06:32 +0000 |
commit | 2dad5357405ad33cfa792f04b3ab62a5d188841e (patch) | |
tree | b8f8893942060fe3cfb04ac374cda96fdfc8f453 /plugins/x2go | |
parent | Initial commit. (diff) | |
download | remmina-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.txt | 66 | ||||
-rw-r--r-- | plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg | 297 | ||||
-rw-r--r-- | plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg | 297 | ||||
-rw-r--r-- | plugins/x2go/x2go_plugin.c | 3463 | ||||
-rw-r--r-- | plugins/x2go/x2go_plugin.h | 46 |
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" |