This repository has been archived on 2023-11-03. You can view files and clone it, but cannot push or open issues or pull requests.
ACMS/src/client.py

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')