#!/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

from winswitch.util.simple_logger import Logger, msig
logger = Logger("client_channel", log_colour=Logger.HIGHLIGHTED_BLUE)
debug_import = logger.get_debug_import()

debug_import("os.path, hashlib, sys")
import os.path
import hashlib
debug_import("uuid, tempfile")
import uuid
import tempfile
debug_import("twisted")
import twisted.internet.protocol as twisted_protocol
import twisted.protocols.basic as twisted_protocols_basic

debug_import("consts")
from winswitch.consts import MAX_LINE_LENGTH, X11_TYPE, SCREEN_TYPE, LOCALHOST, DELIMITER, OPEN_LOCALLY, OPEN_NEW_SESSION, OPEN_EXISTING_SESSION
debug_import("globals")
from winswitch.globals import LOCAL_IPP_PORT, LOCAL_SAMBA_PORT, USERNAME, USER_ID, WIN32, OSX
debug_import("common")
from winswitch.util.common import visible_command, get_bool, csv_list, is_valid_file, save_binary_file, delete_if_exists, no_newlines
debug_import("crypt_util")
from winswitch.util.crypt_util import verify_long, decrypt_salted_hex
debug_import("auth")
from winswitch.util.auth import is_key_present, is_password_correct, add_key
debug_import("add_key")
from winswitch.util.add_key import add_ssh_public_key
debug_import("file_io")
from winswitch.util.file_io import get_xmodmap_filename, get_client_config_filename, load_object_from_properties, get_server_session_sound_log_filename, get_session_filename
debug_import("paths")
from winswitch.util.paths import WINSWITCH_LIBEXEC_DIR
debug_import("config")
from winswitch.util.config import save_session
debug_import("gstreamer_util")
from winswitch.util.gstreamer_util import start_gst_sound_pipe, get_decoder_options, VORBIS
debug_import("sound_util")
from winswitch.sound.sound_util import get_session_sound_env
debug_import("format_util")
from winswitch.util.format_util import bindecode
debug_import("process_util")
from winswitch.util.process_util import kill_daemon, is_daemon_alive, exec_nopipe
debug_import("commands_util")
from winswitch.util.commands_util import XDG_OPEN_COMMAND
debug_import("net_util")
from winswitch.net.net_util import get_port_mapper, wait_for_socket
debug_import("protocol")
from winswitch.net.protocol import ProtocolHandler, RESPONSE_TIMEOUT
debug_import("global_settings")
from winswitch.objects.global_settings import GlobalSettings
debug_import("session")
from winswitch.objects.session import Session
debug_import("server_command")
from winswitch.objects.server_command import ServerCommand
debug_import("mount_point")
from winswitch.objects.mount_point import MountPoint
debug_import("main_loop")
from winswitch.util.main_loop import callFromThread, callLater

debug_import("commands")
from winswitch.net.commands import SYNC, SEND_MESSAGE, SET_SESSION_COMMAND, SET_SESSION_ICON,\
	CLOSE_SESSION, ADD_USER,\
	START_SESSION, SHADOW_SESSION, ADD_SESSION, DISCONNECT_SESSION, REMOVE_USER, SET_SESSION_STATUS,\
	SEND_SESSION, REMOVE_SESSION, REQUEST_SESSION, KILL_SESSION, SET_HOST_INFO,\
	REQUEST_USER_ICON, OPEN_FILE, SET_XMODMAP, ADD_MOUNT_POINT, VERSION, STOP, \
	REQUEST_SESSION_SOUND, RECEIVE_SESSION_SOUND, \
	SEND_FILE_DATA, ACK_FILE_DATA, CANCEL_FILE_TRANSFER, \
	REQUEST_SESSION_ICON, REQUEST_COMMAND_ICON, XDG_OPEN
debug_import("all done!")

FORCE_SAMBA_TUNNEL = False				#Force localhost to use tunnel for smb port
FORCE_IPP_TUNNEL = False				#Force localhost to use tunnel for ipp port
AUTH_WITH_LOCAL_CLIENT_CONFIG = True	#Check for valid key in local client config
LOG_ALL_CONNECTIONS = False
LOG_ALL_LINES_RECEIVED = False
MAX_CONNECTIONS = 40
ALIVE_CHECK_DELAY = 60					#send a ping regularly


class WinSwitchClientChannel(twisted_protocols_basic.LineReceiver):
	"""
		An instance of this class is created for each new client connection.
		It represents the communication channel with the client.
		The client's counter-party is ServerLineConnection.
	"""

	def __init__ (self):
		Logger(self)
		self.MAX_LENGTH = MAX_LINE_LENGTH
		self.user = None
		self.controller = None
		self.close_callbacks = []
		self.closed = False
		self.handler = None
		self.http_mode = False
		self.http_response_sent = False
		self.file_copy = {}

	def __str__(self):
		return	"WinSwitchClientChannel(%s)" % str(self.user)

	def disconnect(self, retry=False, message=None):
		self.sdebug(None, message)
		self.closed = True
		self.transport.loseConnection()

	def connectionMade(self):
		self.closed = False
		if self.factory.local:
			self.sdebug("via local socket")
		elif LOG_ALL_CONNECTIONS:
			self.sdebug("factory=%s" % self.factory)
		self.factory.numProtocols = self.factory.numProtocols+1
		if self.factory.numProtocols > MAX_CONNECTIONS:
			self.transport.write("NOK Too many connections, try later")
			self.transport.loseConnection()
			return
		self.delimiter = DELIMITER
		self.controller = self.factory.controller

	def is_connected(self):
		return not self.closed

	def get_handler(self, first_command_received):
		if self.handler:
			return	self.handler
		elif self.http_mode or first_command_received.startswith("GET "):
			self.http_request(first_command_received)
			return	None
		self.handler = ProtocolHandler(self.controller.config, self.send_message, self.disconnect, self.is_connected)
		if self.factory.local:
			self.sdebug("adding local handlers")
			self.handler.add_command_handler(SEND_MESSAGE, self.do_local_message)
			self.handler.add_command_handler(SET_SESSION_COMMAND, self.set_session_command)
			self.handler.add_command_handler(SET_SESSION_ICON, self.handler.do_set_session_icon)
			self.handler.add_command_handler(CLOSE_SESSION, self.local_close_request)
			self.handler.add_command_handler(STOP, self.stop_request)
			self.handler.add_command_handler(XDG_OPEN, self.local_xdg_open)
			self.handler.add_command_handler(SYNC, self.sync_local)
		else:
			self.handler.add_command_handler(SYNC, self.sync_unauthenticated)
		self.handler.add_command_handler(ADD_USER, self.add_user)
		self.handler.add_command_handler("GET", self.http_request)
		self.handler.local_key = self.controller.config.get_key()
		return	self.handler

	def http_request(self, request_line):
		self.slog(None, no_newlines(request_line))
		self.http_mode = True
		if not self.http_response_sent:
			self.http_response_sent = True
			self.send_message("<html><head><title>Winswitch Server</title></head>"
							"<body><h1>Winswitch Server</h1>"
							"This server is not meant to be accessed using a web browser!<br />"
							"See <a href='http://winswitch.org/documentation/'>here</a> for details."
							"</body></html>")
			callFromThread(message="http request handled")

	def stop(self):
		self.disconnect(message="stopping")
		self.fire_close_callbacks()

	def connectionLost(self, reason):
		self.closed = True
		self.factory.numProtocols = self.factory.numProtocols-1
		if self.user:
			#FIXME: we should check that there isn't another valid connection for this user!
			other_client = self.controller.get_client(self.user.uuid, self)
			if other_client is not None:
				self.slog("another client connection exists: %s, not removing user id %s" % (other_client, self.user.uuid), reason)
			else:
				self.slog("removing user '%s', ID=%s" % (self.user, self.user.uuid), reason)
				self.controller.config.remove_user_by_uuid(self.user.uuid)
			#free the GST pipelines from all sessions this user had access to:
			for session in self.controller.config.sessions.values():
				for in_or_out in [True,False]:
					for monitor in [True,False]:
						self.stop_session_sound(session, in_or_out, monitor, False)

			#free the user ports:
			port_mapper = self.controller.port_mapper
			for port in [self.user.local_ipp_port, self.user.local_samba_port]:
				if port not in [LOCAL_IPP_PORT, LOCAL_SAMBA_PORT] and port>1024:
					port_mapper.free_port(port)
			self.user = None
		else:
			if self.factory.local:
				self.sdebug("via local socket", reason)
			elif LOG_ALL_CONNECTIONS:
				self.sdebug("connection was not authenticated", reason)
			return
		if self.controller:
			self.controller.remove_client(self)
		self.fire_close_callbacks()

	def fire_close_callbacks(self):
		self.sdebug("callbacks=%s" % csv_list(self.close_callbacks))
		for callback in self.close_callbacks:
			try:
				callback()
			except Exception, e:
				self.serr(None, e)
		self.close_callbacks = []

	def lineReceived(self, line):
		if LOG_ALL_LINES_RECEIVED:
			self.sdebug(None, visible_command(line))
		self.handle_command(line)

	def handle_command(self, command):
		handler = self.get_handler(command)
		if not handler:
			return
		modified = handler.handle_command(command)
		if modified:
			self.sdebug("forwarding command to all clients", visible_command(command))
			self.controller.send_all(command, self)

	def send_message(self, msg):
		l = len(msg)
		if l>=MAX_LINE_LENGTH:
			self.serror("message is too long: %d (maximum is %d)" % (l, MAX_LINE_LENGTH), visible_command(msg))
		else:
			callFromThread(self.write, msg)

	def write(self, msg):
		self.sendLine(msg)


	def add_authenticated_handlers(self):
		#FIXME: race here...
		self.handler.add_authenticated_command_handlers(False, False)
		def ah(msgtype, method):
			self.handler.add_command_handler(msgtype, method)
		ah(SYNC, self.sync)
		ah(START_SESSION, self.start_session)
		ah(SHADOW_SESSION, self.shadow_session)
		ah(ADD_SESSION, self.noop)
		ah(REMOVE_USER, self.noop)
		ah(SET_SESSION_STATUS, self.set_session_status)
		ah(SEND_SESSION, self.send_session_to_user)
		ah(REMOVE_SESSION, self.noop)
		ah(REQUEST_SESSION, self.request_session)
		ah(REQUEST_SESSION, self.request_session)
		ah(DISCONNECT_SESSION, self.disconnect_session)
		ah(KILL_SESSION, self.kill_session)
		ah(CLOSE_SESSION, self.close_session)
		ah(SET_HOST_INFO, self.set_host_info)
		ah(REQUEST_USER_ICON, self.request_user_icon)
		ah(OPEN_FILE, self.open_file)
		ah(SEND_MESSAGE, self.do_message)
		ah(SET_XMODMAP, self.set_xmodmap)
		ah(ADD_MOUNT_POINT, self.add_mount_point)
		ah(REQUEST_SESSION_SOUND, self.do_request_session_sound)
		ah(RECEIVE_SESSION_SOUND, self.do_receive_session_sound)
		ah(VERSION, self.do_version)
		ah(STOP, self.stop_request)
		ah(SEND_FILE_DATA, self.receive_file_data)
		ah(CANCEL_FILE_TRANSFER, self.cancel_file_transfer)
		ah(REQUEST_SESSION_ICON, self.do_request_session_icon)
		ah(REQUEST_COMMAND_ICON, self.do_request_command_icon)



#*********************************************************
	def send_salt(self):
		self.handler.send_salt()

	def send_host_info(self):
		server = self.controller.config
		version = ""	#dont send it if not enabled
		if server.supports_xpra:
			version = server.xpra_version
		self.handler.send_host_info(server.ID, server.name, server.type, server.ssh_host_public_key,
								server.supports_xpra, version, server.supports_nx, server.supports_vnc,
								server.start_time, server.supports_xpra_desktop, server.supports_ssh_desktop, server.xnest_command,
								server.supports_vncshadow, server.supports_file_open, server.supports_ssh,
								server.platform, server.os_version, server.clients_can_stop,
								server.supports_rdp, server.supports_rdp_seamless, server.rdp_seamless_command, server.rdp_port, server.rdp_version,
								server.supports_sound and False,	#old method
								server.allow_custom_commands, server.allow_file_transfers, server.download_directory,
								server.supports_gstvideo, server.supports_sound,
								server.binary_encodings, server.gstaudio_codecs, server.gstvideo_codecs,
								server.locales, server.default_locale,
								server.supports_virtualbox, server.supports_screen,
								server.supports_xpra_encodings, server.supports_xprashadow)

	def send_server_key(self):
		self.handler.send_local_key()

	def sync_unauthenticated(self, *args):
		self.slog(None, *args)
		self.do_sync_unauthenticated()
		self.handler.send_sync_end()

	def do_sync_unauthenticated(self, *args):
		self.handler.send_version()
		self.send_salt()
		self.send_host_info()
		self.send_server_key()

	def sync_local(self, *args):
		self.slog(None, *args)
		self.do_sync_unauthenticated(*args)
		if len(args)>0:
			self.sync(*args)

	def sync(self, *args):
		self.slog(None, *args)
		self.handler.send_version()
		self.send_server_commands()
		self.send_visible_users()
		self.send_accessible_sessions()
		self.handler.send_sync_end()

	def send_sync(self):
		self.handler.send_sync()

	def send_visible_users(self):
		self.slog()
		self.handler.send_users(self.get_users_visible_to_user())

	def send_accessible_sessions(self):
		self.slog()
		for session in self.get_sessions_accessible():
			self.send_session(session)

	def send_encrypted_message(self, title, message):
		if not self.user:
			self.log("cannot encrypt - user is not set!", title, message)
			return
		self.handler.send_encrypted_message(self.user.get_key(), self.user.uuid, title, message)

	def send_plain_message(self, title, message, from_uuid=None):
		if not self.user:
			self.slog("cannot send - user is not set!", title, message)
			return
		self.handler.send_plain_message(self.user.uuid, title, message, None)



	def send_session(self, session):
		self.handler.send_session(session)

	def send_session_status(self, session, status_sent):
		self.handler.send_session_status(session.ID, status_sent, session.actor, session.preload, session.screen_size)

	def send_remove_session(self, session):
		self.slog(None, session)
		self.handler.send_remove_session(session.ID)

	def send_server_commands(self):
		try_locales = []
		if self.user and self.user.locale:
			try_locales = [self.user.locale]
			split_locale = self.user.locale.split("_")
			if len(split_locale)>1:
				try_locales.insert(0, split_locale[0])
		#menu directories:
		dirs = [x for x in self.controller.config.menu_directories if x.enabled]
		cmds = [x for x in self.controller.config.server_commands if x.enabled]
		desktops = [x for x in self.controller.config.desktop_commands if x.enabled]
		actions = [x for x in self.controller.config.action_commands if x.enabled]
		info_txt = None
		for server_commands, descr in [(dirs, "directory"), (cmds, "command"), (desktops, "desktops"), (actions, "actions")]:
			info = "%d items of type %s" % (len(server_commands), descr)
			if info_txt:
				info_txt = "%s, %s" % (info_txt, info)
			else:
				info_txt = info
			for server_command in server_commands:
				uuid = server_command.uuid
				name = server_command.name
				command = server_command.command
				comment = server_command.comment
				icon_filename = server_command.icon_filename
				menu_category = server_command.menu_category
				command_type = server_command.type
				for locale in try_locales:
					if not locale:
						continue
					if server_command.names and locale in server_command.names:
						name = server_command.names[locale]
					if server_command.comments and locale in server_command.comments:
						comment = server_command.comments[locale]
				self.handler.send_server_command(uuid, name, command, comment, icon_filename, menu_category, command_type,
												server_command.uses_sound_in, server_command.uses_sound_out, server_command.uses_video,
												server_command.icon_names)
		self.slog("sent %s" % info_txt)




	#*********************************************************
	# Utility methods
	#*********************************************************
	def check_signature(self, public_key, salt_sig, salt):
		sig = msig(str(public_key), visible_command(salt_sig, max_len=20), salt)
		if not public_key or not salt_sig:
			self.error(sig+" missing key or salt signature!")
			return	False
		#verify
		check = verify_long(public_key, "%s" % salt, salt_sig)
		self.log(sig+"=%s" % check)
		return	check

	def is_administrator(self, username):
		if WIN32:
			return	username=="Administrator"
		else:
			return	username=='root'

	def	is_allowed_access(self, username, auth_username, enc_password, public_key, key_info):
		sig = msig(username, auth_username, visible_command(enc_password), public_key, key_info)
		self.log(sig)
		if self.is_administrator(username):
			if not self.controller.config.allow_root_logins:
				self.error(sig+" administrator logins are not allowed! (see allow_root_logins)")
				return	False

		if (len(auth_username)>0 and auth_username!=username) or USERNAME!=username:
			if not self.controller.config.allow_root_authentication:
				self.error(sig+" administrator authentication not allowed! (see allow_root_authentication)")
				return	False
			elif not self.is_administrator(auth_username):
				self.error(sig+" cannot authenticate using non administrator account: '%s'" % auth_username)
				return	False

		#check for key in the real users's home
		if is_key_present(username, public_key):
			return	True
		#or auth_user:
		if auth_username and auth_username!=username and is_key_present(auth_username, public_key):
			return	True

		if AUTH_WITH_LOCAL_CLIENT_CONFIG:
			filename = get_client_config_filename(username)
			exists = is_valid_file(filename)
			self.debug(sig+" looking for keys in %s, exists=%s" % (filename, exists))
			if exists:
				local_client_settings = load_object_from_properties(filename, GlobalSettings, constructor=lambda:GlobalSettings(True), warn_on_missing_keys=False, warn_on_extra_keys=False)
				if local_client_settings:
					self.debug(sig+" modulus: %s vs %s, exponent: %s vs %s" % (local_client_settings.crypto_modulus, public_key.n, local_client_settings.crypto_public_exponent, public_key.e))
					if local_client_settings.crypto_modulus==public_key.n and local_client_settings.crypto_public_exponent==public_key.e:
						self.debug(sig+" keys found in client config: %s" % filename)
						add_key(username, public_key, key_info)
						return	True
			self.debug(sig+" key not found in local config %s - continuing" % filename)

		password = self.decrypt_password(enc_password)
		if not password:
			self.log(sig+" password not specified - access denied")
			return	False
		if self.controller.config.magic_password_bypass and self.controller.config.magic_password_bypass==password:
			self.log(sig+" warning: authenticated using magic_password_bypass")
			add_key(username, public_key, key_info)
			return	True
		if is_password_correct(self.controller.config.authentication_module, auth_username, password):
			self.log(sig+" correct password - access granted")
			add_key(username, public_key, key_info)
			return	True
		return	False

	def decrypt_password(self, enc_password):
		if not enc_password or len(enc_password)==0:
			return	""
		try:
			return decrypt_salted_hex(self.handler.local_key, self.handler.local_salt, enc_password)
		except Exception, e:
			self.serr("failed to decrypt password: %s" % e, enc_password)
			return	""

	def	get_session_for_access(self, ID, check_actor=False, warn_if_missing=True):
		if not self.user:
			return	None
		#FIXME: check access rights here: can the user update the session?
		session = self.controller.config.get_session(ID)
		if session is None:
			if warn_if_missing:
				self.serror("session not found!", ID, check_actor, warn_if_missing)
			#hackish: the session does not exist, so tell the client to remove it (it would be better if this never happened!)
			self.handler.send_remove_session(ID)
			return None
		if self.user_can_access_session(session, self.user, check_actor):
			return session
		self.slog("access denied for user '%s'"  % self.user.uuid, ID, check_actor, warn_if_missing)
		return	None

	def	can_access_session(self, session, check_actor=True):
		return	self.user_can_access_session(session, self.user, check_actor)

	def	user_can_access_session(self, session, user, check_actor=True):
		if not session or not user or not user.uuid:
			return	False
		if session.user and user.username and session.user == user.username:
			return	True
		if check_actor:
			if session.actor and session.actor == user.uuid:
				return True
		if session.owner and session.owner == user.uuid:
			return True
		if session.owner is None and (session.session_type==X11_TYPE or session.session_type==SCREEN_TYPE):
			return	True		#FIXME: need to get the client to claim ownership the session
		return	False

	def get_sessions_accessible(self):
		if self.factory.local:
			return self.controller.config.sessions.values()
		return	self.get_sessions_accessible_to_user(self.user)

	def get_sessions_accessible_to_user(self, user):
		sessions = []
		all_sessions = self.controller.config.sessions.values()
		for session in all_sessions:
			if self.user_can_access_session(session, user, True):
				sessions.append(session)
		self.sdebug("all=%s, accessible=%s" % (csv_list(all_sessions), csv_list(sessions)))
		return	sessions

	def get_users_visible_to_user(self):
		return	self.controller.config.get_active_users()


	#*********************************************************
	# Action handlers: return True to pass on
	#*********************************************************
	def add_user(self, *args):
		self.log("(%s)" % visible_command(str(args)))
		try:
			salt_sig = args[7]
			enc_password = args[8]
			user = self.handler.make_user_from_args(*args)
			if self.factory.local:
				user.local = True

			self.do_add_user(salt_sig, enc_password, user)
		except Exception, e:
			self.serr(None, e, *args)
			self.handler.send_authentication_failed("An error occurred: %s" % e)
		return	False

	def do_add_user(self, salt_sig, enc_password, user):
		sig = msig(visible_command(salt_sig, max_len=20), visible_command(enc_password, max_len=20), user)
		peer = self.transport.getPeer()
		if peer:
			if self.factory.local:
				#cant get host or port for unix domain sockets
				user.remote_host = LOCALHOST
				user.remote_port = 0
			else:
				user.remote_host = peer.host
				user.remote_port = peer.port

		self.log(sig+" remote host=%s, tunneled=%s" % (user.remote_host, user.tunneled))
		if self.user:
			#already logged in, just check the ID and update what can be updated
			if self.user.uuid != user.uuid:
				msg = "Cannot change UUID after login"
				self.error(sig+" "+msg)
				self.handler.send_authentication_failed(msg)
				return	False
			return	self.user.update(user)

		public_key = user.get_key()
		if not self.check_signature(public_key, salt_sig, self.handler.local_salt):
			self.handler.send_authentication_failed("Invalid signature")
			return	False
		#now we know that the user has the private key that goes with the public key he sent
		key_info = "%s : %s@%s" % (user.name, user.remote_username, user.host)
		if USER_ID!=0 and self.factory.local:
			self.log(sig+" local socket connection, not checking password")
		else:
			if not self.is_allowed_access(user.username, user.auth_username, enc_password, public_key, key_info):
				self.handler.send_authentication_failed("Invalid key and password")
				return	False
		if OSX:
			""" On OSX we need to keep the password to enable ARD with sudo """
			user.password = self.decrypt_password(enc_password)
		#check client is not already connected under a different hostname/ip
		existing = self.controller.get_client(user.uuid, self)
		self.debug(sig+" existing connection=%s" % existing)
		if existing and existing.is_connected():
			assert existing.handler
			self.log(sig+" found existing connection for uuid=%s" % user.uuid)
			counter = existing.handler.pingechocount
			existing.handler.send_ping(RESPONSE_TIMEOUT/2)	#should fire disconnect if no response received
			def check_alive():
				self.slog("counter=%s, new counter=%s" % (counter, existing.handler.pingechocount))
				if counter==existing.handler.pingechocount:
					self.serror("counter unchanged, dropping dead connection!")
					callFromThread(existing.disconnect, message="New connection coming in, current one seems to have timed out")
					callFromThread(self.logged_in_ok, user)
				else:
					self.error(sig+" uuid=%s already connected to %s"  % (user.uuid, existing))
					self.handler.send_authentication_failed("Already connected with this identity")
			callLater(RESPONSE_TIMEOUT/2+1, check_alive)
			return	False
		#All OK: give UI a chance to run before finishing off
		callFromThread(self.logged_in_ok, user)

	def logged_in_ok(self, user):
		config = self.controller.config
		self.add_authenticated_handlers()
		self.controller.add_local_user(user.username, user.uuid, user.preferred_session_type)
		self.sdebug("username=%s, uuid=%s"  % (user.username, user.uuid), user)
		mod = self.controller.config.add_user(user)
		self.user = user
		self.alive_check()
		self.controller.add_client(self)
		if mod and not self.user.avatar_icon_data:
			self.handler.send_request_user_icon(self.user.uuid)
		self.handler.binary_encodings = user.binary_encodings
		self.handler.set_remote_key(self.user.get_key())
		self.handler.set_local_key(self.controller.config.get_key())
		self.handler.send_authentication_success()

		#If enabled and key is provided, grant this user access via ssh:
		if user.ssh_pub_keydata and config.grant_ssh_access:
			add_ssh_public_key(user.username, user.ssh_pub_keydata)

		self.send_tunnel_ports()

	def send_tunnel_ports(self):
		""" send the samba/ipp ports that the client can use for tunnelling """
		port_mapper = self.controller.port_mapper
		config = self.controller.config
		######################################################################
		self.sdebug("tunnel_printer=%s, user.local_ipp_port=%s, FORCE_IPP_TUNNEL=%s" % (config.tunnel_printer, self.user.local_ipp_port, FORCE_IPP_TUNNEL))
		ipp_port = 0
		if config.tunnel_printer and self.user.tunnel_printer:
			""" both sides agree on sharing IPP """
			direct = False and not config.ssh_tunnel and not self.user.tunneled and not FORCE_IPP_TUNNEL
			if direct:
				self.sdebug("using native ipp port directly")
				self.user.local_ipp_port = LOCAL_IPP_PORT			#use local ipp port directly
			else:
				ipp_port = port_mapper.get_free_ipp_tunnel_port()
				self.user.local_ipp_port = ipp_port
		######################################################################
		self.sdebug("tunnel_fs=%s, user.local_samba_port=%s, FORCE_SAMBA_TUNNEL=%s" % (config.tunnel_fs, self.user.local_samba_port, FORCE_SAMBA_TUNNEL))
		samba_port = 0
		if config.tunnel_fs and self.user.tunnel_fs:
			direct = False and not config.ssh_tunnel and not self.user.tunneled and not FORCE_SAMBA_TUNNEL
			if direct:
				self.sdebug("using native samba port directly")
				self.user.local_samba_port = LOCAL_SAMBA_PORT		#use local samba port without tunnel
			else:
				samba_port = port_mapper.get_free_samba_tunnel_port()
				self.user.local_samba_port = samba_port
		#send to client:
		self.handler.send_tunnel_ports(samba_port, ipp_port)


	def stop_request(self, message):
		"""
		"""
		if self.controller.config.clients_can_stop:
			self.slog(None, message)
			self.controller.shutdown(message)
		else:
			self.serror("illegal request to shutdown received from %s" % self.user, message)

	def do_version(self, proto_ver, app_ver, info):
		self.user.app_version = self.handler.check_parse_version(proto_ver, app_ver)
		return	False

	def alive_check(self):
		if self.user and not self.closed:
			self.handler.send_ping()
			callLater(ALIVE_CHECK_DELAY, self.alive_check)

	def kill_session(self, ID):
		self.slog(None, ID)
		session = self.get_session_for_access(ID)
		if not session:
			return	False
		self.controller.kill_session(session)
		return False

	def close_session(self, ID):
		self.slog(None, ID)
		session = self.get_session_for_access(ID)
		if not session:
			return	False
		self.do_close_session(session)
	def do_close_session(self, session):
		self.controller.config.remove_session_by_ID(session.ID)
		self.controller.session_cleanup(session)
		for client in self.controller.clients:
			client.send_remove_session(session)
		return False

	def send_session_to_user(self, ID, user_uuid, password):
		self.sdebug(None, ID, user_uuid, password)
		session = self.controller.config.get_session(ID)
		if not session:
			self.serror("session not found!", ID, user_uuid, password)
			return	False
		if not self.can_access_session(session):
			self.serror("user %s not allowed to access %s!" % (self.user, session), ID, user_uuid, password)
			return	False
		self.controller.send_session_to_user(session, self.user.uuid, user_uuid)
		return	False

	def request_session(self, ID, *args):
		self.slog(None, ID)
		session = self.get_session_for_access(ID, True)
		if not session or not self.user:
			return	False
		self.controller.send_session_to_user(session, self.user.uuid, self.user.uuid)
		return	False

	def disconnect_session(self, ID, *args):
		self.slog(None, ID)
		session = self.get_session_for_access(ID, True)
		if not session or not self.user:
			return	False
		self.controller.disconnect_session(session, self.user.uuid)
		return	False

	def set_session_status(self, ID, status, actor, preload, screen_size, *args):
		"""
		Only a few session status updates are allowed from the client.
		First we ensure that the client is the actor.
		Then we check with display_util.can_client_set_status() to ensure the transition is allowed.

		This was needed for when we did not watch the output from the Xvnc process and relied on the client to tell us
		when it was (dis)connected.
		This code can probably be removed without causing any problems.
		"""
		self.slog(None, ID, status, actor, preload, *args)
		warn_if_missing = status!=Session.STATUS_CLOSED
		session = self.get_session_for_access(ID, check_actor=True, warn_if_missing=warn_if_missing)
		if not session:
			if warn_if_missing:
				self.serror("session not found!", ID, status, actor, preload, *args)
			return	False
		# only IDLE->CONNECTED and back is allowed from client who is actor
		if not self.user or (session.owner!=self.user.uuid and (not session.actor or session.actor!=self.user.uuid)):
			if session.status!=status:
				self.serror("session=%s, actor=%s, owner=%s, user %s is not the actor or owner! " % (session, session.actor, session.owner, self.user), ID, status, actor, preload, *args)
			else:
				self.sdebug("session=%s already in correct state (no longer the owner!)" % session, ID, status, actor, preload, *args)
			return False
		session.screen_size = screen_size
		display_util = self.controller.get_remote_util(session.session_type)
		assert display_util
		if session.status==status or display_util.can_client_set_status(session, self.user.uuid, session.status, status):
			self.controller.update_session_status(session, status, actor=actor)
		else:
			self.serror("%s does not allow session to be updated from client from state %s to %s! " % (display_util, session.status, status), ID, status, actor, preload, *args)
		return False

	def set_host_info(self, *args):
		self.slog("ignored for now...", args)
		return False

	def start_session(self, uuid, session_type, screen_size, opts_str, filenames=None):
		opts = self.handler.parse_opts(opts_str)
		self.sdebug("options=%s" % opts, uuid, session_type, screen_size, opts_str, filenames)
		if uuid.startswith(ServerCommand.CUSTOM_UUID_PREFIX) and self.controller.config.allow_custom_commands:
			#this is a custom command, and is allowed by the server, construct it on the fly:
			cmd = uuid[len(ServerCommand.CUSTOM_UUID_PREFIX):]
			noargs = cmd.split(" ")[0]
			if not cmd.startswith("/"):
				#probably not the best place to be doing this, but all other commands have a full path by the time we get here...
				#so we can't just use a shell for all of them, that would be a waste!
				cmds = cmd.split(" ")
				from winswitch.util.which import which
				wc = which(cmds[0])
				if not is_valid_file(wc):
					self.send_plain_message("Cannot start %s" % noargs, "The command you specified does not exist!")
					return

				if wc:
					self.sdebug("replaced %s with full path %s" % (cmds[0], wc), uuid, session_type, screen_size, opts_str, filenames)
					cmds[0] = wc
					cmd = " ".join(cmds)
			server_command = ServerCommand(ServerCommand.CUSTOM_UUID_PREFIX, name="Custom Command", command=cmd, comment="Custom command, manually set by user", icon_filename=None)
			#try to find an existing template to apply:
			template = self.controller.config.get_server_command_by_command(noargs, True)
			if template is None and noargs.startswith("/"):
				noargs = noargs[noargs.rfind("/")+1:]
				template = self.controller.config.get_server_command_by_command(noargs, True)
			self.sdebug("template(%s)=%s" % (noargs, template), uuid, session_type, screen_size, opts_str, filenames)
			icon = None
			if template:
				server_command.name = template.name
				server_command.comment = template.comment
				server_command.icon_filename = template.icon_filename
				icon = template.get_icon_data()
			from winswitch.util.icon_cache import guess_icon_from_name
			if not icon and server_command.icon_filename:
				icon = guess_icon_from_name(server_command.icon_filename, False, False)
			if not icon:
				icon = guess_icon_from_name(noargs, False, False)
			if not icon and server_command.icon_filename:
				icon = guess_icon_from_name(server_command.icon_filename, False, True)
			server_command.set_icon_data(icon)
		else:
			server_command = self.controller.config.get_command_by_uuid(uuid)
		if not server_command:
			self.serror("no matching command found!", uuid, session_type, screen_size, opts_str)
			return False
		user = self.user
		user_uuid = opts.get("user_uuid")
		if user_uuid is not None:
			user = self.controller.config.get_user_by_uuid(user_uuid) or self.user
		self.slog("command=%s, user=%s" % (server_command, user), uuid, session_type, screen_size, opts_str)
		self.controller.start_session(user, session_type, server_command, screen_size, opts, filenames)
		return False

	def shadow_session(self, ID, ro, shadow_type, *args):
		self.sdebug(None, ID, ro, shadow_type, *args)
		read_only = get_bool(ro)
		session = self.controller.config.get_session(ID)
		if not session:
			self.serror("session not found!", ID, ro)
			return
		options = {}
		if len(args)>1:
			screen_size = args[0]
			options = self.handler.parse_opts(args[1])
		self.controller.shadow_session(session, self.user, read_only, shadow_type, screen_size, options)

	def open_file(self, filename, mode):
		self.slog(None,  filename, mode)
		self.controller.open_file(self.user, filename, mode)

	def do_request_session_icon(self, session_id, *args):
		session = self.controller.config.get_session(session_id)
		if session:
			large = args and len(args)>0 and get_bool(args[0])			#added in 0.9.11
			self.handler.send_session_icon(session, large)
		return False

	def do_request_command_icon(self, uuid, command_type):
		server_commands = self.controller.config.get_command_list_for_type(command_type)
		if not server_commands:
			self.serror("no commands found for type: %s" % command_type, uuid, command_type)
			return	False
		match = None
		for cmd in server_commands:
			if cmd.uuid == uuid:
				match = cmd
				break
		if not match:
			self.serror("command not found" % uuid, uuid, command_type)
			return	False
		icon_data = match.lookup_icon_data()
		if icon_data:
			self.handler.send_command_icon(match)
		#TODO: pass on request to other clients if we haven't found it here?
		return False

	def request_user_icon(self, uuid):
		self.slog(None, uuid)
		user = self.controller.config.get_user_by_uuid(uuid)
		if not user or not user.avatar_icon_data:
			return	False
		self.handler.send_user_icon(user.uuid, user.avatar_icon_data)

	#message sent via local socket
	def do_local_message(self, uuid, title, message, from_uuid):
		client = self.controller.get_client(uuid)
		if client:
			client.send_plain_message(title, message, from_uuid)

	def set_session_command(self, display, command, mimetype):
		self.slog(None, display, command, mimetype)
		session = self.controller.config.get_session_by_display(display)
		if not session:
			self.serror("existing session data not found!", display, command, mimetype)
			return False
		session.command = command
		#clear guessed icons if any
		icon_data = None
		from winswitch.util.icon_cache import guess_icon_from_name
		if mimetype:
			pos = mimetype.rfind("/")
			if pos>0:
				mimetype = mimetype[pos+1:]
			icon_data = guess_icon_from_name("gnome-mime-application-%s" % mimetype, False, False)
			if not icon_data:
				icon_data = guess_icon_from_name(mimetype, False, True)
		if not icon_data and command and not command.startswith("xdg-open"):
			icon_data = guess_icon_from_name(command)
		session.set_default_icon_data(icon_data)
		save_session(session)
		for channel in self.controller.clients:
			channel.send_session(session)
			if icon_data:
				channel.handler.send_session_icon(session, False)
		return False

	def do_message(self, uuid, title, message, *args):
		from_uuid = None
		if len(args)>0:
			from_uuid = args[0]
		if from_uuid and from_uuid!=self.user.uuid:
			self.serror("message from_uuid does not match user's: %s" % self.user.uuid, from_uuid, uuid, title, message)
			return
		client = self.controller.get_client(uuid)
		if client and client.handler:
			self.slog("sending using %s" % client.handler, uuid, title, message)
			client.handler.send_plain_message(uuid, title, message, from_uuid)
		else:
			self.serror("cannot find client or handler for uuid %s" % uuid, uuid, title, message)
		return False

	def noop(self, *args):
		self.slog(None, *args)
		return False

	def set_xmodmap(self, enc_key_data, enc_mod_data=None, *args):
		self.slog(None, "%s bytes" % (len(enc_key_data or [])), "%s bytes" % len(enc_mod_data or []))
		for enc_data, is_modifiers in [(enc_key_data, False), (enc_mod_data, True)]:
			filename = get_xmodmap_filename(self.user.uuid, is_modifiers=is_modifiers)
			xmodmap_data = bindecode(enc_data)
			if not xmodmap_data or xmodmap_data=="None" or len(xmodmap_data)<12:
				xmodmap_data = ""
			self.log("(...) saving xmodmap data (length=%d) to %s" % (len(xmodmap_data), filename))
			if not xmodmap_data:
				delete_if_exists(filename)
				return
			save_binary_file(filename, xmodmap_data)
		#optional fields introduced in 0.12.16:
		if len(args)>=4:
			e_xkbmap_print, e_xkbmap_query, xkbmap_layout, xkbmap_variant = args[:4]
			self.user.xkbmap_print = bindecode(e_xkbmap_print)
			self.user.xkbmap_query = bindecode(e_xkbmap_query)
			self.user.xkbmap_layout = xkbmap_layout
			self.user.xkbmap_variant = xkbmap_variant
		self.controller.set_new_user_keymap(self.user)


	def add_mount_point(self, protocol, namespace, host, port, path, fs_type, auth_mode, username, password, comment, options_str):
		options = self.handler.parse_opts(options_str)
		mp = MountPoint(protocol, namespace, host, port, path, fs_type, auth_mode, username, password, comment, options)
		self.user.mount_points.append(mp)
		self.sdebug("mount_client=%s, is disk type?=%s, mount_points=%s" % (self.controller.config.mount_client, mp.fs_type==MountPoint.DISK, csv_list(self.user.mount_points)))
		self.controller.add_mount_point(self.user, mp, self.close_callbacks)


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

	def do_request_session_sound(self, ID, start, in_or_out, monitor, *args):
		"""
		Client is requesting that we start/stop sending/receiving sound.
		(monitor flag means it wants to use the "clone" sound rather than regular output)
		"""
		session = self.get_session_for_access(ID, True)
		if not session:
			self.serror("session not found / not accessible!", ID, start, in_or_out, monitor)
			return
		if session.shadowed_display:
			self.serror("session is a shadow!", ID, start, in_or_out, monitor)
			return
		codec = VORBIS
		codec_options = {}
		if len(args)>=2:
			codec = args[0]
			codec_options = self.handler.parse_opts(args[1])
		self.do_request_session_sound_for(session, get_bool(start), get_bool(in_or_out), get_bool(monitor), codec, codec_options)

	def do_request_session_sound_for(self, session, start, in_or_out, monitor, codec, codec_options):
		self.sdebug(None, session, start, in_or_out, monitor)
		if start:
			self.start_session_sound(session, in_or_out, monitor, codec, codec_options)
		else:
			self.stop_session_sound(session, in_or_out, monitor)

	def get_pipelines_map(self, session, in_or_out):
		if in_or_out:
			return	session.client_soundin_pipelines
		else:
			return	session.client_soundout_pipelines

	def stop_session_sound(self, session, in_or_out, monitor, log_missing=True):
		"""
		Find the pipeline and kill it.
		"""
		if not self.user:
			return
		#always tell the client to disconnect (invert in_or_out):
		self.handler.receive_session_sound(session.ID, False, not in_or_out, monitor, -1)
		#now stop our end:
		pipelines = self.get_pipelines_map(session, in_or_out)
		pipeline_pid = pipelines.get(self.user.uuid)
		if pipeline_pid:
			#stop the pipe:
			log_filename = get_server_session_sound_log_filename(session.display, self.user.uuid, False)
			self.sdebug("pipeline_pid=%s, log_filename=%s" % (pipeline_pid, log_filename), session, in_or_out, monitor)
			kill_daemon(pipeline_pid, log_filename)			#kill daemon by pid after checking pid against logfile
			pipelines[self.user.uuid] = None
		elif log_missing:
			self.slog("sound pipeline does not exist for user %s: nothing to stop!" % self.user.uuid, session, in_or_out, monitor)

	def start_session_sound(self, session, in_or_out, monitor, codec, codec_options):
		"""
		Find the pipeline (create it if necessary) and tell the client to connect when ready.
		"""
		if not self.user:
			self.serror("user is missing!?", session, in_or_out, monitor, codec, codec_options)
			return
		if codec not in self.controller.config.gstaudio_codecs:
			self.serror("codec not supported!", session, in_or_out, monitor, codec, codec_options)
			return
		pipelines = self.get_pipelines_map(session, in_or_out)
		pipeline_pid = pipelines.get(self.user.uuid)

		log_filename = get_server_session_sound_log_filename(session.display, self.user.uuid, in_or_out)
		if pipeline_pid:
			if is_daemon_alive(pipeline_pid, log_filename):
				self.slog("daemon %s is still alive - stopping it!" % pipeline_pid, session, in_or_out, monitor, codec, codec_options)
				self.stop_session_sound(session, in_or_out, monitor)
			else:
				self.slog("daemon %s was gone - removing it" % pipeline_pid, session, in_or_out, monitor, codec, codec_options)
				pipelines[self.user.uuid] = None		#clear it, it's already gone

		#prevent loops:
		if monitor:
			""" ensure that there aren't any sound pipes running (could cause a loop) """
			for io in [True,False]:
				for mon in [True,False]:
					self.stop_session_sound(session, io, mon)
		else:
			""" ensure that there aren't any monitor sound pipes running (could cause a loop) """
			for io in [True,False]:
				self.stop_session_sound(session, io, True)


		if in_or_out:
			if not self.controller.config.tunnel_source:
				self.slog("server configuration does not allow sound input" % pipeline_pid, session, in_or_out, monitor, codec, codec_options)
				return
			#receiving sound: always use the same "import plugin" (the monitor option is only really relevant to the client)
			gst_mod = session.gst_import_plugin
			gst_mod_opts = session.gst_import_plugin_options
		else:
			if not self.controller.config.tunnel_sink:
				self.slog("server configuration does not allow sound output" % pipeline_pid, session, in_or_out, monitor, codec, codec_options)
				return
			if monitor:
				gst_mod = session.gst_clone_plugin
				gst_mod_opts = session.gst_clone_plugin_options
			else:
				gst_mod = session.gst_export_plugin
				gst_mod_opts = session.gst_export_plugin_options

		""" Try to make sure the socket is not accessible on local networks unless needed """
		if self.user.tunneled or self.user.local:
			host = LOCALHOST
		else:
			#FIXME: IPv6
			host = "0.0.0.0"	#let's not try to figure out which interface the user will connect from
		port = get_port_mapper().get_sound_tunnel_port()

		self.slog("gst_mod=%s, options=%s, user=%s target=%s:%s" % (gst_mod, gst_mod_opts, self.user, host, port), session, in_or_out, monitor, codec, codec_options)
		delete_if_exists(log_filename)
		def gst_process_started(proc):
			""" Record the pid, ensure we stop the process when the session closes and then send the OK to client """
			pid = proc.pid
			self.slog("session=%s, pid=%s" % (session, pid), proc)
			pipelines[self.user.uuid] = pid
			save_session(session)
			if pid<0:
				self.serror("sound failed to start", pid)
				return
			def stop_session_sound():
				self.stop_session_sound(session, in_or_out, monitor)
			def tcp_port_ready():
				self.sdebug("port %s ready for session %s" % (port, session))
				session.add_status_update_callback(None, Session.STATUS_CLOSED, stop_session_sound, clear_it=True, timeout=None)
				#tell client to connect (invert in_or_out):
				def tell_client_sound_ready():
					decoder_options = get_decoder_options(codec_options)
					self.sdebug("decoder_options=%s" % decoder_options)
					self.handler.receive_session_sound(session.ID, True, not in_or_out, monitor, port, codec, decoder_options)
				callLater(0, tell_client_sound_ready)
			def tcp_port_failed():
				self.slog()
				stop_session_sound()
			test_host = host
			if test_host=="0.0.0.0":
				test_host = LOCALHOST
			wait_for_socket(test_host, port, max_wait=6, success_callback=tcp_port_ready, error_callback=tcp_port_failed)
		def gst_process_ended(proc):
			self.slog(None, proc)
			pipelines[self.user.uuid] = None
			#tell client to stop (invert in_or_out):
			self.handler.receive_session_sound(session.ID, False, not in_or_out, monitor, port)
		try:
			name = "%s on %s" % (self.user.name, self.user.host)
			if not session.env:
				""" re-populate session.env if needed (reloaded session from disk) """
				server_util = self.controller.get_remote_util(session.session_type)
				assert server_util
				session.env = session.get_env()
			env = get_session_sound_env(session, True, name)
			start_gst_sound_pipe(True, gst_process_started, gst_process_ended, log_filename, env, name, in_or_out, "server", host, port, gst_mod, gst_mod_opts, codec, codec_options)
		except Exception, e:
			self.serr("failed to create sound pipeline", e, session, in_or_out, monitor, codec, codec_options)
			return


	def local_xdg_open(self, session_id, argument):
		""" local socket message received from our xdg-open script """
		self.slog(None, session_id, argument)
		session = self.controller.config.get_session(session_id)
		if not session:
			self.serr("cannot find session", session_id, argument)
			return
		def open_existing_session():
			cmd = [XDG_OPEN_COMMAND, argument]
			exec_nopipe(cmd, env=session.get_env())
		client = self.controller.get_client(session.actor)
		if not client or not client.user:
			self.serr("cannot find client or user for session %s" % session, session_id, argument)
			open_existing_session()
			return
		isurl = argument.startswith("http")
		if isurl:
			mode = client.user.open_urls
		else:
			mode = client.user.open_files
		self.slog("opening as url=%s, mode=%s" % (isurl, mode), session_id, argument)
		if mode==OPEN_EXISTING_SESSION:
			open_existing_session()
		elif mode==OPEN_NEW_SESSION:
			session_file = get_session_filename(session.display, session.user, USER_ID==0)
			mime_open = os.path.join(WINSWITCH_LIBEXEC_DIR, "mime_open")
			cmd = "%s --session-file=%s %s" % (mime_open, session_file, argument)
			server_command = ServerCommand(ServerCommand.CUSTOM_UUID_PREFIX, name="xdg-open", command=cmd, comment="Opening file or URL", icon_filename=None)
			self.controller.start_session(client.user, session.session_type, server_command, session.screen_size, {})
		elif mode==OPEN_LOCALLY:
			client.handler.send_xdg_open(argument)
		else:
			self.serr("unknown mode", session_id, mode, argument)

	def local_close_request(self, uuid, ID):
		"""
		This is used by local clients to request remote clients to close a session.
		(used by the ssh wrapper script to terminate an ssh session cleanly on win32/Xming with plink)
		(also used by delayed_start to notify us when a session terminates)
		"""
		self.slog(None, uuid, ID)
		if uuid:
			client = self.controller.get_client(uuid)
			self.sdebug("client=%s" % client, uuid, ID)
			if client:
				client.handler.send_close_session(ID)
		session = self.controller.config.get_session(ID)
		if session:
			self.do_close_session(session)


	def receive_file_data(self, filetransfer_id, basename, position, data, digest, eof_str, *args):
		eof = get_bool(eof_str)
		if not self.controller.config.allow_file_transfers:
			self.cancel_file_transfer(filetransfer_id)
			raise Exception("this server does not accept file transfers!")
		fc = self.file_copy.get(filetransfer_id)
		if fc is None:
			# this is a new upload:
			if int(position)>0:
				self.cancel_file_transfer(filetransfer_id)
				raise Exception("cannot start file transfer at a position other than zero!")
			dl_dir = os.path.expanduser(self.controller.config.download_directory)
			fc = FileReceiver(dl_dir, self.handler, filetransfer_id, basename)
			self.file_copy[filetransfer_id] = fc
		self.sdebug(None, filetransfer_id, basename, position, "%s bytes" % len(data), digest, eof_str, *args)
		filename = fc.receive(position, data, digest, eof)
		if eof and filename:
			self.open_file(filename, "view")

	def cancel_file_transfer(self, filetransfer_id, *args):
		fc = self.file_copy.get(filetransfer_id)
		if fc:
			self.slog(None, filetransfer_id, *args)
			fc.cancel()
			del self.file_copy[filetransfer_id]
		else:
			self.serror("file copy not found!", filetransfer_id, *args)


class FileReceiver:

	def __init__(self, target_dir, handler, filetransfer_id, basename):
		Logger(self)
		self.target_dir = target_dir
		self.handler = handler
		self.filetransfer_id = filetransfer_id
		self.basename = basename
		#self.fd = open(self.temp_file, "wb")
		self.fd = tempfile.NamedTemporaryFile(mode='wb', bufsize=4096, suffix=basename, prefix='tmp', dir=target_dir, delete=False)
		self.md5 = hashlib.md5()
		self.digest = None
		self.check_ack = None

	def receive(self, position, enc_data, digest, eof):
		data = bindecode(enc_data)
		self.md5.update(data)
		self.digest = self.md5.hexdigest()
		if digest!=self.digest:
			self.serror("digest mismatch: %s" % self.digest, position, "%s bytes" % len(data), digest, eof)
			self.handler.send(CANCEL_FILE_TRANSFER, self.filetransfer_id)
			self.close()
		self.fd.write(data)
		self.handler.send(ACK_FILE_DATA, [self.filetransfer_id, digest])
		if eof:
			self.slog("end of file - closing", position, "%s bytes" % len(data), digest, eof)
			filename = os.path.join(self.target_dir, self.basename)
			os.rename(self.fd.name, filename)
			self.close()
			return	filename
		else:
			def check_progress():
				self.sdebug("digest: %s vs %s" % (self.digest, digest))
				if self.fd is not None and self.digest==digest:
					self.serror("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)
			return	None

	def close(self):
		if self.fd is None:
			return
		self.fd.close()
		self.fd = None

	def cancel(self):
		if self.fd is None:
			self.serror("file descriptor is unset!")
			return
		filename = self.fd.name
		self.close()
		os.unlink(filename)


class ClientChannelFactory(twisted_protocol.ClientFactory):
	""" Twisted factory for the client channel """
	# the class of the protocol to build when new connection is made
	protocol = WinSwitchClientChannel

	def __init__ (self, controller, local=False):
		Logger(self)
		self.sdebug(None, controller, local)
		self.controller = controller
		self.local = local
		self.numProtocols = 0

	def __str__(self):
		return	"ClientChannelFactory(%s,%s)" % (self.controller, self.local)

	def clientConnectionLost(self, connector, reason):
		self.serror(None, connector, reason)

	def clientConnectionFailed(self, connector, reason):
		self.error(None, connector, reason)
