Skip to content

Commit 26d4546

Browse files
authored
Initial upload
1 parent 9c47d01 commit 26d4546

7 files changed

+869
-0
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
`22 MAR 2020`
2+
Published version 1.0.
3+
4+
`23 MAR 2020`
5+
Commented the code extensively
6+
Wrote the README

Interface_1.png

19.8 KB
Loading

Interface_2.png

27.6 KB
Loading

Interface_3.png

30.2 KB
Loading

README.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<img src="ssftps.svg" width=128>
2+
#Stupidly Simple FTP Server
3+
4+
##What is it?
5+
This python script is based on Giampaolo Rodola's pyftplib Library and aims to
6+
bring this program to anybody whithout the need for a command line nor python knowledge.
7+
It is written on top of the GTK3 Toolkit running consequently on any major distro.
8+
9+
##Requisites:
10+
On any ubuntu-ish distro, you may install the library with the following command line:
11+
```
12+
sudo apt install python3-pyftpdlib
13+
```
14+
15+
##How does it work?
16+
When you open the program, you are presented with this screen:
17+
![](Interface_1.png)
18+
19+
If you just click the switch, the server will start with it's default values:
20+
![](Interface_3.png)
21+
22+
These values are:
23+
24+
* Port: 2121
25+
* User: anonymous
26+
* Pass: none
27+
* Dir : . (that is the local directory the program is run from)
28+
29+
The anonymous user has no write permissions though. This may be fine with you, but if you want to be able to explore your filesystem, upload and change files, you have to add a user. To accomplish this, just push the config button prior to starting the server:
30+
31+
![](Interface_2.png)
32+
33+
Go ahead and change the values to suit you needs. The values are stored, as soon as the dialog is closed, so no apply or save procedure is neccessary. Now go ahead and flick the switch and enjoy the server...
34+
35+
##Known Issues
36+
Setting the loglevel to anything higher than INFO as well as getting an ERROR,
37+
causes the program to crash with a segfault. I will have to look into that more closely.
38+
39+
##Based on
40+
`pyftplib` library by Giampaolo Rodola
41+
42+
`pyGObject` library

ssftps.py

+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
#!/usr/bin/env python
2+
# -*- coding: UTF-8 -*-
3+
4+
# Stupidly Sipmple FTP Server by D.Sánchez
5+
# This program is published under the EU-GPL, get your copy at
6+
# https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/eupl_v1.2_en.pdf
7+
# Based on the GTK3 Library in pyGObject for python and driven by pyftpdlib by
8+
# Giampaolo Rodola - https://github.com/giampaolo/pyftpdlib.
9+
# You may install the dependencies with sudo apt-get install python3-pyftpdlib
10+
11+
import gi, os, threading, logging
12+
gi.require_version('Gtk', '3.0')
13+
14+
from gi.repository import Gtk, Gio
15+
from pyftpdlib.authorizers import DummyAuthorizer
16+
from pyftpdlib.handlers import FTPHandler
17+
from pyftpdlib.servers import FTPServer
18+
19+
class FTP_Server:
20+
"""
21+
A FTP Server Class to be launched whithin a non blocking thread
22+
"""
23+
#Declare version number of the server-class
24+
cVersion = "1.0"
25+
26+
#Class constructor
27+
def __init__( self ):
28+
#Create an atuhorizer...
29+
self.cAuthorizer = DummyAuthorizer()
30+
#Create a FTPHandler ( required for FTPServer )
31+
self.cHandler = FTPHandler
32+
#Create a FTPAuthorizer ( required for FTPServer )
33+
self.cHandler.authorizer = self.cAuthorizer
34+
#Establish a default banner ( optional )
35+
self.cHandler.banner = "ssftps server v" + self.cVersion + " ready."
36+
37+
def run( self ):
38+
#Is the user list empty?
39+
if not myConfig.USERS:
40+
#YES - Activate the anonymous login
41+
self.cAuthorizer.add_anonymous( os.getcwd() )
42+
else:
43+
#NO - Iterate over userlist
44+
for tmpUser in myConfig.USERS:
45+
self.cAuthorizer.add_user( tmpUser['user'], tmpUser['pass'], tmpUser['path'] )
46+
#Instantiate FTPServer
47+
self.cServer = FTPServer( ( myConfig.IPV4, myConfig.PORT ) , self.cHandler )
48+
#Run the server
49+
self.cServer.serve_forever()
50+
51+
def stop( self ):
52+
#Stop the server
53+
self.cServer.close_all()
54+
55+
def add_user( self, tmpUser,tmpPasswd, tmpPath, tmpPrivileges='elradfmwMT' ):
56+
#Add a user with all read write priveleges, unless otherwise specified
57+
self.authorizer.add_user( str( tmpUser ), str( tmpPasswd ), str( tmpPath ), perm=str( tmpPrivileges ) )
58+
"""
59+
Extract from API documentation (for quick reference)
60+
61+
Read permissions:
62+
"e" = change directory (CWD, CDUP commands)
63+
"l" = list files (LIST, NLST, STAT, MLSD, MLST, SIZE commands)
64+
"r" = retrieve file from the server (RETR command)
65+
66+
Write permissions:
67+
"a" = append data to an existing file (APPE command)
68+
"d" = delete file or directory (DELE, RMD commands)
69+
"f" = rename file or directory (RNFR, RNTO commands)
70+
"m" = create directory (MKD command)
71+
"w" = store a file to the server (STOR, STOU commands)
72+
"M" = change file mode / permission (SITE CHMOD command) New in 0.7.0
73+
"T" = change file modification time (SITE MFMT command) New in 1.5.3
74+
"""
75+
76+
class MainWindow(Gtk.Window):
77+
"""
78+
This class defines the main window of the application
79+
"""
80+
def __init__(self):
81+
#Configure Main Window
82+
Gtk.Window.__init__(self, title="ssftps")
83+
self.set_border_width( 5 )
84+
self.set_default_size( 600, 400 )
85+
#Configure a Headerbar
86+
cHeaderBar = Gtk.HeaderBar()
87+
cHeaderBar.set_show_close_button(True)
88+
cHeaderBar.props.title = "Stupidly Simple FTP Server v" + FTP_Server.cVersion
89+
self.set_titlebar(cHeaderBar)
90+
#Button for Headerbar
91+
myConfigButton = Gtk.Button()
92+
myConfigButton.props.relief = Gtk.ReliefStyle.NONE
93+
myConfigButton.add( Gtk.Image.new_from_gicon( Gio.ThemedIcon( name="emblem-system-symbolic" ), Gtk.IconSize.BUTTON ) )
94+
myConfigButton.connect( "clicked", self.configButtonClicked )
95+
#Create a switch for the headerbar
96+
onoffSwitch = Gtk.Switch()
97+
onoffSwitch.props.valign = Gtk.Align.CENTER
98+
onoffSwitch.connect( "state-set", self.onoffSwitchChanged )
99+
#Pack everything
100+
cHeaderBar.pack_end( onoffSwitch )
101+
cHeaderBar.pack_end( myConfigButton )
102+
#Create a scrollable container for the TextView
103+
myScroller = Gtk.ScrolledWindow()
104+
myScroller.set_border_width( 2 )
105+
myScroller.set_policy( Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC )
106+
#Create a Textview
107+
myTextView = Gtk.TextView()
108+
myTextView.set_editable( False )
109+
#Declare a class level TextBuffer to log to.
110+
self.myTextBuffer = myTextView.get_buffer()
111+
#Put it all into our scrollable container
112+
myScroller.add( myTextView )
113+
self.add( myScroller )
114+
#Create all entry boxes for the port, username, password and directory
115+
self.cEntryPORT = Gtk.Entry()
116+
self.cEntryPORT.set_text( myConfig.PORT )
117+
self.cEntryUSER = Gtk.Entry()
118+
self.cEntryUSER.set_text( 'anonymous' )
119+
self.cEntryPASS = Gtk.Entry()
120+
self.cEntryPASS.set_text( '' )
121+
self.cEntryPASS.set_visibility( False ) #This will make it a password input
122+
self.cEntryPATH = Gtk.Entry()
123+
self.cEntryPATH.set_text( '.' )
124+
self.cEntryPATH.set_editable( False ) #We only want to permit walid paths, so we just make it non-editable
125+
self.cEntryPATH.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "folder");
126+
self.cEntryPATH.connect( "icon-press", self.openFileDialogFromPopover )
127+
#Create a Popover
128+
self.cPopover = Gtk.Popover()
129+
self.cPopover.set_border_width( 10 )
130+
#Put all element inside a Box
131+
verticalBox = Gtk.Box( orientation=Gtk.Orientation.VERTICAL )
132+
verticalBox.pack_start( Gtk.Label("Port"), False, True, 3 )
133+
verticalBox.pack_start( self.cEntryPORT, False, True, 3 )
134+
verticalBox.pack_start( Gtk.Label("Username"), False, True, 3 )
135+
verticalBox.pack_start( self.cEntryUSER, False, True, 10 )
136+
verticalBox.pack_start( Gtk.Label("Password"), False, True, 3 )
137+
verticalBox.pack_start( self.cEntryPASS, False, True, 3 )
138+
verticalBox.pack_start( Gtk.Label("Path"), False, True, 3 )
139+
verticalBox.pack_start( self.cEntryPATH, False, True, 3 )
140+
#Pack the boxk into out Popover
141+
self.cPopover.add( verticalBox )
142+
self.cPopover.connect( "closed", self.popoverClosed )
143+
self.cPopover.set_position(Gtk.PositionType.BOTTOM)
144+
145+
def openFileDialogFromPopover( self, widget, icon_pos, event ):
146+
#Define the dialog to be opened
147+
myFileDialog = Gtk.FileChooserDialog("Select a root directory for the FTP server",
148+
self,
149+
Gtk.FileChooserAction.SELECT_FOLDER,
150+
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
151+
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
152+
#Launch the dialog and capture it's output
153+
tmpResponse = myFileDialog.run()
154+
#Did the user accept the dialog?
155+
if tmpResponse == Gtk.ResponseType.OK:
156+
#YES - Write it to the entry box
157+
self.cEntryPATH.set_text( str( myFileDialog.get_filename() ) )
158+
#Destroy the dialog
159+
myFileDialog.destroy()
160+
161+
def popoverClosed( self, widget ):
162+
#When the popover closes, all values are stored into the config
163+
#Has the username been changed?
164+
if self.cEntryUSER.get_text() != 'anonymous':
165+
#YES - Add the new user to the USERS list
166+
myConfig.USERS.append( {'user': self.cEntryUSER.get_text() , 'pass': self.cEntryPASS.get_text(), 'path': self.cEntryPATH.get_text()} )
167+
#Has the port value been changed?
168+
if self.cEntryPORT.get_text()!=myConfig.PORT:
169+
#YES - Save the new port (no error checking is done here)
170+
myConfig.PORT = self.cEntryPORT.get_text()
171+
172+
def logToTextBuffer( self, tmpMessage ):
173+
#Insert the logged text into the local textbuffer
174+
self.myTextBuffer.insert_at_cursor( tmpMessage+"\n" )
175+
176+
def configButtonClicked( self, widget ):
177+
#Position the popover
178+
self.cPopover.set_relative_to( widget )
179+
#Show it...
180+
self.cPopover.show_all()
181+
self.cPopover.popup()
182+
183+
def onoffSwitchChanged( self, widget, state ):
184+
#Is the server to be switchen on?
185+
if state==True:
186+
#YES - Instantiate the server
187+
self.cServer = FTP_Server()
188+
#Put it into a thread and configure it...
189+
self.cThread = threading.Thread( target=self.cServer.run )
190+
#... as a daemon
191+
self.cThread.daemon = True
192+
#Start the server-thread
193+
self.cThread.start()
194+
else:
195+
#NO - Inrom the user that we are stopping the server
196+
self.myTextBuffer.insert_at_cursor( ">>> stopping FTP server on "+myConfig.IPV4+":"+myConfig.PORT+" <<<\n" )
197+
#Stop it!
198+
self.cServer.stop()
199+
200+
class LogHandler(logging.Handler):
201+
"""
202+
This is a redefined class from logging.Handler with the emit method redefined.
203+
"""
204+
def __init__(self, tmpTextBuffer ):
205+
#Initialize the superclass
206+
super(LogHandler, self).__init__()
207+
#Capture the TextBuffer
208+
self.cTextBuffer = tmpTextBuffer
209+
210+
def emit( self, tmpMessage ):
211+
#Store the formattet message
212+
tmpMessage = self.format( tmpMessage )
213+
#Send the log message to the textbuffer
214+
self.cTextBuffer.insert_at_cursor( tmpMessage + "\n" )
215+
216+
class ServerConfiguration():
217+
"""
218+
This class holds the server configuration
219+
"""
220+
def __init__(self):
221+
#This reads the private IP address from all interfaces and chooses the first one (this should work in most cases)
222+
self.IPV4 = os.popen('hostname --all-ip-addresses | cut -d " " -f1').read().rstrip("\n\r")
223+
#We define a default port which is available to the user (port 21 can't be listened on by the unprivileged user)
224+
self.PORT = "2121"
225+
#Default loglevel. Please be aware that for some reason a higher loglevel than this will result in a segfault)
226+
self.LOGLEVEL = logging.INFO
227+
#Although only one user van be defined through the interface, you may add more "default users here"
228+
#This is a list containing a disctionary with the following items: {'user':'','pass':'','path':}
229+
self.USERS = []
230+
231+
232+
#Declare the configuration container as a global variable (not elegant, but the only way I found)
233+
global myConfig
234+
235+
#Create configuration Class
236+
myConfig = ServerConfiguration()
237+
238+
#Create Main Window
239+
myWindow = MainWindow()
240+
241+
#Connect the destroy signal to the main loop quit function
242+
myWindow.connect("destroy", Gtk.main_quit)
243+
244+
#Print the welcome message
245+
myWindow.myTextBuffer.set_text( "================\n" + " ssftp Version " + FTP_Server.cVersion + "\n================\nWritten by D.Sanchez and published under the EU-GPL\n" )
246+
247+
#Configure log level
248+
logging.basicConfig(level=myConfig.LOGLEVEL)
249+
250+
#Show Windows on screen
251+
myWindow.show_all()
252+
253+
#Create a handler to control the log-message output
254+
myHandler = LogHandler( myWindow.myTextBuffer )
255+
256+
#Link the handler to default logger
257+
logging.getLogger('pyftpdlib').addHandler(myHandler)
258+
259+
#Main Loop
260+
Gtk.main()

0 commit comments

Comments
 (0)