网络安全期末大作业 | 八月更文挑战

4,641 阅读9分钟

环境说明

  • 操作系统:mac
  • 编程语言:Python3
  • 依赖Python库:tkinter、pymysql、ssl、socket等(大多均为Python内置)
  • mysql中应先建立数据库:filetransfer,新建表user,含有三个字段:id、username、password

生成证书

PROJECT_NAME="jiang zheng fool Project"
# Generate the openssl configuration files.
cat > ca_cert.conf << EOF
[ req ]
distinguished_name     = req_distinguished_name
prompt                 = no
[ req_distinguished_name ]
 O                      = $PROJECT_NAME  Certificate Authority
EOF
cat > server_cert.conf << EOF
[ req ]
distinguished_name     = req_distinguished_name
prompt                 = no
[ req_distinguished_name ]
 O                      = $PROJECT_NAME
 CN                     = SERVER
EOF
cat > client_cert.conf << EOF
[ req ]
distinguished_name     = req_distinguished_name
prompt                 = no

[ req_distinguished_name ]
 O                      = $PROJECT_NAME Device Certificate
 CN                     = SERVER
EOF
mkdir ca
mkdir server
mkdir client
mkdir certDER
# private key generation
openssl genrsa -out ca.key 1024
openssl genrsa -out server.key 1024
openssl genrsa -out client.key 1024
# cert requests
openssl req -out ca.req -key ca.key -new -config ./ca_cert.conf
openssl req -out server.req -key server.key -new -config ./server_cert.conf
openssl req -out client.req -key client.key -new -config ./client_cert.conf
# generate the actual certs.
openssl x509 -req -in ca.req -out ca.crt -sha1 -days 5000 -signkey ca.key
openssl x509 -req -in server.req -out server.crt -sha1 -CAcreateserial -days 5000 -CA ca.crt -CAkey ca.key
openssl x509 -req -in client.req -out client.crt -sha1 -CAcreateserial -days 5000 -CA ca.crt -CAkey ca.key
mv ca.crt ca.key ca/
mv server.crt server.key server/
mv client.crt client.key client/

SSL协议提供的服务主要有:

1)认证用户和服务器,确保数据发送到正确的客户机和服务器;

2)加密数据以防止数据中途被窃取;

3)维护数据的完整性,确保数据在传输过程中不被改变。

生成证书文件如下:

image.png

打开ca.crt 可以看到如下图所示的结果:

image.png

image.png

概述

该项目主要有两个小项目组成,包括群聊功能,和文件传输系统。群聊功能主要实现了多人群聊,缓存消息等功能。文件传输系统包括文件的上传和下载。

群聊功能

程序启动

- client.py  客户端1
- client2.py  客户端2
- server.py   服务端

效果图

image.png

文件夹介绍

  • cer -- 该文件夹存放了CA根证书及服务器、客户端证书(使用OpenSSL生成)
    • CA -- 根证书及秘钥
    • server -- 服务器秘钥、代签名证书及已签名证书
    • client -- 客户端秘钥、代签名证书及已签名证书

文件介绍

MultiPersonChat

  • client.py 客户端1
  • client2.py 客户端2
  • server.py 服务端

完成功能

  • 上线提醒,下线提醒
  • ssl链接
  • 密码加密
  • 用户不可重复登录
  • server端需要缓存最近的部分历史消息,以便向刚上线的client时进行推送
  • 可视化界面
  • 多用户聊天

client.py Client类

方法名具体功能
send_login_info发送登录用户的用户名和密码给服务器验证,并return验证结果
send_register_info发送用户注册的用户名和密码给服务器,并返回注册结果
recv_data客户端向服务器接收数据
close关闭客户端与服务器连接的套接字

client.py LoginPanel 类

方法名具体功能
set_panel_position设置登陆界面在屏幕中的位置和大小
config_for_reg_panel给登陆界面设置其他配置
set_title放置界面标题
set_form放置登陆表单
set_btn放置注册和登陆按钮
show调用实例方法给登录界面做整体布局
close实现对界面的关闭
get_input获取用户输入的用户名和密码
login_func封装到登陆界面中的登录按钮的功能
reg_func封装到登录界面的注册按钮中,实现从登录界面跳转到注册界面

server.py

方法名功能
encrypt_psw使用 MD5 算法对用户的密码进行加密
check_user检查用户登录时输入的用户名和密码是否正确
add_user将要注册的用户名进行判断是否有重复用户名
update_online_list更新客户端在线用户列表
online_notice给所有在线客户端发送新客户端上线的通知
offline_notice给所有在线用户发送用户离线通知
handle_login处理登录请求
handle_reg处理客户端的注册请求
handle_msg对客户端要发送的内容进行广播
handle服务器运行的主框架

关键代码如下,负责请求的分发:

def handle(new_socket, addr):
    """
    服务器运行的主框架
    :param new_socket: 本次连接的客户端套接字
    :param addr: 本次连接客户端的ip和port
    """
    try:
        while True:
            req_type = new_socket.recv(1).decode("utf-8")  # 获取请求类型
            print(req_type)
            if req_type:  # 如果不为真,则说明客户端已断开
                if req_type == "1":  # 登录请求
                    print("开始处理登录请求")
                    handle_login(new_socket)
                elif req_type == "2":  # 注册请求
                    print("开始处理注册请求")
                    handle_reg(new_socket)
                elif req_type == "3":  # 发送消息
                    print("开始处理发送消息请求")
                    handle_msg(new_socket)
            else:
                break
    except Exception as ret:
        print(str(addr) + " 连接异常,准备断开: " + str(ret))
    finally:
        try:
            # 客户端断开后执行的操作
            new_socket.close()
            online_socket.remove(new_socket)
            offline_notice(new_socket)
            socket2user.pop(new_socket)
            time.sleep(4)
            update_online_list()
        except Exception as ret:
            print(str(addr) + "连接关闭异常")


缓存功能介绍

通过结构体把每一个聊天的内容和用户存储到数组当中,等用户登录的时候再分发给用户

文件安全传输功能

启动方法

  • 启动服务器:
python server_ssl.py   
python server_no_ssl.py
  • 启动客户端:
python main.py

文件夹说明

  • cer -- 该文件夹存放了CA根证书及服务器、客户端证书(使用OpenSSL生成)
    • CA -- 根证书及秘钥
    • server -- 服务器秘钥、代签名证书及已签名证书
    • client -- 客户端秘钥、代签名证书及已签名证书
  • ClientCache -- 该目录存放向服务器请求更新的下载列表数据
  • ClientDownload -- 客户端下载路径
  • ServerRec -- 服务器上传路径

文件说明

  • main.py 客户端启动文件
  • client_login.py 客户端登录界面
  • client_mian.py 客户端主界面
  • view.py 客户端主界面视图
  • client_socket_no_ssl.py 客户端不加密通信对象
  • client_socket_ssl.py 客户端加密通信对象
  • server_no_ssl.py 服务器不加密通信代码
  • server_ssl.py 服务器加密不加密通信代码
  • result.txt 用来记录服务器的下载列表
  • Serverlog.txt 服务器日志

流程图

服务端流程如下: image.png

客户端流程图如下: image.png

自定义传输协议

在服务器与客户端的每次交流都添加了自定义报头,客户端主动向服务器请求数据,服务器被动返回,因此两者的数据包头会不同。本系统使用了python的struct结构体实现了报头二进制流的传输。

客户端的报头内容如下:(1024字节)

  • Command包括:Download、Upload、Update、Login和Register
  • filename在download指令下是要下载的文件名,在Upload模式下是本地上传文件的路径。
  • filesize是文件的大小
  • time是数据请求的时间
  • user和password是用户名和密码,每次请求数据都会验证一次,模拟Cookie模式。
header = {
            'Command': 'Download',
            'fileName': filename,
            'fileSize': '',
            'time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
            'user': self.username,
            'password': self.password,
        }

python中struct 用法如下

header_hex = bytes(json.dumps(header).encode('utf-8'))
fhead = struct.pack('1024s', header_hex)
self.ssock.send(fhead)

服务器的报头内容如下:(128字节)

由于服务器是被动地回复客户端,所以报头内容不需要太多,故使用128字节。

  • Feedback指示要回应的指令
  • Stat指示响应的状态(如注册、登录等)
  • Filesize是文件的大小
  • User是当前用户
header = {
                'Feedback': 'Login',
                'stat': 'Success',
                'fileSize': os.stat(listResult).st_size,
                'user': username
             }

关键代码

负责分发请求:

    def conn_thread(self,connection):
        while True:
            try:
                connection.settimeout(60)
                fileinfo_size = struct.calcsize('1024s')
                buf = connection.recv(fileinfo_size)
                if buf:  # 如果不加这个if,第一个文件传输完成后会自动走到下一句
                    header_json = str(struct.unpack('1024s', buf)[0], encoding='utf-8').strip('\00')
                    #print(header_json)
                    header = json.loads(header_json)
                    Command = header['Command']

                    if Command == 'Upload':
                        fileName = header['fileName']
                        fileSize = header['fileSize']
                        time = header['time']
                        user = header['user']
                        filenewname = os.path.join('ServerRec/', fileName)
                        print('Upload: file new name is %s, filesize is %s' % (filenewname, fileSize))
                        recvd_size = 0  # 定义接收了的文件大小
                        file = open(filenewname, 'wb')
                        print('start receiving...')
                        while not recvd_size == fileSize:
                            if fileSize - recvd_size > 1024:
                                rdata = connection.recv(1024)
                                recvd_size += len(rdata)
                            else:
                                rdata = connection.recv(fileSize - recvd_size)
                                recvd_size = fileSize
                            file.write(rdata)
                        file.close()
                        print('receive done')

                        fileSize = float(fileSize)
                        if fileSize<1024.0:
                            fileSize = "%s bytes"%(int(fileSize))
                        elif fileSize/1024.0 <= 1024.0:
                            fileSize = "%.2f Kb"%(fileSize/1024.0)
                        elif fileSize/1024.0/1024.0 <= 1024.0:
                            fileSize = "%.2f Mb"%(fileSize/1024.0/1024.0)
                        else:
                            fileSize = "%.2f Gb"%(fileSize/1024.0/1024.0/1024.0)

                        uploadmsg = '{"文件名": "%s", "上传者": "%s", "上传时间": "%s", "大小": "%s"}\n'%\
                                    (fileName,user,time,fileSize)
                        with open('result.txt', 'a', encoding='utf-8') as list:
                            list.write(uploadmsg)

                        uploadlog = '\n%s upload a file "%s" at %s' % \
                                        (user, fileName, time)
                        with open('Serverlog.txt', 'a', encoding='utf-8') as list:
                            list.write(uploadlog)
                        #connection.close()

                    elif Command == 'Login':
                        # 查询数据表数据
                        username = header['user']
                        password = header['password']
                        time = header['time']
                        sql = "select * from user where username = '%s' and password = '%s'"%(username,password)
                        cursor.execute(sql)
                        data = cursor.fetchone()
                        if data:
                            listResult = './result.txt'
                            # 定义文件头信息,包含文件名和文件大小
                            header = {
                                'Feedback': 'Login',
                                'stat': 'Success',
                                'fileSize': os.stat(listResult).st_size,
                                'user': username
                            }
                            header_hex = bytes(json.dumps(header).encode('utf-8'))
                            fhead = struct.pack('128s', header_hex)
                            connection.send(fhead)

                            fo = open(listResult, 'rb')
                            while True:
                                filedata = fo.read(1024)
                                if not filedata:
                                    break
                                connection.send(filedata)
                            fo.close()
                            print('%s login successfully')

                            loginlog = '\n%s try to login at "%s" , Stat: Success ' % \
                                        (username, time)
                            with open('Serverlog.txt', 'a', encoding='utf-8') as list:
                                list.write(loginlog)
                            #connection.close()
                        else:
                            header = {
                                'Feedback': 'Login',
                                'stat': 'Fail',
                                'fileSize': '',
                                'user': username
                            }
                            header_hex = bytes(json.dumps(header).encode('utf-8'))
                            fhead = struct.pack('128s', header_hex)
                            connection.send(fhead)
                            loginlog = '\n%s try to login at "%s" , Stat: Fail ' % \
                                       (username, time)
                            with open('Serverlog.txt', 'a', encoding='utf-8') as list:
                                list.write(loginlog)

                    elif Command == 'Download':
                        # 查询数据表数据
                        username = header['user']
                        password = header['password']
                        time = header['time']
                        sql = "select * from user where username = '%s' and password = '%s'" % (username, password)
                        cursor.execute(sql)
                        data = cursor.fetchone()
                        filename = header['fileName']
                        if data:

                            filepath = os.path.join('./ServerREc/', filename)
                            # 定义文件头信息,包含文件名和文件大小
                            header = {
                                'Feedback': 'Download',
                                'stat': 'Success',
                                'fileSize': os.stat(filepath).st_size,
                                'user': username
                            }
                            header_hex = bytes(json.dumps(header).encode('utf-8'))
                            fhead = struct.pack('128s', header_hex)
                            connection.send(fhead)

                            fo = open(filepath, 'rb')
                            while True:
                                filedata = fo.read(1024)
                                if not filedata:
                                    break
                                connection.send(filedata)
                            fo.close()
                            print('send file over...')
                            downloadlog = '\n%s download a file "%s" at %s' % \
                                          (username, filename, time)
                            with open('Serverlog.txt', 'a', encoding='utf-8') as list:
                                list.write(downloadlog)
                            # connection.close()
                        else:
                            header = {
                                'Feedback': 'Download',
                                'stat': 'LoginFail',
                                'fileSize': '',
                                'user': username
                            }
                            header_hex = bytes(json.dumps(header).encode('utf-8'))
                            fhead = struct.pack('128s', header_hex)
                            connection.send(fhead)

                    elif Command == 'Update':
                        # 查询数据表数据
                        username = header['user']
                        password = header['password']
                        sql = "select * from user where username = '%s' and password = '%s'" % (username, password)
                        cursor.execute(sql)
                        data = cursor.fetchone()
                        if data:
                            listResult = './result.txt'
                            # 定义文件头信息,包含文件名和文件大小
                            header = {
                                'Feedback': 'Update',
                                'stat': 'Success',
                                'fileSize': os.stat(listResult).st_size,
                                'user': username
                            }
                            header_hex = bytes(json.dumps(header).encode('utf-8'))
                            fhead = struct.pack('128s', header_hex)
                            connection.send(fhead)

                            fo = open(listResult, 'rb')
                            while True:
                                filedata = fo.read(1024)
                                if not filedata:
                                    break
                                connection.send(filedata)
                            fo.close()
                            #print('send list over...')
                            # connection.close()
                        else:
                            header = {
                                'Feedback': 'Login',
                                'stat': 'Fail',
                                'fileSize': '',
                                'user': username
                            }
                            header_hex = bytes(json.dumps(header).encode('utf-8'))
                            fhead = struct.pack('128s', header_hex)
                            connection.send(fhead)

                    elif Command == 'Register':
                        # 查询数据表数据
                        username = header['user']
                        password = header['password']
                        time = header['time']
                        sql = "select * from user where username = '%s'" % (username)
                        cursor.execute(sql)
                        data = cursor.fetchone()
                        if data:
                            # 定义文件头信息,包含文件名和文件大小
                            header = {
                                'Feedback': 'Register',
                                'stat': 'Exist',
                                'fileSize': '',
                                'user': username
                            }
                            header_hex = bytes(json.dumps(header).encode('utf-8'))
                            fhead = struct.pack('128s', header_hex)
                            connection.send(fhead)
                            loginlog = '\n%s try to register at "%s" , Stat: Fail ' % \
                                       (username, time)
                            with open('Serverlog.txt', 'a', encoding='utf-8') as list:
                                list.write(loginlog)
                        else:
                            sql = "insert into user values ('','%s','%s')"%(username,password)
                            cursor.execute(sql)
                            db.commit()
                            # 定义文件头信息,包含文件名和文件大小
                            header = {
                                'Feedback': 'Register',
                                'stat': 'Success',
                                'fileSize': '',
                                'user': username
                            }
                            header_hex = bytes(json.dumps(header).encode('utf-8'))
                            fhead = struct.pack('128s', header_hex)
                            connection.send(fhead)
                            loginlog = '\n%s try to register at "%s" , Stat: Success ' % \
                                       (username, time)
                            with open('Serverlog.txt', 'a', encoding='utf-8') as list:
                                list.write(loginlog)

            except socket.timeout:
                connection.close()
                break
            except ConnectionResetError:
                connection.close()
                break

效果图

文件列表的效果图如下: image.png

文件上传的效果图如下: image.png

遇到问题

  • 不会GUI :使用Python GUI编程(Tkinter)工具包
  • 不会生成证书:openssl 命令
  • hostname match errorcontext.wrap_socket(self.sock, server_hostname='SERVER', server_side=False)中的server_hostname要和server.conf 中的CN保持一致

实验收获与体会

这次的实验让我学到了很多东西,让我对 SSL 加密传输和 TCP 协议有了更深的了 解。以前都是在书上看到的 TCP 协议知识,这次通过自己抓包,验证了 TCP 的三次握 手、ACK 号等等,让我对计算机网络知识更加感兴趣了。此外,这次实验自己设计协议, 自己申请 SSL 证书、实现 GUI 等都是以前没有试过的,完成之后成就感非常大,对相 关领域的理解又加深了一点。 此次实验遇到的问题主要有 GUI 的设计、SSL 证书的申请等。因为事前不是很了 解,所以开发时比较难处理,后来经过大量查阅资料、逛博客、学习别人代码,终于把 困难一一解决,成功实现了所有功能。这次的实验锻炼了我思考和解决问题的能力,也 增加了自己的经验,非常充实。