#!/usr/bin/perl

# Copyright (C) 2007-2019 by X2Go Project, https://wiki.x2go.org
#       Oleksandr Shneyder <o.shneyder@phoca-gmbh.de>

# X2Go 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.
#
# X2Go 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 St, Fifth Floor, Boston, MA 02110-1301, USA.

use strict;
use File::Path::Expand;
# comment out this "use" and the following two lines, and instead
# uncomment the block below if you need to do early boot stage
# debugging of the automounter, when rsyslogd isn't running yet
use Sys::Syslog qw( :standard :macros );
openlog($0, 'cons,pid', 'user');
setlogmask(LOG_UPTO(LOG_NOTICE));
#open (B, ">> /var/log/usbdebug");
#sub syslog {
#       print B $_[0] . ': ' . $_[1] . "\n";
#}

my $user;
if ( -f "/etc/x2go/x2gothinclient-minidesktop_start" ) {
	# this is a minidesktop environment, which uses
	# username "x2gothinclient" regardless of whether
	# it runs on X2Go-TCE-Live or X2Go-TCE-NFS
	$user = 'x2gothinclient';
}
elsif ( -d "/lib/live/config" ) {
	# this is X2Go-TCE-Live, but not with a minidesktop
	# (if it were, the first condition would have matched),
	# so we use Debian-Live's standard username "user"
	$user = 'user';
}
else {
	# this is X2Go-TCE-NFS or something completely different,
	# so we'll play it safe and pick the username "x2gothinclient"
	# just like previous versions of this script did
	$user = 'x2gothinclient';
}

# We need this as chown requires numeric uid/gid
my ($login,$pass,$uid,$gid) = getpwnam($user);

# Some last-ditch efforts to fulfill the prerequisites for File Sharing:
# - This is stuff that should already have happened earlier in the boot process.
# - Also, if a directory already exists, we silently assume that ownership and
#   permissions are correct. This is so that users that deliberately set
#   different ownership/permission values don't have their settings silently
#   overwritten.

unless ( -d expand_filename("~$user/mounts")) {
	mkdir expand_filename("~$user/mounts");
	chmod 0700, expand_filename("~$user/mounts");
	chown $uid, $gid, expand_filename("~$user/mounts");
}

unless ( -d expand_filename("~$user/export")) {
	mkdir expand_filename("~$user/export");
	chmod 0700, expand_filename("~$user/export");
	chown $uid, $gid, expand_filename("~$user/export");
}

unless ( -d expand_filename("~$user/logins")) {
	mkdir expand_filename("~$user/logins");
	chmod 0700, expand_filename("~$user/logins");
	chown $uid, $gid, expand_filename("~$user/logins");
}

sub check_x2gothinclientmode {
	my $ret = 0;

	# Check for x2gothinclientd first...
	my $x = `ps u -C x2gothinclientd`;
	if ($x =~ m/\W*x2gothinclientd\W*/)
	{
		$ret = 1;
	}

	# And for x2goclient --thinclient if nothing was found.
	if (!$ret) {
		$x = `ps u -C x2goclient`;
		if ($x =~ m/\W*--thinclient\W*/)
		{
			$ret = 1;
		}
	}

	return $ret;
}

#    TCE-NFS                     TCE-Live                                              MMD-Live
if ((check_x2gothinclientmode()) || (-x "/lib/live/config/2900-x2go-thinclientconfig") || (-x "/etc/x2go/x2gothinclient-minidesktop_start"))
{
	syslog('notice', "some kind of thinclient mode detected");

	open (F, ">> /var/log/usb");

	my $dev = $ENV{'DEVNAME'};
	my $model = $ENV{'ID_MODEL'};
	my $vendor = $ENV{'ID_VENDOR'};
	my $action = $ENV{'ACTION'};
	my @ldev = split("/","$dev");
	my $ldev = @ldev[@ldev-1];
	# mntdir is not the directory where the mountpoint will be rooted,
	# but where tracking of mount states takes place
	my $mntdir;
	if ( -d expand_filename("~$user/mounts")) {
		$mntdir = expand_filename("~$user/mounts");
	} elsif ( -d "/var/run" ) {
		$mntdir = "/var/run";
	} elsif ( -d "/run" ) {
		$mntdir = "/run";
	} else {
		die "No directory found that we could use as \$mntdir..."
	}

	my $name = "${vendor}_${model}";
	$name =~ s/ //g;
	$name =~ s/\\//g;
	$name =~ s/\///g;
	print F "action: $action,  device: $dev, model: $model ($ldev), total: $name\n";
	mkdir("/media");
	mkdir("/media/$name");
	print F "$name\n";

	if (`lsblk -ln -oRM $dev` =~ /0$/) {
		syslog('notice', "device is non-removable device, skipping");
		exit 0;
	}

	if ( $action eq "add" )
	{

		###
		### ACTION: mount device after it has been added to USB subsystem
		###

		syslog('notice', "device add action called");

		# prepare mount points
		mkdir("/media");
		mkdir("/media/$name");
		mkdir("/media/$name/$ldev");

		# mount the USB device
		# sync is supported by all file systems
		#
		# uid is supported by vfat (via fat),ntfs,hfs,hpfs
		#
		# uni_xlate is supported by vfat and supposed to change "unhandled"
		# Unicode characters into escape sequences; otherwise, such
		# characters are replaced by a verbatim question mark, triggering
		# information loss. Very old versions of ntfs used to "support" this
		# option, but historic documents mention that it was broken and
		# removed some time in the Linux 2.4 days.
		# Character set handling under FAT is complicated to begin with.
		# Apparently, 8.3 short file names (which really are the low-level
		# and most compatible representation of file names) are always
		# encoded with a specific 8-bit codepage - importantly, multi-byte
		# encodings are not supported. This specific codepage can either be
		# the default one set in the Linux kernel or any other supported one
		# selected via the codepage=... mount option.
		# Long file names are stored in "Unicode format" according to the
		# kernel documentation, which seems to really be raw UCS-2, i.e.,
		# the raw 2-byte-value of a code point. Consequently, only characters
		# in the Basic Multilingual Plane (BMP) can be used in long file
		# names and each file is restricted to a character length of at most
		# 255, with the additional restriction of at most 256 characters per
		# path. This "encoding" could be problematic since code points
		# outside of the BMP are being used more and more nowadays, but most
		# users should still be fine.
		# Typically, systems are not using the UCS-2, but a different
		# encoding like UTF-8 or UTF-16. These encodings can handle the full
		# Unicode block, also code points after the BMP. Hence, the kernel
		# has to perform some kind of conversion from UCS-2 to the desired
		# encoding, which again can either be the default one set in the
		# kernel or any other supported one set through the iocharset=...
		# mount option.
		# At that point, things get tricky. (V)FAT is typically a case-
		# insensitive file system, which means that lower- and uppercase
		# versions of a character should be handled as the same character.
		# The Linux kernel developers do not want to store a lower- to
		# uppercase mapping for the full Unicode set of characters, though,
		# for space and performance reasons. Hence, file systems mounted via
		# iocharset=utf8 will be completely case-sensitive. To work around
		# that problem, another option called 'utf8' exists, which can be
		# used in conjunction with an 8-bit encoding iocharset option.
		# Apparently, with such a setup the kernel returns UTF-8-encoded
		# data, but internal name mangling operations are carried out in the
		# given iocharset encoding. Such a scenario is most useful when
		# setting the iocharset parameter to an encoding for which lower-
		# and uppercase pairs are defined properly.
		# Enabling uni_xlate disables utf8 and is only useful when using
		# an iocharset encoding not covering the UCS-2 range. We do not want
		# to use this.
		#
		# we must not trigger on iso9660 and udf, or else hybrid USB media
		# would only cause a mount of the iso9660 raw device,
		# blocking the mount of individual partitions
		# real optical media ->x2gocdmanager/x2gothinclient-cdmanager package

		if (system("mount -tntfs $dev /media/$name/$ldev -o uid=$user,sync") == 0)
		{
			syslog('notice', "USB device $name ($ldev) successfully mounted (ntfs detected)");
			# if mounted, inform x2goclient about it...
			system("touch $mntdir/$ldev.mounted");
			open (D, ">", expand_filename("~$user/export/$name.$ldev"));
			print D "export=/media/$name/$ldev\n";
			close (D);
		}
		elsif (system("mount -tvfat $dev /media/$name/$ldev -o uid=$user,sync,utf8") == 0)
		{
			syslog('notice', "USB device $name ($ldev) successfully mounted (vfat detected)");
			# if mounted, inform x2goclient about it...
			system("touch $mntdir/$ldev.mounted");
			open (D, ">", expand_filename("~$user/export/$name.$ldev"));
			print D "export=/media/$name/$ldev\n";
			close (D);
		}
		elsif (system("mount -t hfs $dev /media/$name/$ldev -o uid=$user,sync") == 0)
		{
			syslog('notice', "USB device $name ($ldev) successfully mounted (hfs detected)");
			# if mounted, inform x2goclient about it...
			system("touch $mntdir/$ldev.mounted");
			open (D, ">", expand_filename("~$user/export/$name.$ldev"));
			print D "export=/media/$name/$ldev\n";
			close (D);
		}
		elsif (system("mount -t hpfs $dev /media/$name/$ldev -o uid=$user,sync") == 0)
		{
			syslog('notice', "USB device $name ($ldev) successfully mounted (hpfs detected)");
			# if mounted, inform x2goclient about it...
			system("touch $mntdir/$ldev.mounted");
			open (D, ">", expand_filename("~$user/export/$name.$ldev"));
			print D "export=/media/$name/$ldev\n";
			close (D);
		}
		elsif (system("fuseext2 $dev /media/$name/$ldev -o ro,allow_other") == 0)
		{
			syslog('notice', "USB device $name ($ldev) successfully mounted readonly (ext*fs detected)");
			# if mounted, inform x2goclient about it...
			system("touch $mntdir/$ldev.mounted");
			open (D, ">", expand_filename("~$user/export/$name.$ldev"));
			print D "export=/media/$name/$ldev\n";
			close (D);
		}
		else
		{
			# the mount failed, let's assume that the device is encrypted...
			my $enc=`ls -1 $mntdir | grep .encrypted`;
			if ($enc eq "")
			{
				# use cryptsetup to decrypt the device...
				system("/sbin/cryptsetup --key-file /etc/keys/keystick.key luksOpen $dev keystick");

				# mount the ,,decrypted'' USB device via devmapper...
				if (system("mount /dev/mapper/keystick /media/$name/$ldev") == 0)
				{
					# inform x2goclient about this...
					system("touch $mntdir/$ldev.encrypted");
					system("chown -R $user /media/$name/$ldev/dsa.key");
					open (D, ">", expand_filename("~$user/logins/$name.$ldev"));
					print D "login=/media/$name/$ldev\n";
					close (D);
					print F "encrypted mount successful ($ldev)\n";
				}
				else
				{
					# on mount failures release the decrypted device again
					system("/sbin/cryptsetup luksClose keystick");
					print F "mount failed ($ldev)\n";
				}
			}
			else
			{
				print F "cryptodisk already present\n";
			}
		}
		if (-e "/media/$name/$ldev") {
			print F "detected mountpoint '/media/$name/$ldev'\n";
			print F "running '/sbin/blkid -o value -s LABEL $dev'\n";
			my $label = `/sbin/blkid -o value -s LABEL $dev`;
			chomp($label);
			if ($label) {
				print F "symlinking '/media/$name/$ldev' and '/media/$name/$label'\n";
				unlink "/media/$name/$label" if ( -l "/media/$name/$label" );
				symlink("/media/$name/$ldev", "/media/$name/$label");
				open (D, ">>", expand_filename("~$user/export/$name.$ldev"));
				print D "export=/media/$name/$label\n";
				close (D);
			}
		}

	}
	elsif ($action eq "remove")
	{

		###
		### ACTION: unmount device after it has been removed from the USB subsystem
		###

		syslog('notice', "device remove action called");

		# we rely on our own mount logistics here...
		if ( -e "$mntdir/$ldev.mounted" )
		{
			# inform x2goclient that the device has been removed
			system ("umount -ff /media/$name/$ldev");
			unlink ("$mntdir/$ldev.mounted");
			open (D, ">", expand_filename("~$user/export/$name.$ldev.unexport"));
			open (I, "<", expand_filename("~$user/export/$name.$ldev"));
			while (<I>) {
				$_ =~ s/^export=/unexport=/i;
				print D $_;
			}
			close (I);
			close (D);
			syslog('notice', "USB device $name ($ldev) successfully unmounted");
		}
		elsif ( -e "$mntdir/$ldev.encrypted" )
		{
			# inform x2goclient that the device has been removed
			# release the encrypted device mapping
			unlink ("$mntdir/$ldev.encrypted");
			open ( D, ">", expand_filename("~$user/logins/$name.$ldev.unexport"));
			print D "logout=/media/$name/$ldev\n";
			system("umount /media/$name/$ldev");
			system("/sbin/cryptsetup luksClose keystick");
			close (D);
		}
	}

	close (F);
} else {
	syslog('notice', "not in any thinclient mode, exiting");
}
