Skip to content

Commit

Permalink
支持FTPS
Browse files Browse the repository at this point in the history
  • Loading branch information
jark006 committed Nov 11, 2024
1 parent caff5ab commit e587958
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 38 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ env
venv
FtpServer.json
*.csv
*.crt
*.key
__pycache__
mypyftpdlib/__pycache__
ftpServer.onefile-build
Expand Down
55 changes: 33 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,60 @@
第三列:权限,使用`readonly``只读`(需保证文本格式为UTF-8) 设置`只读权限`。使用`readwrite``读写`设置`读写权限`,或者使用自定义,从以下权限挑选自行组合:

参考链接:https://pyftpdlib.readthedocs.io/en/latest/api.html#pyftpdlib.authorizers.DummyAuthorizer.add_user
读取权限:
- "e" = 更改目录 (CWD 命令)
- "l" = 列出文件 (LIST、NLST、STAT、MLSD、MLST、SIZE、MDTM 命令)
- "r" = 从服务器检索文件 (RETR 命令)

写入权限:
- "a" = 将数据附加到现有文件 (APPE 命令)
- "d" = 删除文件或目录 (DELE、RMD 命令)
- "f" = 重命名文件或目录 (RNFR、RNTO 命令)
- "m" = 创建目录 (MKD 命令)
- "w" = 将文件存储到服务器 (STOR、STOU 命令)
- "M" = 更改文件模式 (SITE CHMOD 命令)
- "T" = 更新文件上次修改时间 (MFMT 命令)

读取权限:
- "e" = 更改目录 (CWD 命令)
- "l" = 列出文件 (LIST、NLST、STAT、MLSD、MLST、SIZE、MDTM 命令)
- "r" = 从服务器检索文件 (RETR 命令)

写入权限:
- "a" = 将数据附加到现有文件 (APPE 命令)
- "d" = 删除文件或目录 (DELE、RMD 命令)
- "f" = 重命名文件或目录 (RNFR、RNTO 命令)
- "m" = 创建目录 (MKD 命令)
- "w" = 将文件存储到服务器 (STOR、STOU 命令)
- "M" = 更改文件模式 (SITE CHMOD 命令)
- "T" = 更新文件上次修改时间 (MFMT 命令)

第四列:根目录路径


### 例如
**例如**

|||||
|-|-|-|-|
| JARK006|123456|readonly|D:\Downloads|
| JARK007|456789|readwrite|D:\Data|
| JARK008|abc123|只读|D:\FtpRoot|
| JARK009|abc456|elr|D:\FtpRoot|
| anonymous||elr|D:\FtpRoot|
| ...||||

### 其他
注: anonymous 是匿名用户,允许不设密码,其他用户必须设置密码

**其他**

若读取到有效配置,则自动禁用主页面的用户/密码设置。
若临时不需多用户配置,可将文件重命名为其他名称。
1. 若读取到有效配置,则自动`禁用`主页面的用户/密码设置。
1. 若临时不需多用户配置,可将文件重命名为其他名称。
1. 若配置文件存在`中文汉字`,需确保文本为 `UTF-8` 格式
1. 密码不要出现英文逗号 `,` 字符,以免和csv文本格式冲突

---

### 使用到的第三方组件或服务
### FTPS 配置

1. [pyftpdlib](https://github.com/giampaolo/pyftpdlib)
在Linux或MINGW64终端中生成SSL证书文件,`不要重命名`文件为其他名称,直接将其放到程序所在目录则自动启用 `FTPS [TLS/SSL显式加密, TLSv1.3]`

1. [tkinter](https://docs.python.org/3/library/tkinter.html)
```sh
# 生成 ftpServer.key 和 ftpServer.crt 有效期100年,需填入一些简单信息(地区/名字/Email等)
openssl req -x509 -newkey rsa:2048 -keyout ftpServer.key -out ftpServer.crt -nodes -days 36500
```

1. [pystray](https://github.com/moses-palmer/pystray)
### 使用到的库

1. [pyftpdlib](https://github.com/giampaolo/pyftpdlib)
1. [tkinter](https://docs.python.org/3/library/tkinter.html)
1. [pystray](https://github.com/moses-palmer/pystray)
1. [Pillow](https://github.com/python-pillow/Pillow)

---
Expand Down
2 changes: 1 addition & 1 deletion Settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self) -> None:
):
self.appDirectory = self.appDirectory[0].upper() + self.appDirectory[1:]

self.savePath = os.path.join(self.appDirectory, "FtpServer.json")
self.savePath = os.path.join(self.appDirectory, "ftpServer.json")

self.directoryList: list[str] = [self.appDirectory]
self.userName: str = "JARK006"
Expand Down
5 changes: 4 additions & 1 deletion UserList.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ def load(self):
if (
len(item[0].strip()) > 0
and len(item[1].strip()) > 0
and len(item[2].strip()) > 0
and len(item[3].strip()) > 0
):
if item[0].strip() in self.userNameSet:
Expand All @@ -96,6 +95,10 @@ def load(self):
print(
f"该用户名条目 [{item[0].strip()}] 的路径不存在或无访问权限 [{item[3].strip()}] 已跳过此内容 [{line}]"
)
elif item[0].strip() != "anonymous" and len(item[2].strip()) == 0:
print(
f"该用户名条目 [{item[0].strip()}] 没有密码(只有匿名用户 anonymous 可以不设密码),已跳过此内容 [{line}]"
)
else:
self.userNameSet.add(item[0].strip())
self.userList.append(
Expand Down
8 changes: 4 additions & 4 deletions file_version_info.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0.
filevers=(1, 15, 0, 0),
prodvers=(1, 15, 0, 0),
filevers=(1, 16, 0, 0),
prodvers=(1, 16, 0, 0),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
Expand All @@ -31,12 +31,12 @@ VSVersionInfo(
'080404b0',
[StringStruct('CompanyName', 'Github@JARK006'),
StringStruct('FileDescription', 'FtpServer Github@JARK006'),
StringStruct('FileVersion', '1.15.0.0'),
StringStruct('FileVersion', '1.16.0.0'),
StringStruct('InternalName', 'FtpServer'),
StringStruct('LegalCopyright', 'Copyright (C) 2024'),
StringStruct('OriginalFilename', 'FtpServer.exe'),
StringStruct('ProductName', 'FtpServer'),
StringStruct('ProductVersion', '1.15.0.0')])
StringStruct('ProductVersion', '1.16.0.0')])
]),
VarFileInfo([VarStruct('Translation', [2052, 1200])])
]
Expand Down
47 changes: 39 additions & 8 deletions ftpServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@

from tkinter import ttk, scrolledtext, filedialog, messagebox
from mypyftpdlib.authorizers import DummyAuthorizer
from mypyftpdlib.handlers import FTPHandler
from mypyftpdlib.handlers import FTPHandler, TLS_FTPHandler
from mypyftpdlib.servers import ThreadedFTPServer
from PIL import ImageTk, Image
from io import BytesIO
from functools import reduce

# pip install Pillow pypiwin32 pyinstaller nuitka pystray pyopenssl pyasynchat

# 在终端中生成SSL证书 (ftpServer.key, ftpServer.crt 有效期100年) 放到程序所在目录则自动启用 FTPS [TLS/SSL显式加密, TLSv1.3]
# $> openssl req -x509 -newkey rsa:2048 -keyout ftpServer.key -out ftpServer.crt -nodes -days 36500

# 打包 单文件 隐藏终端窗口
# pyinstaller.exe -F -w .\ftpServer.py -i .\ftpServer.ico --version-file .\file_version_info.txt
# pyinstaller.exe .\ftpServer.spec
Expand All @@ -31,7 +34,7 @@

appName = "FTP Server"
appLabel = "FTP文件服务器"
appVersion = "v1.15"
appVersion = "v1.16"
appAuthor = "Github@JARK006"
githubLink = "https://github.com/jark006/FtpServer"
windowsTitle = f"{appLabel} {appVersion} By {appAuthor}"
Expand All @@ -48,6 +51,8 @@
isIPv4ThreadRunning: bool = False
isIPv6ThreadRunning: bool = False

certFilePath = os.path.join(os.path.dirname(sys.argv[0]), "ftpServer.crt")
keyFilePath = os.path.join(os.path.dirname(sys.argv[0]), "ftpServer.key")

def updateSettingVars():
global settings
Expand Down Expand Up @@ -245,6 +250,8 @@ def startServer():
def serverThreadFunV4():
global serverV4
global isIPv4ThreadRunning
global certFilePath
global keyFilePath

authorizer = DummyAuthorizer()

Expand All @@ -270,7 +277,16 @@ def serverThreadFunV4():
perm=userItem.perm,
)

handler = FTPHandler
if os.path.exists(certFilePath) and os.path.exists(keyFilePath):
handler = TLS_FTPHandler
handler.certfile = certFilePath
handler.keyfile = keyFilePath
handler.tls_control_required = True
handler.tls_data_required = True
print("[FTP IPv4] 已加载 SSL 证书文件,默认开启 FTPS [TLS/SSL显式加密, TLSv1.3]")
else:
handler = FTPHandler

handler.authorizer = authorizer
handler.encoding = "gbk" if settings.isGBK else "utf8"
serverV4 = ThreadedFTPServer(("0.0.0.0", settings.IPv4Port), handler)
Expand All @@ -284,6 +300,8 @@ def serverThreadFunV4():
def serverThreadFunV6():
global serverV6
global isIPv6ThreadRunning
global certFilePath
global keyFilePath

authorizer = DummyAuthorizer()

Expand All @@ -309,7 +327,17 @@ def serverThreadFunV6():
perm=userItem.perm,
)

handler = FTPHandler
# handler = FTPHandler
if os.path.exists(certFilePath) and os.path.exists(keyFilePath):
handler = TLS_FTPHandler
handler.certfile = certFilePath
handler.keyfile = keyFilePath
handler.tls_control_required = True
handler.tls_data_required = True
print("[FTP IPv6] 已加载 SSL 证书文件,默认开启 FTPS [TLS/SSL显式加密, TLSv1.3]")
else:
handler = FTPHandler

handler.authorizer = authorizer
handler.encoding = "gbk" if settings.isGBK else "utf8"
serverV6 = ThreadedFTPServer(("::", settings.IPv6Port), handler)
Expand Down Expand Up @@ -546,15 +574,15 @@ def scale(n: int) -> int:
)
userNameVar = tkinter.StringVar()
userNameEntry = ttk.Entry(window, textvariable=userNameVar, width=scale(12))
userNameEntry.place(
x=scale(40), y=scale(40), width=scale(150), height=scale(25)
)
userNameEntry.place(x=scale(40), y=scale(40), width=scale(150), height=scale(25))

ttk.Label(window, text="密码").place(
x=scale(10), y=scale(70), width=scale(30), height=scale(25)
)
userPasswordVar = tkinter.StringVar()
userPasswordEntry = ttk.Entry(window, textvariable=userPasswordVar, width=scale(12), show="*")
userPasswordEntry = ttk.Entry(
window, textvariable=userPasswordVar, width=scale(12), show="*"
)
userPasswordEntry.place(
x=scale(40), y=scale(70), width=scale(150), height=scale(25)
)
Expand Down Expand Up @@ -649,6 +677,9 @@ def popup(event: tkinter.Event):
startButton.invoke()
window.withdraw()

if os.path.exists(certFilePath) and os.path.exists(keyFilePath):
print("检测到 SSL 证书文件, 默认使用 FTPS [TLS/SSL显式加密, TLSv1.3]")

window.mainloop()


Expand Down
3 changes: 1 addition & 2 deletions mypyftpdlib/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@
except ImportError:
pwd = grp = None

# TODO disable for now
# try:
# from OpenSSL import SSL # requires "pip install pyopenssl"
# except ImportError:
# SSL = None
SSL = None
from OpenSSL import SSL

from . import __ver__
from .authorizers import AuthenticationFailed
Expand Down

0 comments on commit e587958

Please sign in to comment.