#!/usr/bin/env python

# This file is part of Window-Switch.
# Copyright (c) 2009-2013 Antoine Martin <antoine@nagafix.co.uk>
# Window-Switch is released under the terms of the GNU GPL v3

import os
import sys
import time
import thread

from winswitch.consts import APPLICATION_NAME, NOTIFY_INFO, NOTIFY_AUTH_ERROR, NOTIFY_ERROR, LOCALHOST
from winswitch.globals import LOCALE_LANGUAGE, HOSTNAME, USERNAME, WIN32, OSX
from winswitch.objects.server_settings import ServerSettings
from winswitch.objects.server_config import ServerConfig
from winswitch.objects.session import Session
from winswitch.objects.server_command import ServerCommand
from winswitch.net.net_util import get_port_mapper
from winswitch.net.protocol import ProtocolHandler
from winswitch.util.format_util import bindecode
from winswitch.util.process_util import subprocess_terminate, exec_nopipe
from winswitch.util.file_io import get_server_hostkey_filename, get_local_server_config_filename, load_object_from_properties, get_client_session_sound_log_filename, get_server_signatureimage_filename
from winswitch.util.common import visible_command, get_bool, get_int, csv_list, is_valid_file, get_as_root_list, load_binary_file, save_binary_file, delete_if_exists, parse_csv, generate_UUID
from winswitch.util.config import get_settings, modify_server_config, load_server_signatureimage
from winswitch.util.crypt_util import sign_long, encrypt_salted_hex, make_key_fingerprint
from winswitch.util.vash_util import generate_image
from winswitch.util.main_loop import callFromThread, callLater
from winswitch.util.simple_logger import Logger, msig
from winswitch.util import startmenu

from winswitch.net.commands import SET_SALT, SET_HOST_INFO, SET_REMOTE_KEY, AUTHENTICATION_FAILED, AUTHENTICATION_SUCCESS, VERSION, RECEIVE_SESSION_SOUND,\
		SYNC_END, SEND_SESSION, SYNC, CLOSE_SESSION, ADD_SESSION, \
		ADD_SERVER_COMMAND, REQUEST_USER_ICON, SEND_MESSAGE, ADD_USER, SET_COMMAND_ICON, SET_TUNNEL_PORTS, \
		REQUEST_SESSION_SOUND, SEND_FILE_DATA, ACK_FILE_DATA, CANCEL_FILE_TRANSFER, \
		XDG_OPEN


LOG_ALL_MESSAGES_SENT = "--log-all-messages-sent" in sys.argv

ALIVE_CHECK_DELAY = 60					#send a ping regularly

TRUST_TUNNELLED_SERVERS = True			#TODO: make this configurable - paranoid may not want to connect to servers even tunnelled to a host they know...

#useful for testing
IGNORE_REMOTE_ICONS = False
PREFER_LOCAL_ICONS = True
IGNORE_LOCAL_ICONS = False

port_mapper = get_port_mapper()

class ServerLineConnection:
	"""
		This class is used in combination with a LineReceiver (or other..) and the ProtocolHandler to talk to a server instance.
		The stop method can be called with retry=False and an optional explanation message.
		The LineReceiver must provide a write() method and will pass in data via lineReceived()
		The server's counter-party is WinSwitchClientChannel.
	"""

	def __init__(self, server_link, write, stop, is_connected=None):
		Logger(self)
		self.server_link = server_link
		self.server = self.server_link.server
		self.write = write
		self.stop = stop
		self.is_connected = is_connected or self.server.is_connected
		self.slog(None, server_link, write, stop)
		self.handler = None
		self.settings = get_settings()
		self.on_sync_end = []
		self.disconnect_callbacks = []
		self.bytes_in = 0
		self.bytes_out = 0
		self.file_copy = {}

	def __str__(self):
		return	"ServerLineConnection(%s)" % self.server

	def connectionMade(self):
		self.server.xmodmap_sent = False
		self.handler = ProtocolHandler(self.server, self.send_message, self.stop, self.is_connected)
		self.handler.local_key = self.settings.get_key()
		self.add_handlers()
		self.handler.send_salt()			#so the other end can authenticate itself
		self.request_all()

	def add_handlers(self):
		self.add_default_handlers()

	def add_default_handlers(self):
		def ah(cmd, cb):
			self.handler.add_command_handler(cmd, cb)
		ah(SET_SALT, self.do_set_salt)
		ah(SET_HOST_INFO, self.set_host_info)
		ah(SET_REMOTE_KEY, self.do_set_remote_key)
		ah(AUTHENTICATION_FAILED, self.authentication_failed)
		ah(AUTHENTICATION_SUCCESS, self.authentication_success)
		ah(VERSION, self.do_version)
		ah(SYNC_END, self.handler.noop)

	def add_trusted_handlers(self):
		def ah(cmd, cb):
			self.handler.add_command_handler(cmd, cb)
		ah(SYNC_END, self.sync_end)
		ah(SEND_SESSION, self.do_send_session)
		ah(SYNC, self.request_all)
		ah(CLOSE_SESSION, self.close_session)
		ah(ADD_SESSION, self.add_session)
		ah(ADD_SERVER_COMMAND, self.do_add_server_command)
		ah(REQUEST_USER_ICON, self.request_user_icon)
		ah(SEND_MESSAGE, self.do_message)
		ah(ADD_USER, self.do_add_user)
		ah(SET_COMMAND_ICON, self.do_set_command_icon)
		ah(SET_TUNNEL_PORTS, self.do_set_tunnel_ports)
		ah(REQUEST_SESSION_SOUND, self.do_request_session_sound)
		ah(RECEIVE_SESSION_SOUND, self.do_receive_session_sound)
		ah(ACK_FILE_DATA, self.ack_file_data)
		ah(CANCEL_FILE_TRANSFER, self.cancel_file_transfer)
		ah(XDG_OPEN, self.do_xdg_open)

	def lineReceived(self, line):
		if line:
			self.bytes_in += len(line)
		self.handle_command(line)

	def handle_command(self, command):
		if not self.handler:
			self.serr(None, visible_command(command), Exception("No handler set!"))
		else:
			return	self.handler.handle_command(command)

	def send_message(self, msg):
		if LOG_ALL_MESSAGES_SENT:
			self.sdebug(None, visible_command(msg))
		if msg:
			self.bytes_out += len(msg)
		callFromThread(self.write, msg)

	def clear_on_sync_end(self):
		self.sdebug()
		self.on_sync_end = []

	# Handlers for messages:
	def sync_end(self, *args):
		self.sdebug("on_sync_end=%s" % str(self.on_sync_end), *args)
		if not self.on_sync_end:
			return
		for callback in self.on_sync_end:
			callback()

	def authentication_failed(self, *args):
		self.sdebug(None, *args)
		self.server_link.notify("Authentication Failed on %s" % self.server.get_display_name(),
							"Please check the username and password in the server configuration",
							notification_type=NOTIFY_AUTH_ERROR)
		self.server_link.stop_requested = True
		self.server_link.server.invalid_login = True
		self.server_link.server.touch()
		self.stop()

	def authentication_success(self, *args):
		self.sdebug(None, *args)
		self.handler.add_authenticated_command_handlers(False, True)
		self.server_link.notify_embargo = time.time()+5		#hide notifications until we fire "notify_new_server"
		if self.server.invalid_login:
			self.server.invalid_login = False
			self.server.touch()
		self.clear_on_sync_end()
		if self.server.auto_resume:
			self.on_sync_end.append(self.server_link.resume_sessions)
		self.server.xmodmap_sent = False
		self.may_send_xmodmap()
		callLater(2, self.notify_new_server)
		#could also use sync_end to fire it: self.handler.on_sync_end.append(lambda : self.notify_new_server)
		self.request_all()

	def may_send_xmodmap(self):
		""" Sends the xmodmap if we have it and it hasn't been sent yet """
		has_xmodmap_data = bool(self.settings.xmodmap_keys) or bool(self.settings.xmodmap_modifiers)
		self.slog("has_xmodmap_data=%s, xmodmap_sent=%s" % (has_xmodmap_data, self.server.xmodmap_sent))
		if not has_xmodmap_data or self.server.xmodmap_sent:
			return
		self.do_send_xmodmap()

	def do_send_xmodmap(self):
		self.slog("sending %s bytes of xmodmap_keys and %s bytes of xmodmap_modifiers to %s" % (len(self.settings.xmodmap_keys or ""), len(self.settings.xmodmap_modifiers or ""), self.server))
		self.handler.send_xmodmap(self.settings.xmodmap_keys, self.settings.xmodmap_modifiers,
								self.settings.xkbmap_print, self.settings.xkbmap_query,
								self.settings.xkbmap_layout, self.settings.xkbmap_variant)
		self.server.xmodmap_sent = True


	def notify_new_server(self):
		if self.server.local and not self.settings.notify_local_server:
			return
		if not self.is_connected():
			# already dropped...
			return
		self.server_link.notify_embargo = None
		users = self.server.get_users()
		others = []
		for user in users:
			if user.uuid != self.settings.uuid:
				others.append(user)
		count = len(others)
		text = "Successfully logged in as %s on %s. " % (self.server.username, self.server.get_display_name())
		if count==0:
			text += "\nThere are no other users connected"
		elif count==1:
			text += "\nThere is one other user connected: %s" % (others[0].name)
		else:
			text += "\nThere are %d other users connected" % count
		sessions = self.server.get_live_sessions(allow_shadow=False, ignore=[])
		count = len(sessions)
		if count==0:
			text += "\nThere are no live sessions"
		elif count==1:
			text += "\nThere is one live session: %s" % (sessions[0].name)
		else:
			text += "\nThere are %s live sessions" % count
		if self.server.local:
			title = "Connected to local server '%s'" % self.server.get_display_name()
		else:
			title = "Connected to server '%s'" % self.server.get_display_name()
		self.server_link.notify(title, text, notification_type=NOTIFY_INFO)

	def do_add_user(self, *args):
		UUID = args[3]
		is_us = UUID == self.settings.uuid
		user = self.handler.make_user_from_args(*args)
		mod = self.server.add_user(user)
		if mod and not user.avatar_icon_data:
			if is_us:
				self.sdebug("our uuid (%s) so using our own icon" % UUID, visible_command(str(args)))
				user.set_avatar_icon_data(self.settings.avatar_icon_data)
				return
			self.handler.send_request_user_icon(user.uuid)
		if mod and not is_us:
			who = user.name
			if not who:
				if user.remote_host:
					who = "%s@%s" % (user.remote_username, user.remote_host)
				else:
					who = "%s" % user.remote_username
			self.sdebug("new user: %s, showing as %s" % (user, who), visible_command(str(args)))
			where = self.server.name
			if not where:
				where = self.server.host
			msg = "%s joined server %s" % (who, where)
			self.server_link.notify(msg, "", notification_type=NOTIFY_INFO, from_uuid=UUID)


	def do_add_server_command(self, *args):
		command = self.handler.make_server_command_from_args(*args)
		added = self.server.add_command(command)
		#self.debug("(%s) added=%s" % (visible_command(str(args)), added))
		if not added:
			return
		#update start menu?
		if self.settings.create_start_menu_folders and command.type in [ServerCommand.COMMAND, ServerCommand.DESKTOP]:
			startmenu.add_command(self.server, command)
		#find icons for this new command/menu
		if not IGNORE_LOCAL_ICONS:
			if command.icon_filename not in command.icon_names:
				#old versions (pre 0.12.8) only had 'icon_filename'...
				command.icon_names.append(command.icon_filename)
			command.lookup_icon_data()
		if (not IGNORE_REMOTE_ICONS) and (not command.icon_data or not PREFER_LOCAL_ICONS):
			self.handler.send_request_command_icon(command.uuid, command.type)

	def do_set_command_icon(self, uuid, command_type, enc_data):
		command_obj = None
		#self.debug("do_set_icon(%s,[...]) all command=%s !" % (command, str(self.server.server_commands)))
		server_commands = self.server.get_command_list_for_type(command_type)
		for test in server_commands:
			#self.debug("do_set_icon(%s,[...]) testing='%s', command='%s'" % (command, test, test.command))
			if test.uuid == uuid:
				command_obj = test
				break
		if not command_obj:
			self.serror("command/ not found!", uuid, command_type, "[...]")
			return False
		data = bindecode(enc_data)
		command_obj.icon_data = data
		self.server.touch()
		startmenu.save_command_icon(command_obj.uuid, data)
		return True

	def do_send_session(self, ID, uuid, password):
		"""
		Either receive a session (attach to it)
		or we're asked to give it to someone else (detach)
		"""
		self.sdebug(None, ID, uuid, password)
		session = self.server.get_session(ID)
		if not session:
			self.serror("session not found!", ID, uuid, password)
			return
		our_uuid = self.settings.uuid
		if uuid!=our_uuid:
			#sending to someone else: we just disconnect here and let the server send it
			if session.status in [Session.STATUS_CONNECTED, Session.STATUS_IDLE]:
				if session.is_connected_to(our_uuid):
					self.server_link.session_detach(session)
				else:
					self.serror("uuid '%s' does not match! actor=%s, our UUID=%s" % (uuid, session.actor, our_uuid), ID, uuid, password)
			else:
				self.serror("cannot disconnect session in state %s" % session.status, ID, uuid, password)
			return
		else:
			#this is for us - connect!
			session.password = password
			self.server_link.prepare_session_ports(session, True)

	def close_session(self, ID):
		session = self.server.get_session(ID)
		self.slog("session=%s" % session, ID)
		if session:
			session.set_status(Session.STATUS_CLOSED)
		self.server.touch()
		#FIXME: call applet to close sessions...

	def add_session(self, *args):
		existing = self.server.get_session(args[0])
		was_pre_connected_here = existing and existing.preload and existing.pre_connected and existing.actor==self.settings.uuid
		session = self.handler.do_add_client_session(*args)
		self.sdebug("existing=%s, was_pre_connected_here=%s, session=%s" % (existing, was_pre_connected_here, session), "[...]")
		#ensure this connection is setup (if not already)
		if session.status!=Session.STATUS_CLOSED:
			self.server_link.prepare_session_ports(session, False)
			if (not IGNORE_REMOTE_ICONS) and (not session.window_icon_data and not session.default_icon_data):
				self.request_session_icon(session, False)
				self.request_session_icon(session, True)
			""" pre-connect to local server sessions: """
			#only do this on posix
			if self.schedule_preconnect(session):
				pass
			elif was_pre_connected_here and session.preload is False:
				session.pre_connected = False
				self.slog("pre-connected session has now started - sound may need connecting", *args)
				self.server_link.may_connect_sound(session)

	def schedule_preconnect(self, session):
		if not self.server.local:
			return	False			#only pre-connect to local servers for now..
		if not session.preload:
			return	False			#pre-connect only makes sense for "preload" sessions
		if session.session_type=="xpra" and os.name!="posix":
			return	False			#FIXME: xpra preloading not available on non-posix systems (shows tray)
		callLater(1.0, self.pre_connect, session)
		return True

	def pre_connect(self, session):
		self.slog(None, session)
		def session_pre_connected(*args):
			self.slog("marking the session as pre_connected=%s" % session.preload)
			session.pre_connected = session.preload
		session.add_status_update_callback(None, Session.STATUS_CONNECTED, session_pre_connected, clear_it=True, timeout=30)
		self.server_link.connect_to_session(session, False)

	def add_current_user(self):
		settings = self.settings
		session_type = self.server.preferred_session_type
		token = sign_long(settings.get_key(), "%s" % self.handler.remote_salt)
		assert self.server.get_key()
		assert self.handler.remote_salt
		enc_password = encrypt_salted_hex(self.server.get_key(), self.handler.remote_salt, "%s" % self.server.password)
		username = self.server.username
		if self.server.administrator_login and self.server.administrator_login!=self.server.username:
			username = "%s/%s" % (self.server.username, self.server.administrator_login)
		self.sdebug("server.administrator_login=%s, server.username=%s, using login username=%s" % (self.server.administrator_login, self.server.username, username))
		ssh_key_data = ""
		if is_valid_file(self.server.ssh_pub_keyfile):
			ssh_key_data = self.handler.binencode(load_binary_file(self.server.ssh_pub_keyfile))
		#maybe we should detect those? (not sure how!)
		xpra_x11 = not OSX and not WIN32
		vnc_x11 = not WIN32
		self.handler.send(ADD_USER,
						[username, USERNAME, settings.name, settings.uuid,
						HOSTNAME,
						settings.crypto_modulus, settings.crypto_public_exponent,
						token, enc_password,
						LOCALE_LANGUAGE,
						settings.test_supports_xpra(), settings.test_supports_nx(), settings.test_supports_vnc(),
						session_type, self.server.line_speed,
						settings.tunnel_fs, settings.tunnel_sink, settings.tunnel_printer, settings.tunnel_source,
						self.server.ssh_tunnel,
						settings.test_supports_rdp(),
						ssh_key_data,
						xpra_x11, vnc_x11,
						sys.platform,
						self.handler.binencode(settings.xkbmap_print),
						settings.binary_encodings,
						settings.gstaudio_codecs, settings.gstvideo_codecs,
						settings.open_urls, settings.open_files,
						"pygtk",
						settings.supports_virtualbox,
						self.handler.binencode(settings.xkbmap_query),
                        settings.xkbmap_layout, settings.xkbmap_variant
						])

	def do_version(self, proto_ver, app_ver, info):
		self.server.version_info = info
		self.server.app_version = self.handler.check_parse_version(proto_ver, app_ver)
		#HACK: mark as connected here for conch... states would be better (like session.status)
		if self.server.app_version and not self.is_connected():
			self.server.set_status(ServerConfig.STATUS_CONNECTED)
			self.server.touch()
			self.alive_check()
		return	False

	def alive_check(self):
		if self.is_connected():
			self.handler.send_ping()
			callLater(ALIVE_CHECK_DELAY, self.alive_check)

	def do_set_salt(self, remote_salt):
		previous_salt = self.handler.remote_salt
		self.handler.do_set_salt(remote_salt)
		if not previous_salt or previous_salt!=self.handler.remote_salt:
			#salt has changed - server must have re-started, do some cleanup:
			if previous_salt:
				self.sdebug("salt has changed - server must have re-started, doing some cleanup", remote_salt)
			self.server.server_commands = []
			self.server.touch()
		return False

	def set_host_info(self, *args):
		self.sdebug("existing server.ID=%s" % self.server.ID, visible_command(str(args)))
		ID = args[0]
		s = self.server				#shorthand version since we use it a lot in here
		if s.ID:
			if ID!=s.ID:			#ensure the server matches the ID we expect!
				self.stop(False, "Server ID does not match configuration! Expected %s and received %s" % (s.ID, ID))
				return
		else:
			s.ID = ID
		s.remote_name = args[1]
		if not s.name or s.name==ServerConfig.DEFAULT_NAME:
			s.name = s.remote_name		#set name if still set to default
			if not s.name or s.name=="localhost.localdomain" or s.name=="localhost":
				if s.host=="127.0.0.1":
					s.name = "Local Server"			#could be a remote one via forwarded port... oh well, we're just trying to help, set the name yourself!
				else:
					if s.port==22:
						s.name = s.host
					else:
						s.name = "%s:%s" % (s.host, s.port)
		s.type = args[2]
		s.ssh_host_public_key = args[3]
		s.supports_xpra = get_bool(args[4])
		xpra_version = args[5]
		local_xpra_version = self.settings.xpra_version
		if s.supports_xpra:
			self.sdebug("server supports xpra: local version of xpra is %s, remote version is %s" % (local_xpra_version, xpra_version), "[..]")
			if local_xpra_version and xpra_version:
				if not self.xpra_version_compat(local_xpra_version, xpra_version):
					#xpra version mismatch:
					self.server_link.notify("Xpra version mismatch",
										"xpra version on the server is %s, your version is %s. Xpra support has been disabled" % (xpra_version, local_xpra_version),
										notification_type=NOTIFY_ERROR)
					s.supports_xpra = False
		s.supports_nx = get_bool(args[6])
		s.supports_vnc = get_bool(args[7])
		s.start_time = long(args[8])
		s.supports_xpra_desktop = s.supports_xpra and get_bool(args[9])
		s.supports_ssh_desktop = get_bool(args[10])
		s.xnest_command = args[11]
		s.supports_vncshadow = get_bool(args[12])
		s.supports_file_open = get_bool(args[13])
		s.supports_ssh = get_bool(args[14])
		s.platform = args[15]
		s.os_version = args[16]
		self.sdebug("remote platform info: %s, %s" % (s.platform, s.os_version), "[..]")
		s.clients_can_stop = get_bool(args[17])
		s.supports_rdp = get_bool(args[18])
		s.supports_rdp_seamless = get_bool(args[19])
		s.rdp_seamless_command = args[20]
		s.rdp_port = int(args[21])
		s.rdp_version = args[22]
		self.sdebug("RDP: support=%s, seamless=%s, seamless_command=%s, port=%s, version=%s" %
					(s.supports_rdp, s.supports_rdp_seamless, s.rdp_seamless_command, s.rdp_port, s.rdp_version), "[..]")
		s.supports_sound = False	#used to be: get_bool(args[23]), but now below (changed protocol..)
		s.allow_custom_commands = len(args)>=25 and get_bool(args[24])
		s.allow_file_transfers = len(args)>=26 and get_bool(args[25])
		if len(args)>=27:
			s.download_directory = args[26]
		s.supports_gstvideo = len(args)>=28 and get_bool(args[27])
		s.supports_sound = len(args)>=29 and get_bool(args[28])
		if len(args)>=30:
			encs = parse_csv(args[29])
			if len(encs)>0:
				s.binary_encodings = encs
				self.handler.binary_encodings = encs
		if len(args)>=32:
			s.gstaudio_codecs = parse_csv(args[30])
			s.gstvideo_codecs = parse_csv(args[31])
		if len(args)>=34:
			s.locales = parse_csv(args[32])
			s.default_locale = args[33]
		s.supports_virtualbox = len(args)>=36 and get_bool(args[34])
		s.supports_screen = len(args)>=36 and get_bool(args[35])
		if len(args)>=37:
			s.supports_xpra_encodings = parse_csv(args[36])
		if len(args)>=38:
			s.supports_xprashadow = get_bool(args[37])
		self.sdebug("supports_sound=%s, allow_custom_commands=%s, allow_file_transfers=%s, download_directory=%s, supports_gstvideo=%s, gstaudio_codecs=%s, gstvideo_codecs=%s" %
					(s.supports_sound, s.allow_custom_commands, s.allow_file_transfers, s.download_directory, s.supports_gstvideo, s.gstaudio_codecs, s.gstvideo_codecs), "[..]")
		self.sdebug("protocol support: xpra=%s (desktop=%s, shadow=%s), nx=%s, vnc=%s (shadow=%s), ssh=%s (desktop=%s)" %
				(s.supports_xpra, s.supports_xpra_desktop, s.supports_xprashadow, s.supports_nx, s.supports_vnc, s.supports_vncshadow, s.supports_ssh, s.supports_ssh_desktop), "[..]")
		#ensure default session types are available:
		s.validate_default_session_types()
		s.touch()

	def xpra_version_compat(self, v1, v2):
		try:
			n1 = [int(x) for x in v1.split(".")]
			n2 = [int(x) for x in v2.split(".")]
			assert len(n1)>=2
			assert len(n2)>=2
			#we no longer support versions older than 0.1
			return n1[:3]>=[0,1] and n2[:3]>=[0,1]
		except Exception, e:
			self.serror("%s" % e, v1, v2)
		return	False

	def do_set_remote_key(self, mod, pub_e, proof):
		sig = msig(visible_command(mod), pub_e, visible_command(proof))
		key = self.handler.verify_key(mod, pub_e, proof)
		if not key:
			self.error(sig+" invalid key!")
			self.stop(False, "Invalid key: verification failed!")
			return	False
		modulus = long(mod)
		public_exponent = long(pub_e)
		if modulus==self.server.crypto_modulus and public_exponent==self.server.crypto_public_exponent:
			self.debug(sig+" key for %s has not changed" % self.server.get_display_name())
			self.trust_server(key, False)
			return False
		if TRUST_TUNNELLED_SERVERS and self.server.ssh_tunnel:
			self.log(sig+" automatically trusting tunnelled server: %s" % self.server.get_display_name())
			self.trust_server(key)
			return False

		#test if this is one of our local servers:
		for as_root in get_as_root_list():
			filename = get_local_server_config_filename(as_root)
			if is_valid_file(filename):
				local_config = load_object_from_properties(filename, ServerSettings, constructor=lambda:ServerSettings(True))
				if local_config:
					if local_config.crypto_modulus==modulus and local_config.crypto_public_exponent==public_exponent:
						self.debug(sig+" key belongs to local server '%s'" % local_config)
						self.server.local = True
						self.trust_server(key)
						return	False
		self.sdebug("server=%s, modulus=%s, new modulus=%s, public_exponent=%s, new public_exponent=%s" % (self.server.get_display_name(), self.server.crypto_modulus, modulus, self.server.crypto_public_exponent, public_exponent))
		new_key_fingerprint = make_key_fingerprint(modulus, public_exponent)
		if (self.server.crypto_modulus==None or self.server.crypto_modulus==0l) and (self.server.crypto_public_exponent==None or self.server.crypto_public_exponent==0l):
			#No key recorded for this server yet
			if not self.server.verify_identity:
				self.log(sig+" identity verification is turned off, trusting the key without user checking")
				self.trust_server(key)
				return False
			title = "Confirm Server Identity?"
			text = "Connected to %s on %s.\nServer ID is %s,\nThe server's key signature is:\n%s" % (self.server.get_display_name(), self.server.host, self.server.ID, new_key_fingerprint)
		else:
			#Key mismatch!
			old_key_fingerprint = self.server.regenerate_key_fingerprint()
			title = "Warning: Server Identification Changed"
			text = "The key signature currently recorded for %s (ID=%s):\n%s\ndoes not match the key signature received:\n%s\nDo you still want to connect?" % (self.server.get_display_name(), self.server.ID, old_key_fingerprint, new_key_fingerprint)
			self.serror(sig+" Key changed? expected modulus=%s and exponent=%s, found modulus=%s and exponent=%s" % (self.server.crypto_modulus, self.server.crypto_public_exponent, modulus, public_exponent))
			#FIXME: should not have to clear the key on "dont_trust", should set in in "trust" instead!
		Q_UUID = "%s-CONFIRM-IDENTITY" % self.server.ID
		self.server_link.ask(title, text, self.dont_trust_server, lambda : self.trust_server(key), UUID=Q_UUID)
		self.disconnect_callbacks.append(lambda : self.server_link.cancel_ask(Q_UUID))		#remove dialog if connection is lost
		return False

	def dont_trust_server(self, *args):
		self.sdebug(None, *args)
		self.stop(message="Server key not trusted")

	def trust_server(self, new_key=None, save=True):
		self.sdebug(None, new_key, save)
		if new_key:
			self.server.set_key(new_key);
			self.handler.remote_key = new_key
		#signature image?
		if new_key and self.server.key_fingerprint_image is None:
			#this may take some time, so run it in a thread
			#but load the image from the main thread! (as gtk may be involved)
			filename = get_server_signatureimage_filename(self.server)
			def fingerprint_done(*args):
				load_server_signatureimage(self.server)
				self.server.touch()
			generate_image(self.server.key_fingerprint, filename, ok_cb=fingerprint_done)
		if save:
			self.server.touch()
			if new_key:
				modify_server_config(self.server, ["crypto_modulus", "crypto_public_exponent", "key_fingerprint"])
			self.save_host_key(self.server)
		callLater(0, self.start_login)

	def start_login(self):
		self.sdebug()
		self.add_trusted_handlers()
		self.add_current_user()

	#TODO: add it if the hostname has changed?
	def save_host_key(self, server):
		if not server.ssh_host_public_key:
			if server.platform.startswith("win"):
				self.slog("not expecting any ssh public key data for a Windows server", server)
			else:
				self.serror("no ssh public key data to save", server)
			return
		filename = get_server_hostkey_filename(server)
		self.slog("to file '%s'" % filename, server)
		data = "# SSH host keys added by %s" % APPLICATION_NAME
		key = server.ssh_host_public_key
		pos = server.ssh_host_public_key.rfind("==")
		if pos>0:
			key = server.ssh_host_public_key[:pos+2]
		data += "\n%s %s\n" % (server.host, key)
		save_binary_file(filename, data)

	def request_all(self, *args):
		cutoff = time.time()
		def sync_end_check_timeout():
			self.server.check_timeout(cutoff)
		self.sdebug("cutoff=%s" % cutoff, *args)
		self.on_sync_end.append(sync_end_check_timeout)
		self.server.last_sync_sent = time.time()
		self.handler.send_version()
		self.handler.send_sync()

	def kill_session(self, session):
		self.sdebug(None, session)
		self.handler.send_kill_session(session.ID)

	def open_file(self, filename, mode):
		self.sdebug(None, filename, mode)
		self.handler.send_open_file(filename, mode)

	def start_session(self, uuid, session_type, screen_size, opts):
		self.sdebug(None, uuid, session_type, screen_size, opts)
		self.handler.send_start_session(uuid, session_type, screen_size, opts)

	def shadow_session(self, ID, read_only, shadow_type, screen_size, options):
		self.sdebug(None, ID, read_only, shadow_type, screen_size, options)
		self.handler.send_shadow_session(ID, read_only, shadow_type, screen_size, options)

	def set_session_icon(self, session):
		self.sdebug(None, session)
		self.handler.send_session_icon(session, False)

	def set_session_status(self, session):
		self.sdebug(None, session)
		self.handler.send_session_status(session.ID, session.status, session.actor, session.preload, session.screen_size)

	def send_session_to_user(self, session, uuid):
		self.sdebug(None, session, uuid)
		self.handler.send_session_to_user(session.ID, uuid, session.password)

	def request_session_icon(self, session, large):
		self.handler.send_request_session_icon(session.ID, large)

	def request_user_icon(self, uuid):
		if uuid!=self.settings.uuid:
			self.serror("request is not for our user ID!", uuid)
			return
		self.send_avatar()

	def send_avatar(self):
		self.handler.send_user_icon(self.settings.uuid, self.settings.avatar_icon_data)

	def do_message(self, uuid, title, message, *args):
		self.slog(None, uuid, title, message, *args)
		from_uuid = None
		if len(args)>0:
			from_uuid = args[0]
		if uuid!=self.settings.uuid:
			self.serror("message received is not for us!", uuid, title, message)
			return
		self.server_link.notify(title, message, notification_type=NOTIFY_INFO, from_uuid=from_uuid)
		return False

	def do_set_tunnel_ports(self, smb, ipp, *args):
		samba_port = int(smb)
		ipp_port = int(ipp)
		old_samba = self.server.remote_samba_tunnel_port
		old_ipp = self.server.remote_ipp_tunnel_port
		self.sdebug("old ports: smb=%s, ipp=%s" % (old_samba, old_ipp), smb, ipp, *args)
		if old_ipp==ipp_port and old_samba==samba_port:
			return			#no change
		self.server_link.tunnel_ports_changed(old_samba, old_ipp, samba_port, ipp_port)

	def send_mount_points(self):
		self.sdebug("mount points=%s" % csv_list(self.settings.mount_points))
		for mp in self.settings.mount_points:
			self.handler.send_mount_point(mp.protocol, mp.namespace, mp.host, mp.port, mp.path, mp.type, mp.auth_mode, mp.username, mp.password, mp.comment, mp.options)


	def do_request_session_sound(self, *args):
		self.serror("should not be called on client!", *args)

	def request_sound(self, session, start, in_or_out, monitor, codec=None, codec_options={}):
		"""	Ask the server to start/stop the sound pipe
			do_receive_session_sound will be called when the tcp server is ready/stopped.
		"""
		if codec is None:
			codec = self.settings.default_gstaudio_codec
		self.handler.request_session_sound(session.ID, start, in_or_out, monitor, codec, codec_options)

	def do_receive_session_sound(self, ID, start, in_or_out, monitor, port, codec=None, codec_options={}):
		"""
		Server is telling us that the sound sender has started/stopped.
		So we start (or stop) the receiver end.
		"""
		session = self.server.get_session(ID)
		if not session:
			self.serror("session not found!", ID, start, in_or_out, monitor, port)
			return
		self.sdebug("session=%s" % session, ID, start, in_or_out, monitor, port)
		self.do_receive_session_sound_for(session, get_bool(start), get_bool(in_or_out), get_bool(monitor), get_int(port), codec, codec_options)

	def do_receive_session_sound_for(self, session, start, in_or_out, monitor, port, codec, codec_options):
		if start:
			self.start_sound(session, in_or_out, monitor, port, codec, codec_options)
		else:
			session.stop_sound(in_or_out, monitor)

	def start_sound(self, session, in_or_out, monitor, remote_port, codec, codec_options):
		if not self.settings.supports_sound:
			self.serror("sound support is disabled!", session, in_or_out, monitor, remote_port, codec, codec_options)
			return
		if not self.server.supports_sound:
			self.serror("server does not support sound!?", session, in_or_out, monitor, remote_port, codec, codec_options)
			return

		if in_or_out:
			if not self.settings.tunnel_sink:
				self.serror("soundin is disabled!", session, in_or_out, monitor, remote_port, codec, codec_options)
				return
			gst_mod = self.settings.gst_sound_sink_module
			gst_mod_opts = self.settings.gst_sound_sink_options
		else:
			if not self.settings.tunnel_source:
				self.serror("soundout is disabled!", session, in_or_out, monitor, remote_port, codec, codec_options)
				return
			if monitor:
				gst_mod = session.gst_clone_plugin
				gst_mod_opts = session.gst_clone_plugin_options
			else:
				gst_mod = self.settings.gst_sound_source_module
				gst_mod_opts = self.settings.gst_sound_source_options

		#where to connect
		if self.server.ssh_tunnel and not self.server.local:
			#find existing/start tunnel:
			forwarder = self.server_link.add_session_port_forward(session, None, remote_port)
			#SSH: connect to local end of tunnel:
			_, port, _, _ = forwarder
			host = LOCALHOST
		else:
			#Direct!
			host = self.server.host
			port = remote_port
		if port<0:
			self.serror("illegal port: %s" % port, session, in_or_out, monitor, remote_port, codec, codec_options)
			return

		def gst_process_started(proc):
			""" Ensure we kill the process automatically when the session closes """
			self.slog(None, proc)
			session.local_sound_pipeline[in_or_out] = proc
			session.local_sound_is_monitor[in_or_out] = monitor
			session.touch()
			def stop_session_sound():
				self.slog()
				subprocess_terminate(proc)
			session.add_status_update_callback(None, Session.STATUS_CLOSED, stop_session_sound, clear_it=True, timeout=None)
		def gst_process_ended(proc):
			self.slog(None, proc)
			session.stop_sound(in_or_out)
			session.touch()
			#tell server to stop sending/receiving:
			self.request_sound(session, False, not in_or_out, monitor)
		try:
			from winswitch.util.gstreamer_util import start_gst_sound_pipe
			from winswitch.sound.sound_util import get_session_sound_env
			log_filename = get_client_session_sound_log_filename(session.ID, True)
			delete_if_exists(log_filename)
			name = "%s on %s" % (session.name, self.server.name)
			env = get_session_sound_env(session, True, name)
			start_gst_sound_pipe(False, gst_process_started, gst_process_ended, log_filename, env, name, in_or_out, "client", host, port, gst_mod, gst_mod_opts, codec, codec_options)
		except Exception, e:
			self.serr("failed to create soundin", e, session, in_or_out, monitor, remote_port, codec, codec_options)


	def do_xdg_open(self, argument):
		self.slog(None, argument)
		if os.name=='posix':
			exec_nopipe(['xdg-open', argument])
			return
		if os.name=='mac':
			exec_nopipe(['open', argument], shell=True)
			return
		if argument.startswith("http"):
			import webbrowser
			webbrowser.open(argument)
			return
		if os.name=='nt':
			exec_nopipe(['start', argument], shell=True)
		else:
			self.serr("unsupported os: %s" % os.name, argument)

	def send_files(self, filenames, set_progress, close_progress):
		self.sdebug(None, filenames)
		fcs = []
		i = 0
		files_progress = {}
		for filename in filenames:
			try:
				def file_set_progress(_filename, progress):
					""" record the current progress for the filename given, then calculate total and show it """
					self.sdebug(None, _filename, progress)
					files_progress[_filename] = progress
					v = 0
					for p in files_progress.values():
						v += p
					total = v/len(files_progress)
					if total<1:
						set_progress(total)
					else:
						close_progress()

				fc = self.send_file(filename, file_set_progress, close_progress)
				fcs.append(fc)
				i += 1
			except Exception, e:
				self.serr("error on %s" % filename, e, filenames)
		return	fcs

	def send_file(self, filename, set_progress, close_progress):
		self.sdebug(None, filename, set_progress, close_progress)
		def error_cb(title, message):
			self.server_link.notify(title, message, notification_type=NOTIFY_ERROR)
			close_progress()
		fc = FileCopy(self.handler, filename, set_progress, error_cb)
		self.file_copy[fc.id] = fc
		fc.start()
		self.slog("starting file copy %s" % fc.id, filename, set_progress, close_progress)
		return	fc

	def ack_file_data(self, filetransfer_id, digest, *args):
		self.sdebug(None, filetransfer_id, digest, *args)
		fc = self.file_copy.get(filetransfer_id)
		if fc:
			fc.ack(digest)
		else:
			self.serror("file copy not found in %s" % str(self.file_copy), filetransfer_id, digest, *args)

	def cancel_file_transfer(self, filetransfer_id, *args):
		fc = self.file_copy.get(filetransfer_id)
		if fc:
			fc.close()
		else:
			self.serror("file copy not found!", filetransfer_id, *args)


class FileCopy:

	def __init__(self, handler, filename, set_progress, error_cb):
		Logger(self)
		self.filename = filename
		self.basename = os.path.basename(filename)
		self.handler = handler
		self.set_progress = set_progress
		self.error_cb = error_cb
		self.fd = None
		self.chunk_size = 4096
		self.data_sent = 0
		self.file_size = os.stat(filename).st_size
		if self.file_size>self.chunk_size*100:
			self.chunk_size = 32768
		import hashlib
		self.filetransfer_id = generate_UUID()
		self.md5 = hashlib.md5()
		self.last_digest = None
		self.last_ack = None
		self.eof = False
		self.cancelled = False
		self.check_ack = None

	def start(self):
		self.fd = open(self.filename, 'rb')
		self.send_bytes()

	def ack(self, digest):
		if digest!=self.last_digest:
			self.serror("digest does not match last: %s" % self.last_digest, digest)
			self.close()
		else:
			self.last_ack = digest
			self.send_bytes()

	def send_bytes(self):
		if self.eof or self.cancelled:
			self.sdebug("already ended. eof=%s, cancelled=%s" % (self.eof, self.cancelled))
			return
		if self.fd is None:
			self.serror("file descriptor already closed!")
			return
		thread.start_new_thread(self.do_send_bytes, ())

	def do_send_bytes(self):
		position = self.fd.tell()
		data = self.fd.read(self.chunk_size)
		self.data_sent += len(data)
		self.md5.update(data)
		self.last_digest = self.md5.hexdigest()
		self.slog("sending %s bytes of %s at position %s, digest=%s" % (len(data), self.basename, position, self.last_digest))
		self.eof = self.chunk_size!=len(data)
		self.handler.send(SEND_FILE_DATA, [self.filetransfer_id, self.basename, position, self.handler.binencode(data), self.last_digest, self.eof])
		if self.eof:
			self.slog("file end")
			self.close()
			callFromThread(self.set_progress, self.filename, 1.0)
		else:
			def check_progress(ack):
				self.sdebug("last_ack=%s" % self.last_ack, ack)
				if not self.eof and not self.cancelled and self.last_ack==ack:
					self.error_cb("File transfer has failed", "The file %s was not transferred, the network copy has timed out." % self.basename)
					self.serror("ack digest unchanged - transfer abandoned")
					self.close()
			if self.check_ack and self.check_ack.active():
				self.check_ack.cancel()
			self.check_ack = callLater(30, check_progress, self.last_ack)
			if self.data_sent>0:
				callFromThread(self.set_progress, self.filename, float(self.data_sent)/self.file_size)

	def cancel(self):
		if self.eof or self.cancelled:
			return
		self.cancelled = True
		self.handler.send(CANCEL_FILE_TRANSFER, self.filetransfer_id)
		self.close()

	def close(self):
		if self.fd:
			self.fd.close()
			self.fd = None
			self.md5 = None
		else:
			self.serror("already closed")
