486 lines
14 KiB
Python
486 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
A simple client for ACMS. Description to be improved.
|
|
"""
|
|
|
|
|
|
import os
|
|
import sys
|
|
import ssl
|
|
import tty
|
|
import time
|
|
import random
|
|
import socket
|
|
import atexit
|
|
import tkinter
|
|
import termios
|
|
import readline
|
|
import subprocess
|
|
|
|
from colorama import init, Fore
|
|
from tkinter.messagebox import showwarning, showinfo
|
|
from threading import Thread
|
|
|
|
from Client.errorHandler import clientSendErrHandler, clientRecvErrHandler
|
|
from Common.socketCommands import sendData, recvData
|
|
|
|
|
|
__authors__ = "HorlogeSkynet, CaumartinYann"
|
|
__copyright__ = "Copyright 2017, ACMS"
|
|
__license__ = "GPLv3"
|
|
__status__ = "Production"
|
|
__date__ = "02/22/2017"
|
|
|
|
|
|
class SendThread(Thread):
|
|
def __init__(self):
|
|
Thread.__init__(self)
|
|
|
|
self.keepGoing = True
|
|
|
|
def run(self):
|
|
while True:
|
|
c = sys.stdin.buffer.read(1)
|
|
if not c:
|
|
break
|
|
|
|
else:
|
|
sock.send(c)
|
|
|
|
if not self.keepGoing:
|
|
break
|
|
|
|
def stop(self):
|
|
self.keepGoing = False
|
|
self.join()
|
|
|
|
|
|
#####################
|
|
# Display functions #
|
|
#####################
|
|
|
|
def clearScreen():
|
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|
|
|
|
|
def setUpMasterWindow():
|
|
# Tkinter root application
|
|
master = tkinter.Tk()
|
|
|
|
# A title and an icon
|
|
master.title("A Cooperative Medical System")
|
|
master.tk.call('wm', 'iconphoto', master._w, tkinter.PhotoImage(file="Client/Resources/ACMS_Logo.png"))
|
|
|
|
# Manage the window closing
|
|
master.protocol('WM_DELETE_WINDOW', quit)
|
|
|
|
# Let's return the object created
|
|
return master
|
|
|
|
|
|
def loadDrTuxOnWindow(master):
|
|
# Let's load (and resize) the ACMS's logo into a canvas on the window
|
|
logo = tkinter.PhotoImage(file="Client/Resources/ACMS_Logo.png").subsample(15, 15)
|
|
canvas = tkinter.Canvas(master, width=logo.width() + 4, height=logo.height() + 4)
|
|
canvas.create_image(3, 3, anchor=tkinter.NW, image=logo)
|
|
canvas.image = logo # F**k the garbage collector
|
|
canvas.grid(row=0, column=1)
|
|
|
|
# An oval around the logo #swag
|
|
canvas.create_rectangle(1, 1, logo.width() + 4, logo.height() + 4)
|
|
|
|
|
|
def promptServerSettings(serverIP_default, serverPort_default):
|
|
master = setUpMasterWindow()
|
|
|
|
loadDrTuxOnWindow(master)
|
|
|
|
# A label and an entry for the IP Address
|
|
tkinter.Label(master, text='IP Address').grid(row=1, column=0)
|
|
serverIP = tkinter.StringVar()
|
|
serverIP.set(serverIP_default)
|
|
tkinter.Entry(master, textvariable=serverIP, width=30, bd=5, bg=('dark orange' if serverIP_default != TCP_IP or serverPort_default != TCP_PORT else 'white')).grid(row=1, column=1)
|
|
|
|
# A label and an entry for the port
|
|
tkinter.Label(master, text='Port').grid(row=2, column=0)
|
|
serverPort = tkinter.IntVar()
|
|
serverPort.set(serverPort_default if serverPort_default != 0 else '')
|
|
tkinter.Entry(master, textvariable=serverPort, width=30, bd=5, bg=('dark orange' if serverIP_default != TCP_IP or serverPort_default != TCP_PORT else 'white')).grid(row=2, column=1)
|
|
|
|
# A button for the connection
|
|
tkinter.Button(master, text='Connect', command=master.destroy, bg='forest green').grid(row=3, column=1)
|
|
|
|
# while(1)...
|
|
master.mainloop()
|
|
|
|
try:
|
|
return serverIP.get(), serverPort.get()
|
|
|
|
except:
|
|
return '', 0
|
|
|
|
|
|
def promptUserCredentials(login_default):
|
|
master = setUpMasterWindow()
|
|
|
|
loadDrTuxOnWindow(master)
|
|
|
|
# A label and an entry for the IP Address
|
|
tkinter.Label(master, text='Login').grid(row=1, column=0)
|
|
login = tkinter.StringVar()
|
|
login.set(login_default)
|
|
tkinter.Entry(master, textvariable=login, width=30, bd=5, bg=('dark orange' if login_default != '' else 'white')).grid(row=1, column=1)
|
|
|
|
# A label and an entry for the port
|
|
tkinter.Label(master, text='Password').grid(row=2, column=0)
|
|
password = tkinter.StringVar()
|
|
password.set('')
|
|
tkinter.Entry(master, textvariable=password, show='●', width=30, bd=5, bg=('dark orange' if login_default != '' else 'white')).grid(row=2, column=1)
|
|
|
|
# A button for the connection
|
|
tkinter.Button(master, text='Log in', command=master.destroy, bg='forest green').grid(row=3, column=1)
|
|
|
|
# while(1)...
|
|
master.mainloop()
|
|
|
|
return login.get(), password.get()
|
|
|
|
|
|
#####################
|
|
# Sockets functions #
|
|
#####################
|
|
|
|
def cleaningFunction():
|
|
sock.close()
|
|
print("Bye !\n")
|
|
|
|
|
|
###################
|
|
# Auto-Completion #
|
|
###################
|
|
|
|
def completion(text, state):
|
|
options = [i for i in commands if i.startswith(text)]
|
|
if state < len(options):
|
|
return options[state]
|
|
else:
|
|
return None
|
|
|
|
|
|
##########
|
|
# main() #
|
|
##########
|
|
|
|
if __name__ == '__main__':
|
|
|
|
# Socket DEFAULT settings
|
|
TCP_IP = "127.0.0.1"
|
|
TCP_PORT = 4242
|
|
BUFFER_SIZE = 2048
|
|
|
|
# If the client has been started with an up flag, some graphic interactions will appear (instead of the console only)
|
|
graphicMode = True if len(sys.argv) > 1 and (sys.argv[1] == '1' or str.lower(sys.argv[1]) == 'true') else False
|
|
|
|
# Client socket initialization
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
# Let's add a SSL layer above the socket created previously
|
|
sock = ssl.wrap_socket(sock, server_side=False, ca_certs="Client/Credentials/server.crt", cert_reqs=ssl.CERT_REQUIRED, ssl_version=ssl.PROTOCOL_TLSv1_2)
|
|
|
|
# The socket has been created, we'll have to close it on program's ending
|
|
atexit.register(cleaningFunction)
|
|
|
|
# Colorama module initialization
|
|
init(autoreset=True)
|
|
|
|
# Displays some stuff
|
|
clearScreen()
|
|
print("\n\tWelcome on the ACMS\'s Client !\n")
|
|
|
|
# Connection procedure #
|
|
# Simple affectations in order to 'remember' user entries
|
|
serverIP, serverPort = TCP_IP, TCP_PORT
|
|
|
|
if graphicMode:
|
|
while True:
|
|
# Let's ask and get the (IP, Port) in order to connect to the server !
|
|
serverIP, serverPort = promptServerSettings(serverIP, serverPort)
|
|
|
|
# An invisible window to open up a message box without bug
|
|
messageBox = tkinter.Tk()
|
|
messageBox.withdraw()
|
|
|
|
try:
|
|
# Let's connect the client to the server
|
|
sock.connect((serverIP, serverPort))
|
|
showinfo("Successfully connected !", "You\'re now connected to \"" + serverIP + ':' + str(serverPort) + "\" !")
|
|
break
|
|
|
|
except ConnectionRefusedError:
|
|
showwarning("Connection has failed !", "No server seems running on \"\"" + serverIP + ':' + str(serverPort) + "\"\"...")
|
|
|
|
except (OSError, OverflowError):
|
|
showwarning("Connection has failed !", "Please check again the connection information...")
|
|
|
|
except ConnectionResetError:
|
|
showwarning("Your IP is blacklisted !", "Too many connections have been received from your IP on \"" + serverIP + ':' + str(serverPort) + "\"...")
|
|
quit()
|
|
|
|
finally:
|
|
messageBox.destroy()
|
|
|
|
else:
|
|
print(Fore.BLUE + "\nPlease, provide the ACMS\'s server information of your institute:\n" + Fore.RESET)
|
|
|
|
while True:
|
|
serverIP = ''
|
|
while not serverIP:
|
|
try:
|
|
serverIP = input('\tServer IP: ').strip()
|
|
|
|
except EOFError:
|
|
quit()
|
|
|
|
serverPort = ''
|
|
while not serverPort:
|
|
try:
|
|
serverPort = int(input('\tServer port: '))
|
|
|
|
except EOFError:
|
|
quit()
|
|
|
|
except ValueError:
|
|
serverPort = ''
|
|
|
|
try:
|
|
# Let's connect the client to the server
|
|
sock.connect((serverIP, serverPort))
|
|
print(Fore.GREEN + "\nYou\'re now connected to \"" + serverIP + ':' + str(serverPort) + "\" !\n" + Fore.RESET)
|
|
break
|
|
|
|
except ConnectionRefusedError:
|
|
print(Fore.RED + "\nNo server seems running on " + serverIP + ':' + str(serverPort) + "...\n" + Fore.RESET)
|
|
|
|
except (OSError, OverflowError):
|
|
print(Fore.RED + "\nPlease check again the connection information...\n" + Fore.RESET)
|
|
|
|
except ConnectionResetError:
|
|
print(Fore.RED + "\nToo many connections have been received from your IP on " + serverIP + ':' + str(serverPort) + "...\n" + Fore.RESET)
|
|
quit()
|
|
|
|
# Login procedure #
|
|
login = ''
|
|
while True:
|
|
password = '' # This variable has a reduced scope (stock password is bad !)
|
|
|
|
if graphicMode:
|
|
login, password = promptUserCredentials(login)
|
|
|
|
# An invisible window to open up a message box without a bug afterwards
|
|
messageBox = tkinter.Tk()
|
|
messageBox.withdraw()
|
|
|
|
else:
|
|
login = ''
|
|
while not login:
|
|
try:
|
|
login = input('\tLogin: ')
|
|
|
|
except EOFError:
|
|
quit()
|
|
|
|
while not password:
|
|
attributes = termios.tcgetattr(sys.stdin.fileno())
|
|
attributes[3] = attributes[3] & ~termios.ECHO
|
|
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
|
|
|
|
try:
|
|
password = input('\tPassword: ')
|
|
|
|
except:
|
|
quit()
|
|
|
|
finally:
|
|
attributes[3] = attributes[3] | termios.ECHO
|
|
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
|
|
|
|
sendData(sock, 'login:' + login, clientSendErrHandler)
|
|
if recvData(sock, clientRecvErrHandler) == 'LOGIN_OK':
|
|
sendData(sock, 'password:' + password, clientSendErrHandler)
|
|
if recvData(sock, clientRecvErrHandler) == 'PASSWORD_OK':
|
|
print(Fore.GREEN + "\n\nCongratulations, you\'re now authenticated on the ACMS server !\n\n" + Fore.RESET)
|
|
break
|
|
|
|
time.sleep(2 + random.random() * 0.5)
|
|
|
|
if graphicMode:
|
|
showwarning("Authentication failure !", "These credentials are invalid.")
|
|
messageBox.destroy()
|
|
|
|
else:
|
|
print(Fore.RED + "\n\nAuthentication failure: These credentials are invalid.\n" + Fore.RESET)
|
|
|
|
# At the login, the user will be in the root directory
|
|
currentWorkingDir = recvData(sock, clientRecvErrHandler)
|
|
if currentWorkingDir.startswith('NEW_PATH:'):
|
|
currentWorkingDir = currentWorkingDir.split(':', 1)[1]
|
|
|
|
else:
|
|
print(Fore.RED + "Received from server: \"" + currentWorkingDir + '\"\n' + Fore.RESET)
|
|
quit()
|
|
|
|
# Before showing the prompt to the client, let's fetch the commands he'll be able to perform (auto-completion stuff)
|
|
sendData(sock, 'help', clientSendErrHandler)
|
|
commands = [c for c in [c.split(' ')[0].rstrip(':').split(' ')[0] for c in recvData(sock, clientRecvErrHandler).split('\t')[1:]] if c]
|
|
|
|
# Sets the completion key
|
|
readline.parse_and_bind("tab: complete")
|
|
readline.set_completer(completion)
|
|
|
|
# Usage procedure
|
|
while True:
|
|
|
|
command = ''
|
|
try:
|
|
command = input('[' + Fore.RED + login + Fore.RESET + '@' + Fore.MAGENTA + serverIP + Fore.RESET + ':' + Fore.BLUE + currentWorkingDir + Fore.RESET + ']% ')
|
|
|
|
except EOFError:
|
|
# Force disconnection by server on EOF
|
|
sendData(sock, 'exit', clientSendErrHandler)
|
|
quit()
|
|
|
|
if not command or not command.strip():
|
|
continue
|
|
|
|
else:
|
|
# The command is sent to the server...
|
|
sendData(sock, command, clientSendErrHandler)
|
|
|
|
# ... and the answer is caught here
|
|
data = recvData(sock, clientRecvErrHandler)
|
|
|
|
# If the socket has been closed by server, let's shutdown the client
|
|
if not data:
|
|
print(Fore.RED + "\nThe connection with the server has been closed, let\'s close the client !\n" + Fore.RESET)
|
|
break
|
|
|
|
elif data.startswith('NEW_PATH:'):
|
|
currentWorkingDir = data.split(':', 1)[1]
|
|
|
|
elif data == 'CLEAR_SCREEN':
|
|
clearScreen()
|
|
|
|
elif data == 'REDIRECT':
|
|
old_settings = termios.tcgetattr(sys.stdin)
|
|
tty.setcbreak(sys.stdin)
|
|
|
|
sendThread = SendThread()
|
|
sendThread.start()
|
|
|
|
while True:
|
|
c = recvData(sock, clientRecvErrHandler)
|
|
if c == "VIM_STOP_REDIRECT_ACMS_2017_TOKEN:$}a*M-2i5<9Ù3(+$":
|
|
sendThread.stop()
|
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
print('\n', end='')
|
|
break
|
|
|
|
elif c.startswith("Vim : Alerte : La sortie ne s'effectue pas sur un terminal") \
|
|
or c.startswith("Vim : Alerte : L'entrée ne se fait pas sur un terminal") \
|
|
or c.startswith("Vim: Warning: Output is not to a terminal") \
|
|
or c.startswith("Vim: Warning: Input is not from a terminal"):
|
|
pass
|
|
|
|
else:
|
|
sys.stdout.buffer.write(c.encode())
|
|
sys.stdout.flush()
|
|
|
|
# It happens that sometimes (with specific terminals [hello Ubuntu]), buffer's threads had sent data after the process termination
|
|
while True:
|
|
dummy = recvData(sock, clientRecvErrHandler)
|
|
|
|
if dummy == "VIM_EMPTY_BUFFER_PROCEDURE_2017_TOKEN:$}a*M-2i5<9Ù3(+$":
|
|
break
|
|
|
|
else:
|
|
# This is the end of the VIM's stuffs... Let's just print it
|
|
print(dummy)
|
|
|
|
elif data == 'NEW_USER_PROCEDURE':
|
|
attributes = termios.tcgetattr(sys.stdin.fileno())
|
|
attributes[3] = attributes[3] & ~termios.ECHO
|
|
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
|
|
|
|
try:
|
|
password_1 = input("\nEnter a password: ")
|
|
password_2 = input("\nRetype the password: ")
|
|
|
|
if len(password_1) == 0 or password_1 != password_2:
|
|
print(Fore.RED + "\n\nPasswords do not match (or are empty ?)..." + Fore.RESET, end='')
|
|
sendData(sock, 'ABORTED', clientSendErrHandler)
|
|
|
|
else:
|
|
sendData(sock, 'USER_PASSWORD:' + password_1, clientSendErrHandler)
|
|
|
|
except:
|
|
sendData(sock, 'ABORTED', clientSendErrHandler)
|
|
|
|
finally:
|
|
attributes[3] = attributes[3] | termios.ECHO
|
|
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
|
|
|
|
print('\n\n' + recvData(sock, clientRecvErrHandler) + '\n')
|
|
|
|
elif data == 'CHANGE_PASSWD_PROCEDURE':
|
|
attributes = termios.tcgetattr(sys.stdin.fileno())
|
|
attributes[3] = attributes[3] & ~termios.ECHO
|
|
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
|
|
|
|
try:
|
|
oldPassword = input("\nYour (current) password: ")
|
|
|
|
sendData(sock, 'OLD_PASSWORD:' + oldPassword, clientSendErrHandler)
|
|
|
|
result = recvData(sock, clientRecvErrHandler)
|
|
if result != 'OLD_PASSWORD_OK':
|
|
print('\n\n' + result + '\n')
|
|
|
|
else:
|
|
newPassword_1 = input("\nEnter new password: ")
|
|
newPassword_2 = input("\nRetype new password: ")
|
|
|
|
if len(newPassword_1) == 0 or newPassword_1 != newPassword_2:
|
|
print(Fore.RED + "\n\nPasswords do not match (or are empty ?)..." + Fore.RESET, end='')
|
|
sendData(sock, 'ABORTED', clientSendErrHandler)
|
|
|
|
else:
|
|
sendData(sock, 'NEW_PASSWORD:' + newPassword_1, clientSendErrHandler)
|
|
|
|
except:
|
|
sendData(sock, 'ABORTED', clientSendErrHandler)
|
|
|
|
finally:
|
|
attributes[3] = attributes[3] | termios.ECHO
|
|
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
|
|
|
|
# Tricky part: in one case we don't have to receive any message from server...
|
|
if result == 'OLD_PASSWORD_OK':
|
|
print('\n\n' + recvData(sock, clientRecvErrHandler) + '\n')
|
|
|
|
elif data.startswith('VNCSERVER_RUNNING:'):
|
|
if not os.path.exists('VNC/bin/VNCClient'):
|
|
print(Fore.RED + "\nVNC module is not available on your system.\n" + Fore.RESET)
|
|
sendData(sock, 'FATAL_ERROR', clientSendErrHandler, sock)
|
|
|
|
else:
|
|
p = subprocess.Popen(['./VNC/bin/VNCClient', serverIP + ':' + str(data.split(':')[1])], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
sendData(sock, 'VNCCLIENT_RUNNING', clientSendErrHandler, sock)
|
|
p.wait()
|
|
sendData(sock, 'USELESS_DATA', clientSendErrHandler, sock)
|
|
print('\n', end='')
|
|
|
|
else:
|
|
print('\n' + data + '\n')
|