#!/usr/bin/python3
# coding=utf-8
##################################################################
#              ____     _     _ __  __                 
#             / __/__  (_)___(_) /_/ /  ___  ___  ___ _
#            _\ \/ _ \/ / __/ / __/ /__/ _ \/ _ \/ _ `/
#           /___/ .__/_/_/ /_/\__/____/\___/_//_/\_, / 
#              /_/                              /___/  
# 
# Copyright (c) 2025 Chongqing Spiritlong Technology Co., Ltd.
# All rights reserved.
# @author	arthuryang
# @brief	SSH工具
##################################################################

# session名称格式：用户名@HOSTNAME；当HOSTNAME是一个域名（包含了.）时，则session名称为此域名
# session作为一个字典，必须包含HostName,UserName,PortNumber三个key
# HOST文件是一个xlsx文件，有HOSTS和ROOT两个sheet。HOST里面至少包含HOSTNAME、ADDRESS、PORT、PASSWORD四个列，包含了需要管理的主机；ROOT里面至少包含HOSTNAME、PUBLIC、COMMENT三个列（COMMENT指示了ROOT密钥所有者)
# 	HOST文件仅供管理员使用
# session文件是一个xlsx文件，名为HOST的sheet中，至少应该包含USER、HOSTNAME、ADDRESS、PORT四个列。PUBLIC和PRIVATE是可选的列（没有指定则会自动生成）
# 密钥文件总是放在USER_SSH_BASE下的OPENSSH/PPK里面
# （仅限Windows）如果没有指定session文件，则使用PuTTY在注册表中寻找session。Linux下必须指定session文件来使用


import	os
import	base64
import	secrets
import	string

# 仅在windows下可用
if os.name=='nt':
	import	winreg

import	hmac
import	hashlib
import	paramiko
import	SpiritLong_utility
import	SpiritLong_excel
from	nacl.signing	import	SigningKey	# pip install pynacl
from	nacl.encoding	import	Base64Encoder	as nacl_Base64Encoder
from	nacl.encoding	import	RawEncoder	as nacl_RawEncoder

# PuTTY注册表sessions路径
PUTTY_REGISTRY_PATH	= r"SOFTWARE\SimonTatham\PuTTY\Sessions"

# SSH目录位置，其中OPENSSH和PPK目录下分别是openSSH的密钥和PuTTY密钥，密钥名为session名称
if os.name=='nt':
	# Windows下应该在 用户/OneDrive/文档/SSH
	USER_SSH_BASE	= os.path.join(os.getenv('USERPROFILE'), 'OneDrive', '文档', 'SSH')
else:
	# Linux下应该在 ~/.ssh
	USER_SSH_BASE	= os.path.join(os.getenv('HOME'), '.ssh')

# 需要读取/保存的设置
PUTTY_CONFIG	= [
	"HostName",		# 主机地址
	"PortNumber",		# 端口
	"UserName",		# 用户名
	"PublicKeyFile",	# 密钥文件（ppk文件）位置
]

# PuTTY注册表中的全部配置
PUTTY_CONFIG_DEFAULT	= {
	'HostName'			: '',
	'PortNumber'			: 22,
	'UserName'			: 'root',
	'PublicKeyFile'			: '',			# ppk文件位置
	
	# 字体
	'Font'				: 'Fixedsys',
	'FontHeight'			: 10,
	'FontIsBold'			: 0,			# 字体样式：是否加粗
	'FontQuality'			: 3,			# 字体质量 3->ClearType
	'FontCharSet'			: 134,			# 字体编码

	# Alt+Enter全屏
	'FullScreenOnAltEnter'		: 1,

	# 初始分辨率，单位为字符
	'TermWidth'			: 132,
	'TermHeight'			: 80,

	'WinTitle'			: 'arthuryang@PROGRAM',
	'Protocol'			: 'ssh',
	'BlinkCur'			: 1,
		
	# 行缓存数量	
	'ScrollbackLines'		: 999999,
	# X11转发使能才可以使用图形界面
	'X11Forward'			: 1,

	# TCP选项
	'PingInterval'			: 0,	# 发送TCPKeepalives的间隔：分钟数部分
	'PingIntervalSecs'		: 30,	# 发送TCPKeepalives的间隔：秒数部分
	'TCPNoDelay'			: 1,	# TCP_NODELAY
	'TCPKeepalives'			: 1,	# SO_KEEPALIVE

	'NoApplicationKeys'		: 1,	# 禁止应用程序小键盘，这可以在vi中正常使用小键盘
	'NoRemoteWinTitle'		: 1,	# 禁止远程修改标题

	# 以下是PuTTY默认设置
	'Present'			: 1,
	'LogFileName'			: 'putty.log',
	'LogType'			: 0,
	'LogFileClash'			: 4294967295,
	'LogFlush'			: 1,
	'LogHeader'			: 1,
	'SSHLogOmitPasswords'		: 1,
	'SSHLogOmitData'		: 0,
	'CloseOnExit'			: 1,
	'WarnOnClose'			: 1,	
	'TerminalType'			: 'xterm',
	'TerminalSpeed'			: '38400,38400',
	'TerminalModes'			: 'CS7=A,CS8=A,DISCARD=A,DSUSP=A,ECHO=A,ECHOCTL=A,ECHOE=A,ECHOK=A,ECHOKE=A,ECHONL=A,EOF=A,EOL=A,EOL2=A,ERASE=A,FLUSH=A,ICANON=A,ICRNL=A,IEXTEN=A,IGNCR=A,IGNPAR=A,IMAXBEL=A,INLCR=A,INPCK=A,INTR=A,ISIG=A,ISTRIP=A,IUCLC=A,IUTF8=A,IXANY=A,IXOFF=A,IXON=A,KILL=A,LNEXT=A,NOFLSH=A,OCRNL=A,OLCUC=A,ONLCR=A,ONLRET=A,ONOCR=A,OPOST=A,PARENB=A,PARMRK=A,PARODD=A,PENDIN=A,QUIT=A,REPRINT=A,START=A,STATUS=A,STOP=A,SUSP=A,SWTCH=A,TOSTOP=A,WERASE=A,XCASE=A',
	'AddressFamily'			: 0,
	'ProxyExcludeList'		: '',
	'ProxyDNS'			: 1,
	'ProxyLocalhost'		: 0,
	'ProxyMethod'			: 0,
	'ProxyHost'			: 'proxy',
	'ProxyPort'			: 80,
	'ProxyUsername'			: '',
	'ProxyPassword'			: '',
	'ProxyTelnetCommand'		: 'connect %host %port\n',
	'ProxyLogToTerm'		: 1,
	'Environment'			: '',
	'UserNameFromEnvironment	'	: 0,
	'LocalUserName'			: '',
	'NoPTY'				: 0,
	'Compression'			: 0,
	'TryAgent'			: 1,
	'AgentFwd'			: 0,
	'GssapiFwd'			: 0,
	'ChangeUsername'		: 0,
	'Cipher'			: 'aes,chacha20,3des,WARN,des,blowfish,arcfour',
	'KEX'				: 'ecdh,dh-gex-sha1,dh-group14-sha1,rsa,WARN,dh-group1-sha1',
	'HostKey'			: 'ed448,ed25519,ecdsa,rsa,dsa,WARN',
	'PreferKnownHostKeys'		: 1,
	'RekeyTime'			: 60,
	'GssapiRekey'			: 2,
	'RekeyBytes'			: '1G',
	'SshNoAuth'			: 0,
	'SshNoTrivialAuth'		: 0,
	'SshBanner'			: 1,
	'AuthTIS'			: 0,
	'AuthKI'			: 1,
	'AuthGSSAPI'			: 1,
	'AuthGSSAPIKEX'			: 1,
	'GSSLibs'			: 'gssapi32,sspi,custom',
	'GSSCustom'			: '',
	'SshNoShell'			: 0,
	'SshProt'			: 3,
	'LogHost'			: '',
	'SSH2DES'			: 0,
	'RemoteCommand'			: '',
	'RFCEnviron'			: 0,
	'PassiveTelnet'			: 0,
	'BackspaceIsDelete'		: 1,
	'RXVTHomeEnd'			: 0,
	'LinuxFunctionKeys'		: 0,
	'NoApplicationCursors'		: 0,
	'NoMouseReporting'		: 0,
	'NoRemoteResize'		: 0,
	'NoAltScreen'			: 0,
	'NoRemoteClearScroll'		: 0,
	'RemoteQTitleAction'		: 1,
	'NoDBackspace'			: 0,
	'NoRemoteCharset'		: 0,
	'ApplicationCursorKeys'		: 0,
	'ApplicationKeypad'		: 1,
	'NetHackKeypad'			: 0,
	'AltF4'				: 1,
	'AltSpace'			: 0,
	'AltOnly'			: 0,
	'ComposeKey'			: 0,
	'CtrlAltKeys'			: 1,
	'TelnetKey'			: 0,
	'TelnetRet'			: 1,
	'LocalEcho'			: 2,
	'LocalEdit'			: 2,
	'Answerback'			: 'PuTTY',
	'AlwaysOnTop'			: 0,
	'HideMousePtr'			: 0,
	'SunkenEdge'			: 0,
	'WindowBorder'			: 1,
	'CurType'			: 0,
	'Beep'				: 1,
	'BeepInd'			: 0,
	'BellWaveFile'			: '',
	'BellOverload'			: 1,
	'BellOverloadN'			: 5,
	'BellOverloadT'			: 2000,
	'BellOverloadS'			: 5000,
	'DECOriginMode'			: 0,
	'AutoWrapMode'			: 1,
	'LFImpliesCR'			: 0,
	'CRImpliesLF'			: 0,
	'DisableArabicShaping'		: 0,
	'DisableBidi'			: 0,
	'WinNameAlways'			: 1,
	'FontVTMode'			: 4,
	'UseSystemColours'		: 0,
	'TryPalette'			: 0,
	'ANSIColour'			: 1,
	'Xterm256Colour'		: 1,
	'TrueColour'			: 1,
	'BoldAsColour'			: 1,
	'Colour0'			: '187,187,187',
	'Colour1'			: '255,255,255',
	'Colour2'			: '0,0,0',
	'Colour3'			: '85,85,85',
	'Colour4'			: '0,0,0',
	'Colour5'			: '0,255,0',
	'Colour6'			: '0,0,0',
	'Colour7'			: '85,85,85',
	'Colour8'			: '187,0,0',
	'Colour9'			: '255,85,85',
	'Colour10'			: '0,187,0',
	'Colour11'			: '85,255,85',
	'Colour12'			: '187,187,0',
	'Colour13'			: '255,255,85',
	'Colour14'			: '0,0,187',
	'Colour15'			: '85,85,255',
	'Colour16'			: '187,0,187',
	'Colour17'			: '255,85,255',
	'Colour18'			: '0,187,187',
	'Colour19'			: '85,255,255',
	'Colour20'			: '187,187,187',
	'Colour21'			: '255,255,255',
	'RawCNP'			: 0,
	'UTF8linedraw'			: 0,
	'PasteRTF'			: 0,
	'MouseIsXterm'			: 0,
	'RectSelect'			: 0,
	'PasteControls'			: 0,
	'MouseOverride'			: 1,
	'Wordness0'			: '0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0',
	'Wordness32'			: '0,1,2,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1',
	'Wordness64'			: '1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,2',
	'Wordness96'			: '1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1',
	'Wordness128'			: '1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1',
	'Wordness160'			: '1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1',
	'Wordness192'			: '2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2',
	'Wordness224'			: '2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2',
	'MouseAutocopy'			: 1,
	'MousePaste'			: 'explicit',
	'CtrlShiftIns'			: 'explicit',
	'CtrlShiftCV'			: 'none',
	'LineCodePage'			: '',
	'CJKAmbigWide'			: 0,
	'UTF8Override'			: 1,
	'Printer'			: '',
	'CapsLockCyr'			: 0,
	'ScrollBar'			: 1,
	'ScrollBarFullScreen'		: 0,
	'ScrollOnKey'			: 0,
	'ScrollOnDisp'			: 1,
	'EraseToScrollback'		: 1,
	'LockSize'			: 0,
	'BCE'				: 1,
	'BlinkText'			: 0,
	'X11Display'			: '',
	'X11AuthType'			: 1,
	'X11AuthFile'			: '',
	'LocalPortAcceptAll'		: 0,
	'RemotePortAcceptAll'		: 0,
	'PortForwardings'		: '',
	'BugIgnore1'			: 0,
	'BugPlainPW1'			: 0,
	'BugRSA1'			: 0,
	'BugIgnore2'			: 0,
	'BugHMAC2'			: 0,
	'BugDeriveKey2'			: 0,
	'BugRSAPad2'			: 0,
	'BugPKSessID2'			: 0,
	'BugRekey2'			: 0,
	'BugMaxPkt2'			: 0,
	'BugOldGex2'			: 0,
	'BugWinadj'			: 0,
	'BugChanReq'			: 0,
	'StampUtmp'			: 1,
	'LoginShell'			: 1,
	'ScrollbarOnLeft'		: 0,
	'BoldFont'			: '',
	'BoldFontIsBold'		: 0,
	'BoldFontCharSet'		: 0,
	'BoldFontHeight'		: 0,
	'WideFont'			: '',
	'WideFontIsBold'		: 0,
	'WideFontCharSet'		: 0,
	'WideFontHeight'		: 0,
	'WideBoldFont'			: '',
	'WideBoldFontIsBold'		: 0,
	'WideBoldFontCharSet'		: 0,
	'WideBoldFontHeight'		: 0,
	'ShadowBold'			: 0,
	'ShadowBoldOffset'		: 1,
	'SerialLine'			: 'COM1',
	'SerialSpeed'			: 115200,
	'SerialDataBits'		: 8,
	'SerialStopHalfbits'		: 2,
	'SerialParity'			: 0,
	'SerialFlowControl'		: 1,
	'WindowClass'			: '',
	'ConnectionSharing'		: 0,
	'ConnectionSharingUpstream'	: 1,
	'ConnectionSharingDownstream'	: 1,
	'SSHManualHostKeys'		: '',
	'SUPDUPLocation'		: 'The Internet',
	'SUPDUPCharset'			: 0,
	'SUPDUPMoreProcessing'		: 0,
	'SUPDUPScrolling'		: 0,
}

######################################## 内部工具 ########################################

## 在数据前面增加4字节长度，注意长度是大端。在SSH处理过程中使用。
def _add_header_big_Endian_bytes(data):
	if isinstance(data, str):
		# 字符串需要先转换到bytes
		data	= data.encode('ascii')
	return len(data).to_bytes(4, 'big')+data
	
## 从putty的ppk文件提取出密钥各部分信息
# 返回{key_type, public_string， private_string, comment}，注意private_string是私钥base64编码，public_string是算法名称+公钥两个部分拼起来后的base64编码
def _parse_ppk_file(ppk_file_path):
	items	= {}

	try:
		with open(ppk_file_path, 'r', encoding='utf-8') as f:
			lines	= f.readlines()
			i	= 0
			while i<len(lines):
				n	= 1
				try:
					title, value	= lines[i].split(':', maxsplit=1)
					value	= value.strip()
					if title.startswith('PuTTY-User-Key-File'):
						# key的算法类型
						title	= 'key_type'
					if title=='Public-Lines':
						title	= 'public_string'
						n	= int(value)+1
						key_value_lines	= [v.strip() for v in lines[(i+1):(i+n)]]
						value	= ''.join(key_value_lines)
					elif title=='Private-Lines':
						title	= 'private_string'
						n	= int(value)+1
						key_value_lines	= [v.strip() for v in lines[(i+1):(i+n)]]
						value	= ''.join(key_value_lines)
					elif title=='Comment':
						title	= 'comment'
						value	= value.strip()
					items[title]	= value
				except:
					title	= None
					value	= None
				i	+= n;			
		return items
	except Exception as e:
		print(e)
		return None

## 从OPENSSH私钥文件提取密钥
# 返回
def _parse_key_file(key_file_path):
	key_string	= ''
	try:
		with open(key_file_path, 'r', encoding='utf-8') as f:
			key_content	= 0
			while key_content<2:
				line	= f.readline()
				if "END OPENSSH PRIVATE KEY" in line:
					key_content	= 2
				if key_content==1:
					key_string	+= line.strip()
				if "BEGIN OPENSSH PRIVATE KEY" in line:
					key_content	= 1
	except Exception as e:
		print(f"Failed to read key file: {key_file_path}")
		print(e)
		return None
	
	# 得到的密钥字符串是base64编码了的
	key_bytes	= base64.b64decode(key_string)

	# 分解成多个部分
	head_bytes	= b'openssh-key-v1\0'
	offset		= 0
	parts		= []
	while offset<len(key_bytes):
		if offset==0:
			# 开头的OPENSSH密钥标识
			head	= key_bytes[:len(head_bytes)]
			if head!=head_bytes:
				print("Failed: header part wrong")
				return None 
			parts.append(head)
			offset	+= len(head)
			continue

		if len(parts)==4:
			# 接下来是4字节长度
			length	= 4
		else:
			# 其他部分都有开头的4字节
			length	= int.from_bytes(key_bytes[offset:offset+4], 'big')
			offset	+= 4
		
		# 确保不越界
		length	= min(len(key_bytes)-offset, length)

		# 获取数据
		parts.append(key_bytes[offset:offset+length])
		offset	+= length
	
	# 校验各部分
	if len(parts)!=7:
		print(f"Failed: There are {len(parts)} parts instead of 7")
		return None
	
	if parts[1]!=b'none' or parts[2]!=b'none' or parts[3]!=b'':
		print(f"Failed: cipher name/KDF name/KDF options should be none/''")
		print(parts[1], parts[2], parts[3])
		return None

	if int.from_bytes(parts[4], 'big')!=1:
		print('Failed: key n>1')
		return None

	# 公钥已经是类型+公钥了
	public_bytes		= parts[5]
	# 公钥数据
	public_key_bytes	= public_bytes[(int.from_bytes(public_bytes[:4], 'big')+4+4):]
	# 密钥类型
	key_type		= parts[5][4:4+int.from_bytes(public_bytes[:4])]

	# 私钥内包含了6个部分：校验和1、校验和2、算法、公钥、私钥+公钥、注释，每个部分前面有4字节big endian长度
	private_bytes		= parts[6]
	offset	= 0
	parts	= []
	for i in range(6):
		length	= int.from_bytes(private_bytes[offset:offset+4])
		offset	+= 4
		
		# 确保不越界
		length	= min(len(key_bytes)-offset, length)

		parts.append(private_bytes[offset:offset+length])
		offset	+= length

	for p in parts:
		print(p)

	if len(parts)<5 or public_key_bytes!=parts[3]:
		print("Failed: private key error")
		return None
	
	private_key_bytes	= parts[4][:len(parts[4])-len(public_key_bytes)]
	comment		= parts[5]

	return {
		'public'		: public_key_bytes,
		'private'		: private_key_bytes,
		'key_type'		: key_type.decode('ascii'),
		'public_string'		: base64.b64encode(public_bytes).decode('ascii'),
		'private_string'	: base64.b64encode(_add_header_big_Endian_bytes(private_key_bytes)).decode('ascii'),
		'comment'		: comment,
	}

## 连接到远程主机，返回连接对象
def _connect_session(session_name, base=USER_SSH_BASE, password=None, OPENSSH_file=None, session_file=None):
	# 获取sessions
	sessions	= get_sessions(session_file)
	if not sessions:
		print("无法读取sessions")
		return None
	if session_name not in sessions:
		print(f'找不到名为{session_name}的session')
		return None
	s		= sessions[session_name]
	ssh		= paramiko.SSHClient()

	# 加载已知HOSTS
	ssh.load_system_host_keys()
		
	# 自动接受服务器指纹
	ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())

	if password:
		# 密码登录
		try:
			ssh.connect(
				hostname	= s['HostName'],
				username	= s['UserName'],
				password	= password,
				port		= s['PortNumber'],
				timeout		= 3,
			)
		except Exception as e:
			print('连接失败', e)
			ssh	= None
	else:
		# 密钥登录
		if not OPENSSH_file:
			# 未指定密钥文件则在默认位置寻找
			OPENSSH_file	= os.path.join(base, 'OPENSSH', f'{session_name}.key')
		try:
			ssh.connect(
				hostname	= s['HostName'],
				username	= s['UserName'],
				pkey		= paramiko.Ed25519Key.from_private_key_file(OPENSSH_file),
				port		= s['PortNumber'],
				timeout		= 3,
			)
		except Exception as e:
			print('连接失败', e)
			ssh	= None

	return ssh

######################################## 导出函数 ########################################

## 生成随机密钥
#	algorithm	密钥算法，默认为ed25519
# 返回{private,public,private_string,public_string}
# 	private和public分别对应于私钥和公钥的二进制字节数组
# 	private_string是private加上大端字节数之后的base64编码字符串
#	public_string是算法名称+大端字节数+public这三者的base64编码字符串，
# 	其中_string为二进制密钥对应的字符串，
def generate_random_key(algorithm='ssh-ed25519'):
	# 生成ed25519密钥
	key	= SigningKey.generate()
	verify	= key.verify_key

	# 生成密钥bytes
	private_key	= key.encode(nacl_RawEncoder)
	public_key	= verify.encode(nacl_RawEncoder)

	return {
		# 原始密钥
		'private'	: private_key,
		'public'	: public_key,

		# 密钥base64编码
		'private_string'	: base64.b64encode(_add_header_big_Endian_bytes(private_key)).decode('ascii'),
		# 算法名称+密钥，然后base64编码
		'public_string'		: base64.b64encode(_add_header_big_Endian_bytes(algorithm)+_add_header_big_Endian_bytes(public_key)).decode('ascii'),
	}

## 根据公钥和私钥生成ppk文件
#	filename	ppk文件名，含路径
#	public_key	公钥，带有长度的base64字符串（含算法名称）
#	private_key	私钥，带有长度的base64字符串
def generate_ppk_file(filename, public_key, private_key, comment):
	# 将字符串换成字节数组
	public_key	= public_key.encode('ascii')
	private_key	= private_key.encode('ascii')

	# 公钥分行
	public_lines	= [public_key[i:i+64].decode('ascii')+'\n' for i in range(0, len(public_key), 64)]

	# 私钥分行
	private_lines	= [private_key[i:i+64].decode('ascii')+'\n' for i in range(0, len(private_key), 64)]

	# 计算MAC，注意私钥和公钥要base64解码
	data	= _add_header_big_Endian_bytes('ssh-ed25519')			+\
		  _add_header_big_Endian_bytes('none')				+\
		  _add_header_big_Endian_bytes(comment)				+\
		  _add_header_big_Endian_bytes(base64.b64decode(public_key))	+\
		  _add_header_big_Endian_bytes(base64.b64decode(private_key))
	MAC	= hmac.new(b'', data, hashlib.sha256).hexdigest()

	with open(filename, 'w', encoding='utf-8') as f:
		f.write("PuTTY-User-Key-File-3: ssh-ed25519\n")
		f.write("Encryption: none\n")
		f.write(f"Comment: {comment}\n")
		f.write(f"Public-Lines: {len(public_lines)}\n")
		f.writelines(public_lines)
		f.write(f"Private-Lines: {len(private_lines)}\n")
		f.writelines(private_lines)
		f.write(f"Private-MAC: {MAC}\n")

## 生成OPEN_SSH私钥文件（仅支持ed25519)
#	filename	ppk文件名，含路径
#	public_key	公钥，带有长度的base64字符串（含算法名称）
#	private_key	私钥，带有长度的base64字符串
def generate_OPENSSH_file(filename, public_key, private_key, comment=''): 
	# OpenSSH密钥格式 ed25519 private v1密钥格式，参考：https://sshref.dev/
	# 0.0 "openssh-key-v1" string plus terminating nullbyte (15 bytes)
	# 1.0 uint32 allocator for 1.0.0 (4 bytes)
	#     1.0.0 cipher name string (ASCII bytes)
	# 2.0 uint32 allocator for 2.0.0 (4 bytes)
	#     2.0.0 KDF name string (ASCII bytes)
	# 3.0 uint32 allocator for KDF options (3.0.0 to 3.0.1) (4 bytes) (ALWAYS 0 for unencrypted keys, so no following substructure)
	# 4.0 uint32 counter for # of keys (4 bytes)
	#     4.0.0 uint32 allocator for public key #n (4.0.0.0 to 4.0.0.1) (4 bytes)
	#         4.0.0.0 uint32 allocator for 4.0.0.0.0 (4 bytes)
	#             4.0.0.0.0 public key #n keytype string (ASCII bytes)
	#         4.0.0.1 uint32 allocator for 4.0.0.1.0 (4 bytes)
	#             4.0.0.1.0 public key #n payload (bytes)
	#     4.0.1 uint32 allocator for private key structure #n (4.0.1.0 to 4.0.1.5) (4 bytes)
	#         4.0.1.0 uint32 decryption "checksum" #1 (should match 4.0.1.1) (4 bytes)
	#         4.0.1.1 uint32 decryption "checksum" #2 (should match 4.0.1.0) (4 bytes)
	#         4.0.1.2 Copy of 4.0.0.0; allocator for 4.0.1.2.0 (4 bytes)
	#             4.0.1.2.0 Copy of 4.0.0.0.0 (ASCII bytes)
	#         4.0.1.3 Copy of 4.0.0.1; allocator for 4.0.1.3.0 (4 bytes)
	#             4.0.1.3.0 Copy of 4.0.0.1.0 (bytes)
	#         4.0.1.4 uint32 allocator for 4.0.1.4.0 (4 bytes)
	#             4.0.1.4.0 Private key #n (bytes)
	#         4.0.1.5 uint32 allocator for 4.0.1.5.0 (4 bytes)
	#             4.0.1.5.0 comment for key #n string (ASCII bytes)
	#         4.0.1.6 sequential padding

	key_n		= 1				# 密钥数量 (4.0 uint32 counter for # of keys)
	
	# 解算base64到bytes，注意公钥字节数组内是两个部分：类型+公钥，每个部分前面有4字节big endian长度
	public_bytes		= base64.b64decode(public_key)
	# 实际的公钥数据字节：去掉
	public_key_bytes	= public_bytes[(int.from_bytes(public_bytes[:4], 'big')+4+4):]

	# 解算base64到bytes
	private_bytes		= base64.b64decode(private_key)

	# 校验和1：未使用 (4.0.1.0 uint32 decryption "checksum" #1)
	# 校验和2：未使用 (4.0.1.1 uint32 decryption "checksum" #2)
	private_key_bytes	= _add_header_big_Endian_bytes('')+_add_header_big_Endian_bytes('')
	# 算法名（ssh-ed25519）(4.0.1.2.0 Copy of 4.0.0.0.0)
	# 公钥内容32字节 (4.0.1.3.0 Copy of 4.0.0.1.0)
	private_key_bytes	+=  public_bytes
	# 私钥内容64字节 (4.0.1.4.0 Private key #n (bytes))
	private_key_bytes	+=   _add_header_big_Endian_bytes(private_bytes[4:]+public_key_bytes)
	# 私钥注释 (4.0.1.5.0 comment for key #n string)
	private_key_bytes	+=  _add_header_big_Endian_bytes(comment)
	# sequential padding填充，私钥数据长度必须是8的倍数 (4.0.1.6 sequential padding)
	padding_length	= 8 - (len(private_key_bytes) % 8)
	if padding_length<8:
		private_key_bytes	+= bytes(range(1, 1+padding_length))	# 填充1,2,3...padding_length
	
	key_bytes	=  b'openssh-key-v1\0'			# OpenSSH密钥标识 (0.0 "openssh-key-v1" string plus terminating nullbyte)
	key_bytes	+= _add_header_big_Endian_bytes('none')		# 加密算法：无 (1.0 uint32 allocator for 1.0.0 cipher name string)
	key_bytes	+= _add_header_big_Endian_bytes('none')		# KDF算法：无 (2.0 uint32 allocator for 2.0.0 KDF name string)
	key_bytes	+= _add_header_big_Endian_bytes('')			# KDF选项 (3.0 uint32 allocator for KDF options, ALWAYS 0 for unencrypted keys)
	key_bytes	+= key_n.to_bytes(4, 'big')		# 密钥数量
	key_bytes	+= _add_header_big_Endian_bytes(public_bytes)	# 公钥
	key_bytes	+= _add_header_big_Endian_bytes(private_key_bytes)	# 私钥

	# 编码成base64字符串
	key_string	= base64.b64encode(key_bytes).decode('ascii')

	# 写入文件
	with open(filename, 'w', encoding='utf-8') as f:
		f.write("-----BEGIN OPENSSH PRIVATE KEY-----\n")
		f.write('\n'.join(key_string[i:i+70] for i in range(0, len(key_string), 70)))
		f.write("\n-----END OPENSSH PRIVATE KEY-----\n")

## 获取全部可用session
#	session_file	session文件为空时，则从PuTTY注册表配置中读取（仅Windows）
# 返回{
#	'HostName'	
#	'PortNumber'	
#	'UserName'	
#	'public_string'	
#	'private_string'
#	'session_name'
# }
def get_sessions(session_file=None):
	sessions	= {}

	if not session_file:
		# 仅在windows下可用
		if os.name!='nt':
			print("非windows环境必须指定session_file")
			return sessions
		
		try:
			# 读取putty的所有session的注册表
			session_root_key	= winreg.OpenKey(winreg.HKEY_CURRENT_USER, PUTTY_REGISTRY_PATH)
			subkeys_n, _, _	= winreg.QueryInfoKey(session_root_key)
			for i in range(subkeys_n):
				# 每个subkey是一个session，只取关键的配置
				session_name	= winreg.EnumKey(session_root_key, i)
				session		= get_PuTTY_session(session_name)

				# 校验session名的格式：user@HOSTNAME；若session名称里面包含了"."，则不是HOSTNAME，而是session名称，仍然放在HOSTNAME字段
				parts	= session_name.split('@')
				if len(parts)>1:
					# 包含@则必须是username@HOSTNAME的样式
					session['HOSTNAME']	= parts[1]
					if parts[0]!=session['UserName']:
						# 用户名得对得上才行
						session	= None
				elif '.' not in parts[0]:
					# 若不包含@，则应该是一个域名，必须包含"."，否则无效
					session	= None
				else:
					# 不包含@但包含"."的将session名称放在HOSTNAME
					session['HOSTNAME']	= parts[0]
				if session:
					# 获取密钥
					v	= _parse_ppk_file(session['PublicKeyFile'])
					if v:
						session['public_string']	= v['public_string']
						session['private_string']	= v['private_string']

					# 把session名记下来
					session['session_name']	= session_name
					sessions[session_name]	= session
			winreg.CloseKey(session_root_key)
			
		except Exception as e:
			print(f"读取注册表失败: {e}") 
	
		return sessions
	
	# 从session文件中获取
	records	= []
	try:
		book	= SpiritLong_excel.open_xlsx(session_file)
		records	= SpiritLong_excel.get_records_with_title(book['HOST'])
	except Exception as e:
		print(f"无法读取配置文件{session_file}：{e}")
		return sessions
	
	for r in records:
		try:
			# session名称：用户名@HOSTNAME，但若HOSTNAME有.则直接使用HOSTNAME作为session名称
			session_name	= r['HOSTNAME'] if '.' in r['HOSTNAME'] else f"{r['USER']@r['HOSTNAME']}"
			sessions[session_name]	= {
				'HostName'		: str(r['ADDRESS']),
				'PortNumber'		: r['PORT'],
				'UserName'		: r['USER'],
				'HOSTNAME'		: r['HOSTNAME'],
				'public_string'		: str(r.get('PUBLIC', '')).strip(),
				'private_string'	: str(r.get('PRIVATE', '')).strip(),
				'session_name'		: session_name,
			}
		except Exception as e:
			print(f'读取session失败：{str(e)}')

	return sessions

## 从sessions导出session文件
#	sessions	{sessions_name:{HostName，PortNumber, UserName}}
#	session_file	session文件
def export_sessions(sessions, session_file):
	records	= []
	for name in sessions:
		s	= sessions[name]
		
		# 检查是否有ppk文件(Windows only)
		ppk_file	= s.get('PublicKeyFile')
		if ppk_file:
			v	= _parse_ppk_file(ppk_file)
			if v.get('key_type')=='ssh-ed25519':
				s['private_string']	= v['private_string']
				s['public_string']	= v['public_string']
			
		if '.' not in name:
			# name不是域名时，则尝试分离出HOSTNAME
			parts	= name.split('@')
			name	= parts[1] if len(parts)>1 else parts[0]

		records.append({
			'ADDRESS'	: s['HostName'],
			'HOSTNAME'	: name,
			'PORT'		: s['PortNumber'],
			'USER'		: s['UserName'],
			'PRIVATE'	: s.get('private_string', ''),
			'PUBLIC'	: s.get('public_string',''),
		})

	# 写入到excel
	SpiritLong_excel.records_to_excel(session_file, {
		'HOST'	: {
			# 确保按此顺序放置各列
			'titles'	: ['USER', 'HOSTNAME', 'ADDRESS', 'PORT', 'PUBLIC', 'PRIVATE'],
			'records'	: records,
		}
	})

## 更新sessions所对应的密钥文件，如果session的密钥为空，则随机生成密钥
#	sessions	{sessions_name:{HostName，PortNumber, UserName}}
def update_key_file(sessions):
	# 检查是否有默认位置
	PPK_path	= os.path.join(USER_SSH_BASE, 'PPK')
	if os.name=='nt' and not os.path.exists(PPK_path):
		os.mkdir(PPK_path)

	OPENSSH_path	= os.path.join(USER_SSH_BASE, 'OPENSSH')
	if not os.path.exists(OPENSSH_path):
		os.mkdir(OPENSSH_path)
	

	for name in sessions:
		s	= sessions[name]

		public_string	= s.get('public_string', '')
		private_string	= s.get('private_string', '')
		
		# 是否需要生成随机密钥
		if not private_string or not public_string:
			v		= generate_random_key()
			private_string	= v['private_string']
			public_string	= v['public_string']
		
		# 生成密钥文件到默认位置
		if os.name=='nt':
			# Windows下需要生成PPK文件
			filename	= os.path.join(PPK_path, name+'.ppk')
			generate_ppk_file(filename, public_string, private_string, name)
		# 生成OPENSSH密钥
		filename	= os.path.join(OPENSSH_path, name+'.key')
		generate_OPENSSH_file(filename, public_string, private_string, name)

		# 更新session中的密钥
		s['public_string']	= public_string
		s['private_string']	= private_string


## 获取注册表中PuTTY的session信息。仅Windows下可用
#	session_name	session名称
# 返回PUTTY_CONFIG中所定义的字典
def get_PuTTY_session(session_name):
	# 仅在windows下可用
	if os.name!='nt':
		print("windows only")
		return None

	config	= {}
	try:
		session_key		= winreg.OpenKey(winreg.HKEY_CURRENT_USER, f"{PUTTY_REGISTRY_PATH}\\{session_name}")
	
		for c in PUTTY_CONFIG:
			config[c], value_type	= winreg.QueryValueEx(session_key, c)
		winreg.CloseKey(session_key)
	except Exception as e:
		print(e)
	return config

## 从密钥文件更新session文件中的密钥
#	session_name	session名称
#	session_file	session文件
#	key_file	.ppk或者.key（OPENSSH私钥），None表示使用默认文件（Windows是.ppk文件优先，其他则是.key文件优先）
def import_session_key(session_name, session_file, key_file=None):
	private_string	= None
	public_string	= None

	# 需确保parse_method返回的一定是{private_string, public_string, key_type, comment}的字典
	def try_parse_file(parse_method, filename):
		try:
			v	= parse_method(filename)
			if v['key_type']=='ssh-ed25519':
				# 只接受ed25519
				return (v['public_string'], v['private_string'])
			else:
				print(f"Failed: ed25519 only: {filename}")
		except Exception as e:
			print(e)
		return (None, None)

	if not os.path.exists(key_file):
		# 找不到密钥文件，则从默认值搜索
		PPK_file	= os.path.join(USER_SSH_BASE, 'PPK', session_name+'.ppk')
		OPENSSH_file	= os.path.join(USER_SSH_BASE, 'OPENSSH', session_name+'.key')
		if os.name=='nt':
			# Windows下先找ppk文件
			public_string, private_string	= try_parse_file(_parse_ppk_file, PPK_file)
			
		if public_string is None or private_string is None:
			# ppk没找到或Linux下，搜索key文件
			public_string, private_string	= try_parse_file(_parse_key_file, OPENSSH_file)
	elif key_file.endswith('.ppk'):
		# 按后缀名来识别PPK文件
		public_string, private_string	= try_parse_file(_parse_ppk_file, key_file)
	else:
		# 其他都视为OPENSSH密钥
		public_string, private_string	= try_parse_file(_parse_key_file, OPENSSH_file)
	
	if public_string is None or private_string is None:
		# 仍然没找到密钥
		print("Failed to import key")
		return

	# 读取session文件
	sessions	= get_sessions(session_file)
	if session_name not in sessions:
		print(f"no session named as {session_name}")
		return
	# 更新此文件密钥
	sessions[session_name]['public_string']		= public_string
	sessions[session_name]['private_string']	= private_string

	# 导出并重新生成密钥文件
	export_sessions(sessions, session_file)

	# 生成此session的密钥文件
	update_key_file({session_name:sessions[session_name]})

## 根据session文件写入PuTTY注册表选项（Windows only)
#	session_file	session文件名
#	PPK_path	密钥保存路径，ppk文件将放在path/PPK下，OPENSSH文件将放在path/OPENSSH下
#	who_as_root	当用户为root时，需要个表示实际人员的字符串，此字符串将出现在/root/.ssh/authorized_keys里面该密钥的后面注释，以区分是谁的公钥
#	flush		清除当前所有配置，重新写入。注意如果此选项为True，则需要管理员权限
#	font_name	字体名称
def import_PuTTY_sessions(session_file, who_as_root, SSH_path=USER_SSH_BASE, flush=False, font_name='Fixedsys', font_size=10):
	# 仅在windows下可用
	if os.name!='nt':
		print("windows only")
		return

	records	= []
	try:
		book	= SpiritLong_excel.open_xlsx(session_file)
		records	= SpiritLong_excel.get_records_with_title(book['HOST'])
	except Exception as e:
		print(f"无法读取session配置文件{session_file}: {e}")
		return
	
	# 清除当前所有session
	if flush:
		# 由于无法直接删除带子键的键，因此需要遍历。注意，需要尝试创建PUTTY_REGISTRY_PATH，若已存在则会打开
		session_root_key	= winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, PUTTY_REGISTRY_PATH, access=winreg.KEY_ALL_ACCESS)
		subkeys_n, _, _	= winreg.QueryInfoKey(session_root_key)
		session_names	= []
		for i in range(subkeys_n):
			# Session键中应该没有子键
			session_names.append(winreg.EnumKey(session_root_key, i))
		for s in session_names:
			session_key	= winreg.OpenKey(winreg.HKEY_CURRENT_USER, f"{PUTTY_REGISTRY_PATH}\\{s}")
			winreg.DeleteKeyEx(session_key, '')
		winreg.CloseKey(session_root_key)
	
	# PPK文件所在路径
	PPK_path	= os.path.join(SSH_path, 'PPK')
	if not os.path.exists(PPK_path):
		os.mkdir(PPK_path)

	# OPENSSH文件所在路径
	OPENSSH_path	= os.path.join(SSH_path, 'OPENSSH')
	if not os.path.exists(OPENSSH_path):
		os.mkdir(OPENSSH_path)
 
	for r in records:
		# session名称：用户名@HOSTNAME，但若HOSTNAME有.则直接使用HOSTNAME作为session名称
		session_name	= r['HOSTNAME'] if '.' in r['HOSTNAME'] else f"{r['USER']}@{r['HOSTNAME']}"
		# 创建记录
		session_key	= winreg.CreateKey(winreg.HKEY_CURRENT_USER, f"{PUTTY_REGISTRY_PATH}\\{session_name}")
		
		comment		= f"{who_as_root}@{r['HOSTNAME']}" if r['USER']=='root' else f"{r['USER']}"
		
		# 生成ppk
		ppk_file	= os.path.join(PPK_path, session_name+'.ppk')
		generate_ppk_file(ppk_file, r['PUBLIC'], r['PRIVATE'], comment)

		# 生成OPENSSH
		OPENSSH_file	= os.path.join(OPENSSH_path, session_name+'.key')
		generate_OPENSSH_file(OPENSSH_file, r['PUBLIC'], r['PRIVATE'], comment)
		
		# 写入默认的配置值
		for k in PUTTY_CONFIG_DEFAULT:
			# 不是字符串就只可能是DWORD了
			winreg.SetValueEx(session_key, k, 0, winreg.REG_SZ if isinstance(PUTTY_CONFIG_DEFAULT[k], str) else winreg.REG_DWORD, PUTTY_CONFIG_DEFAULT[k])

		# 写入/修改每个session的特有配置值
		winreg.SetValueEx(session_key, 'HostName',	0,	winreg.REG_SZ,		str(r['ADDRESS'])	)	# 主机地址
		winreg.SetValueEx(session_key, 'PortNumber',	0,	winreg.REG_DWORD,	r['PORT']		)	# 端口
		winreg.SetValueEx(session_key, 'UserName',	0,	winreg.REG_SZ,		r['USER']		)	# 用户名
		winreg.SetValueEx(session_key, 'PublicKeyFile',	0,	winreg.REG_SZ,		ppk_file		)	# PPK文件位置
		winreg.SetValueEx(session_key, 'WinTitle',	0,	winreg.REG_SZ,		session_name		)	# 窗口标题
		winreg.SetValueEx(session_key, 'Font',		0,	winreg.REG_SZ,		font_name		)	# 字体名称
		winreg.SetValueEx(session_key, 'FontHeight',	0,	winreg.REG_DWORD,		font_size		)	# 字体大小
		
		winreg.CloseKey(session_key)

## 从PuTTY注册表导出到session_file（Windows only)
## 从注册表读取PuTTY配置，写入到excel。仅Windows下可用。
#	session_file	session文件名
def export_PuTTY_sessions(session_file):
	# 仅在windows下可用
	if os.name!='nt':
		print("windows only")
		return
	
	# 从注册表读取sessions，并导出到session文件
	export_sessions(get_sessions(), session_file)

## 根据host文件更新主机上root认证：公钥（/root/.ssh/authorized_keys）和密码（可选），session的root公钥会自动添加上去，自动更新host的address/port
#	host_file	host文件
#	sessions	None表示从注册表获得sessions
#	update_password	是否更新root密码
#	password_length	随机密码长度
def update_host_root(host_file, sessions=None, update_password=False, password_length=32, session_file=None):
	# 读取host_file
	book	= SpiritLong_excel.open_xlsx(host_file)
	hosts	= SpiritLong_excel.get_records_with_title(book['HOST'])

	# roots需要按host分类
	records	= SpiritLong_excel.get_records_with_title(book['ROOT'])
	roots	= {}
	for r in records:
		if r['HOSTNAME'] not in roots:
			roots[r['HOSTNAME']]	= []
		roots[r['HOSTNAME']].append(r)
	book.close()
	
	if sessions is None:
		# 从注册表中获得sessions，非windows会返回{}
		sessions	= get_sessions()

	for hostname in roots:
		# 在sessions中寻找此host
		result	= [sessions[s] for s in sessions if sessions[s]['HOSTNAME']==hostname and sessions[s]['UserName']=='root']
		if not result:
			print(f"no session for host {hostname}")
			continue

		session	= result[0]

		content	= '\n'.join([f"{k['PUBLIC']} {k['COMMENT']}" for k in roots[hostname]])

		print(f"update root on {hostname:30}", end='')
		
		# 更新/root/.ssh/authorized_keys
		result	= execute_on_session(session['session_name'], f'echo "{content}" >/root/.ssh/authorized_keys')
		if result=='':
			# 返回空表示成功
			print("OK")
		
	# 更新root密码
	if update_password:
		print("update password")
		for h in hosts:
			hostname	= h['HOSTNAME']
			session_name	= f'root@{h['HOSTNAME']}'
			# 设置root密码
			print(f"update root on {hostname:30}", end='')
			result	= update_password_on_session(session_name, username='root', password=h['PASSWORD'], password_length=password_length, session_file=session_file)
			if result:
				# 成功更新密码，注意返回None肯定是失败了
				h['PASSWORD']	= result
				print("OK")
	
		# 如果跟新了密码就需要更新host文件
		root_records	= []
		for r in roots:
			root_records	+= roots[r]

		SpiritLong_excel.records_to_excel(host_file, {
			'HOST'	: {'records'	: hosts },
			'ROOT'	: {'records'	: root_records	}
		})


## 更新HOST文件中的root公钥（来自/root/.ssh/authorized_keys)，自动更新host的address/port
#	host_file	host文件
#	sessions	None表示从注册表获得sessions
def update_host_file(host_file, sessions=None):
	# 读取host_file中的HOST
	book	= SpiritLong_excel.open_xlsx(host_file)
	hosts	= SpiritLong_excel.get_records_with_title(book['HOST'])
	book.close()

	
	if sessions is None:
		# 从注册表中获得sessions，非windows会返回{}
		sessions	= get_sessions()

	roots	= []
	for host in hosts:
		# 在sessions中寻找此host
		result	= [sessions[s] for s in sessions if sessions[s]['HOSTNAME']==host['HOSTNAME'] and sessions[s]['UserName']=='root']
		if not result:
			print(f"no session for host {host['HOSTNAME']}")
			continue

		session	= result[0]

		# 更新hosts中的信息
		host['ADDRESS']	= session['HostName']
		host['PORT']	= session['PortNumber']

		print(f'read host {host['HOSTNAME']:30}', end='')

		# 获取host上的root公钥
		result	= execute_on_session(session['session_name'], f"cat /root/.ssh/authorized_keys")
		if result == '连接主机失败':
			continue
		else:
			print("OK")
		keys	= result.split('\n')
		
		
		for k in keys:
			if not k or k=='连接主机失败':
				# 跳过空行，以及连接失败的情况
				continue
			parts	= k.split(maxsplit=2)
			public	= f"{parts[0]} {parts[1]}"
			comment	= parts[2] if len(parts)>2 else ''
			roots.append({
				'HOSTNAME'	: host['HOSTNAME'],
				'PUBLIC'	: public,
				'COMMENT'	: comment,
			})
	
	# 写入到host文件
	SpiritLong_excel.records_to_excel(host_file, {
		'HOST'	: {'records'	: hosts },
		'ROOT'	: {'records'	: roots	}
	})
		

## 使用SFTP上传文件，使用session
def upload_on_session(session_name, local_file, remote_file, base=USER_SSH_BASE):
	ssh	= _connect_session(session_name, base)
	sftp	= ssh.open_sftp()
	sftp.put(local_file, remote_file)
	sftp.close()
	ssh.close()

## 使用SFTP下载文件，使用session
def download_on_session(session_name, remote_file, local_file, base=USER_SSH_BASE):
	ssh	= _connect_session(session_name, base)
	sftp	= ssh.open_sftp()
	sftp.get(remote_file, local_file)
	sftp.close()
	ssh.close()

## 远程连接执行非交互命令，使用session，返回stdout+stderr
def execute_on_session(session_name, command, base=USER_SSH_BASE, password=None, session_file=None):
	ssh	= _connect_session(session_name, base, password=password, session_file=session_file)
	if not ssh:
		return "连接主机失败"
	stdin, stdout, stderr	= ssh.exec_command(command, False)
	
	stdout_content	= stdout.read().decode(encoding='utf-8')
	stderr_content	= stderr.read().decode(encoding='utf-8')

	ssh.close()
	return stdout_content+stderr_content

## 测试sessions是否可以连接
def test_sessions_connectivity(sessions):
	max_length	= max([len(n) for n in sessions])
	for s in sessions:
		print(f'connecting {s:{max_length+5}}', end='')
		result	= execute_on_session(s, 'echo OK')
		if result=='OK\n':
			print("OK")

## 更新密码
def update_password_on_session(session_name, username='root', password=None, password_length=20, session_password=None, session_file=None):
	if password is None or len(password)<password_length:
		# 密码为空则将生成新密码，符号不包括引号（单双反）和圆括号以及冒号
		alphabet	= string.ascii_letters + string.digits + r"!#$%&*+,-./;<=>?@[]^_{|}~"
		password	= ''.join(secrets.choice(alphabet) for _ in range(password_length))

	# 更新密码
	result	= execute_on_session(session_name, f'echo "{username}:{password}" | chpasswd', password=session_password, session_file=session_file)
	# 返回空字符串表示成功，则返回新设置的密码
	return password if result=='' else None

## 导出SVN指定分支
# TODO


if __name__ == '__main__':
	
	me		= 'arthuryang'

	# 构建OneDrive文档SSH目录路径
	session_file	= os.path.join(USER_SSH_BASE, 'sessions.xlsx')
	# session_file	= os.path.join(USER_SSH_BASE, 'PuTTY配置 THS.xlsx')

	# ------------------ 导入 ------------------
	# import_PuTTY_sessions(session_file, me, flush=True, font_name='Maple Mono NF CN')
	
	# ------------------ 导出 ------------------
	# export_PuTTY_sessions(session_file)

	session_name	= 'arthuryang@PROGRAM'
	# result	= execute_on_session(session_name, 'cat .bashrc')
	# print(result)

	# sessions	= get_sessions(session_file)
	# for s in sessions:
	# 	print(s, sessions[s])

	# download_on_session(session_name, '.bashrc', 'bashrc')
	# upload_on_session(session_name, 'bashrc', 'bashrc')

	# 测试OPEFNSSH文件解析
	# OPENSSH_file	= 'a.key'
	# key	= generate_random_key()
	# generate_OPENSSH_file(OPENSSH_file, key['public_string'], key['private_string'], 'this is comment')

	# result	= _parse_key_file(OPENSSH_file)
	# for k in ['public', 'private', 'public_string', 'private_string']:
	# 	c	= key[k]==result[k]
	# 	print(k, c)
	# 	if not c:
	# 		print(key[k])
	# 		print(result[k])
	# for r in result:
	# 	print(result[r])

	# ------------------ 使用已有密钥更新指定任务 ------------------
	# #	更新session文件中的密钥
	# key_file	= os.path.join(USER_SSH_BASE, 'HOME-NANO.ppk')
	# sessions_name	= 'root@HOME-NANO'
	# import_session_key(session_name, session_file, key_file)
	# #	导入session文件，更新注册表
	# import_PuTTY_sessions(session_file, me, flush=True, font_name='Maple Mono NF CN')
	# #	更新密钥文件
	# sessions	= get_sessions()
	# update_key_file(sessions)

	# 更新密钥
	# sessions	= get_sessions()
	# update_key_file(sessions)
	# import_PuTTY_sessions(session_file, me, flush=True, font_name='Maple Mono NF CN')
	
	# 检查注册表中的session是否能够正确连接
	# sessions	= get_sessions()
	# test_sessions_connectivity(sessions)

	host_file	= os.path.join(USER_SSH_BASE, 'hosts.xlsx')
	# update_host_file(host_file)
	# update_host_root(host_file, update_password=True)
	pass