├── .github └── workflows │ ├── python-package.yml │ └── python-publish.yml ├── .gitignore ├── .travis.yml ├── MANIFEST.in ├── README.md ├── doc ├── ARCHITECTURE ├── Changes ├── INSTALL ├── LICENSE ├── TODO ├── interface_class └── walker ├── pywebdav ├── __init__.py ├── __main__.py ├── lib │ ├── AuthServer.py │ ├── INI_Parse.py │ ├── WebDAVServer.py │ ├── __init__.py │ ├── constants.py │ ├── davcmd.py │ ├── davcopy.py │ ├── davmove.py │ ├── dbconn.py │ ├── delete.py │ ├── errors.py │ ├── iface.py │ ├── locks.py │ ├── propfind.py │ ├── report.py │ ├── status.py │ └── utils.py └── server │ ├── __init__.py │ ├── config.ini │ ├── daemonize.py │ ├── fileauth.py │ ├── fshandler.py │ ├── mysqlauth.py │ └── server.py ├── setup.py └── test ├── .gitignore ├── litmus-0.13.tar.gz └── test_litmus.py /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies and run tests with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.8", "3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip wheel build twine setuptools 30 | python -m pip install . 31 | 32 | - name: Test package format 33 | run: | 34 | python -m build 35 | twine check dist/* 36 | 37 | - name: Test with unittest 38 | run: | 39 | python test/test_litmus.py 40 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v3 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install build 25 | - name: Build package 26 | run: python -m build 27 | - name: Publish package 28 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 29 | with: 30 | user: __token__ 31 | password: ${{ secrets.PYPI_API_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | out 5 | gen### Python template 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | *.py.bak 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # dotenv 59 | .env 60 | 61 | # virtualenv 62 | venv/ 63 | ENV/ 64 | 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | # install dependencies 7 | install: "pip install ." 8 | # run tests 9 | script: py.test -v 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py README.md 2 | include doc/* 3 | include test/* 4 | include pywebdav/lib/*.py pywebdav/server/*.ini 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyWebDAV3 2 | --------- 3 | 4 | PyWebDAV is a standards compliant WebDAV server and library written in Python 5 | 6 | PyWebDAV3 is an updated distribution for python 3 support. 7 | 8 | Python WebDAV implementation (level 1 and 2) that features a library that enables you 9 | to integrate WebDAV server capabilities to your application 10 | 11 | A fully working example on how to use the library is included. You can find the server in the DAVServer package. Upon installation a script called davserver is created in your $PYTHON/bin directory. 12 | 13 | DETAILS 14 | ------- 15 | 16 | Consists of a *server* that is ready to run 17 | Serve and the DAV package that provides WebDAV server(!) functionality. 18 | 19 | Currently supports 20 | 21 | * WebDAV level 1 22 | * Level 2 (LOCK, UNLOCK) 23 | * Experimental iterator support 24 | 25 | It plays nice with 26 | 27 | * Mac OS X Finder 28 | * Windows Explorer 29 | * iCal 30 | * cadaver 31 | * Nautilus 32 | 33 | This package does *not* provide client functionality. 34 | 35 | INSTALLATION 36 | ------------ 37 | 38 | Installation and setup of server can be as easy as follows: 39 | 40 | ```sh 41 | pip install PyWebDAV3 42 | davserver -D /tmp -n -J 43 | ``` 44 | 45 | After installation of this package you will have a new script in you 46 | $PYTHON/bin directory called *davserver*. This serves as the main entry point 47 | to the server. 48 | 49 | If you're living on the bleeding edge then check out the sourcecode from 50 | https://github.com/andrewleech/PyWebDAV3 51 | 52 | After having downloaded code simply install a development egg: 53 | 54 | ```sh 55 | git clone https://github.com/andrewleech/PyWebDAV3 56 | cd PyWebDAV3 57 | python setup.py develop 58 | davserver --help 59 | ``` 60 | 61 | Any updates, fork and pull requests against my github page 62 | 63 | If you want to use the library then have a look at the DAVServer package that 64 | holds all code for a full blown server. Also doc/ARCHITECURE has information for you. 65 | 66 | 67 | QUESTIONS? 68 | ---------- 69 | 70 | Ask here https://github.com/andrewleech/PyWebDAV3 71 | or send an email to the maintainer. 72 | 73 | 74 | REQUIREMENTS 75 | ------------ 76 | 77 | - Python 3.5 or higher (www.python.org) 78 | - PyXML 0.66 (pyxml.sourceforge.net) 79 | 80 | 81 | LICENSE 82 | ------- 83 | 84 | General Public License v2 85 | see doc/LICENSE 86 | 87 | 88 | AUTHOR(s) 89 | --------- 90 | 91 | Andrew Leech [*] 92 | Melbourne, Australia 93 | andrew@alelec.net 94 | 95 | Simon Pamies 96 | Bielefeld, Germany 97 | s.pamies@banality.de 98 | 99 | Christian Scholz 100 | Aachen, Germany 101 | mrtopf@webdav.de 102 | 103 | Vince Spicer 104 | Ontario, Canada 105 | vince@vince.ca 106 | 107 | [*]: Current Maintainer 108 | 109 | 110 | OPTIONAL 111 | -------- 112 | 113 | - MySQLdb (http://sourceforge.net/projects/mysql-python) 114 | - Mysql server 4.0+ for Mysql authentication with 115 | with read/write access to one database 116 | 117 | 118 | NOTES 119 | ----- 120 | 121 | Look inside the file doc/TODO for things which needs to be done and may be done 122 | in the near future. 123 | 124 | Have a look at doc/ARCHITECTURE to understand what's going on under the hood 125 | -------------------------------------------------------------------------------- /doc/ARCHITECTURE: -------------------------------------------------------------------------------- 1 | 2 | OVERVIEW 3 | -------- 4 | 5 | Here is a little overview of the package: 6 | 7 | A. In the pywebdav/lib/ package: 8 | 9 | 1. AuthHTTPServer 10 | This works on top of either the BasicHTTPServer or the 11 | BufferingHTTPServer and implements basic authentication. 12 | 13 | 2. WebDAVServer 14 | This server uses AuthHTTPServer for the base functionality. It also uses 15 | a dav interface class for interfacing with the actual data storage (e.g. 16 | a filesystem or a database). 17 | 18 | B. In the pywebdav/server/ directory: 19 | 20 | 1. server.py 21 | Main file for server. Serves as a start point for the WebDAV server 22 | 23 | 2. fshandler.py 24 | Backend for the DAV server. Makes him serving content from the filesystem. 25 | Have a deeper look at it if you want to implement backends to other data sources 26 | 27 | 3. fileauth.py 28 | Handler for authentication. Nothing very special about it. 29 | 30 | 4. dbconn.py 31 | Mysql Database Handler. 32 | 33 | 5. INI_Parse.py 34 | Parses the config.ini file. 35 | 36 | 6. config.ini 37 | PyWebDav configuration file. 38 | 39 | 40 | Information 41 | ---------- 42 | 43 | This document describes the architecture of the python davserver. 44 | 45 | The main programm is stored in pywebdav/lib/WebDAVServer.py. It exports a class 46 | WebDAVServer which is subclassed from AuthServer. 47 | 48 | The AuthServer class implements Basic Authentication in order to make 49 | connections somewhat more secure. 50 | 51 | For processing requests the WebDAVServer class needs some connection to 52 | the actual data on server. In contrast to a normal web server this 53 | data must not simply be stored on a filesystem but can also live in 54 | databases and the like. 55 | 56 | Thus the WebDAVServer class needs an interface 57 | to this data which is implemented via an interface class (in our 58 | example stored in fshandler.py). This class will be instantiated by the 59 | WebDAVServer class and be used when needed (e.g. retrieving properties, 60 | creating new resources, obtaining existing resources etc.). 61 | 62 | When it comes to parsing XML (like in the PROPFIND and PROPPATCH 63 | methods) the WebDAVServer class uses for each method another extra class 64 | stored e.g. in propfind.py. This class parses the XML body and 65 | createsan XML response while obtaining data from the interface 66 | class. Thus all the XML parsing is factored out into the specific 67 | method classes. 68 | 69 | In order to create your own davserver for your own purposes you have to do the 70 | following: 71 | 72 | - subclass the WebDAVServer class and write your own get_userinfo() method for 73 | identifying users. 74 | 75 | - create your own interface class for interfacing with your actual data. 76 | You might use the existing class as skeleton and explanation of which 77 | methods are needed. 78 | 79 | That should be basically all you need to do. Have a look at 80 | pywebdav/server/fileauth.py in order to get an example how to subclass 81 | WebDAVServer. 82 | 83 | === 84 | * describe the methods which need to be implemented. 85 | -------------------------------------------------------------------------------- /doc/Changes: -------------------------------------------------------------------------------- 1 | 0.9.14 (September 3 2019) 2 | ------------------------- 3 | Include tests dir in release pack to unblock debian packaging tests. 4 | 5 | 0.9.13 (August 15 2019) 6 | ----------------------- 7 | Fix responed data type error 8 | Fix exception when trying to list directory over WebDAV 9 | Don't send Content-Type header twice 10 | Fix python2 support 11 | Don't decode binary data on put 12 | 13 | 0.9.12 (October 29 2017) 14 | ------------------------ 15 | Allow running davserver as PyWebDAV server 16 | 17 | 0.9.11 (August 3 2016) 18 | ---------------------- 19 | Added unit test to run litmus webdav testing suite. 20 | Added travis-ci job to run said test, but it has false positives. Runs locally correctly. 21 | Fixed a number of unicode issues from the python3 translation identified by the unit test. 22 | Still a number of litmus tests failing to do with props and file locking. 23 | 24 | 0.9.10 (July 21 2016) 25 | --------------------- 26 | 27 | The original package name PyWebDAV was being referenced in lib, causing the server to fail 28 | No other change in functionality 29 | [Andrew Leech] 30 | 31 | 0.9.9 (July 8 2016) 32 | ------------------- 33 | 34 | Updated to support Python 3. 35 | No other change in functionality 36 | [Andrew Leech] 37 | 38 | 0.9.8 (March 25 2011) 39 | --------------------- 40 | 41 | Restructured. Moved DAV package to pywebdav.lib. All integrators must simply replace 42 | ''from DAV'' imports to ''from pywebdav.lib''. 43 | [Simon Pamies] 44 | 45 | Remove BufferingHTTPServer, reuse the header parser of BaseHTTPServer. 46 | [Cédric Krier] 47 | 48 | Fix issue 44: Incomplete PROPFIND response 49 | [Sascha Silbe] 50 | 51 | 0.9.4 (April 15 2010) 52 | --------------------- 53 | 54 | Add somme configuration setting variable to enable/disable iterator and chunk support 55 | [Stephane Klein] 56 | 57 | Removed os.system calls thus fixing issue 32 58 | [Simon Pamies] 59 | 60 | Fixed issue 14 61 | [Simon Pamies] 62 | 63 | Removed magic.py module - replaced with mimetypes module 64 | [Simon Pamies] 65 | 66 | Print User-Agent information in log request. 67 | [Stephane Klein] 68 | 69 | Fix issue 13 : return http 1.0 compatible response (not chunked) when request http version is 1.0 70 | [cliff.wells] 71 | 72 | Enhance logging mechanism 73 | [Stephane Klein] 74 | 75 | Fix issue 15 : I've error when I execute PUT action with Apple Finder client 76 | [Stephane Klein] 77 | 78 | Fix issue 14 : config.ini boolean parameter reading issue 79 | [Stephane Klein] 80 | 81 | 0.9.3 (July 2 2009) 82 | ------------------- 83 | 84 | Setting WebDAV v2 as default because LOCK and UNLOCK seem 85 | to be stable by now. -J parameter is ignored and will go away. 86 | [Simon Pamies] 87 | 88 | Fix for PROPFIND to return *all* properties 89 | [Cedric Krier] 90 | 91 | Fixed do_PUT initialisation 92 | [Cedric Krier] 93 | 94 | Added REPORT support 95 | [Cedric Krier] 96 | 97 | Added support for gzip encoding 98 | [Cedric Krier] 99 | 100 | Fix for wrong --port option 101 | [Martin Wendt] 102 | 103 | Handle paths correctly for Windows related env 104 | [Martin Wendt] 105 | 106 | Included mimetype check for files 107 | based on magic.py from Jason Petrone. Included 108 | magic.py into this package. All magic.py code 109 | (c) 2000 Jason Petrone. Included from 110 | http://www.jsnp.net/code/magic.py. 111 | [Joerg Friedrich, Simon Pamies] 112 | 113 | Status check not working when server is running 114 | [Joerg Friedrich] 115 | 116 | Fixed wrong time formatting for Last-Modified 117 | and creationdate (must follow RFC 822 and 3339) 118 | [Cedric Krier] 119 | 120 | 0.9.2 (May 11 2009) 121 | ------------------- 122 | 123 | Fixed COPY, MOVE, DELETE to support locked 124 | resources 125 | [Simon Pamies] 126 | 127 | Fixed PROPFIND to return 404 for non existing 128 | objects and also reduce property bloat 129 | [Simon Pamies] 130 | 131 | Implemented fully working LOCK and UNLOCK based 132 | on in memory lock/token database. Now fully supports 133 | cadaver and Mac OS X Finder. 134 | [Simon Pamies] 135 | 136 | Fixed MKCOL answer to 201 137 | [Jesus Cea] 138 | 139 | Fixed MSIE webdav headers 140 | [Jesus Cea] 141 | 142 | Make propfind respect the depth from queries 143 | [Cedric Krier] 144 | 145 | Add ETag in the header of GET. This is needed to implement 146 | GroupDAV, CardDAV and CalDAV. 147 | [Cedric Krier] 148 | 149 | Handle the "Expect 100-continue" header 150 | [Cedric Krier] 151 | 152 | Remove debug statements and remove logging 153 | [Cedric Krier] 154 | 155 | Use the Host header in baseuri if set. 156 | [Cedric Krier] 157 | 158 | Adding If-Match on PUT and DELETE 159 | [Cedric Krier] 160 | 161 | 0.9.1 (May 4th 2009) 162 | -------------------- 163 | 164 | Restructured the structure a bit: Made server package 165 | a real python package. Adapted error messages. Prepared 166 | egg distribution. 167 | [Simon Pamies] 168 | 169 | Fix for time formatting bug. Thanks to Ian Kallen 170 | [Simon Pamies] 171 | 172 | Small fixes for WebDavServer (status not handled correctly) and 173 | propfind (children are returned from a PROPFIND with "Depth: 0") 174 | [Kjetil Irbekk] 175 | 176 | 0.8 (Jul 15th 2008) 177 | ------------------- 178 | 179 | First try of an implementation of the LOCK and UNLOCK features. 180 | Still very incomplete (read: very incomplete) and not working 181 | in this version. 182 | [Simon Pamies] 183 | 184 | Some code cleanups to prepare restructuring 185 | [Simon Pamies] 186 | 187 | Port to minidom because PyXML isn't longer maintained 188 | [Martin v. Loewis] 189 | 190 | utils.py: Makes use of DOMImplementation class to create a new xml document 191 | Uses dom namespace features to create elements within DAV: namespace 192 | [Stephane Bonhomme] 193 | 194 | davcmd.py: Missing an indent in loop on remove and copy operations on trees, the 195 | effect was that only the last object was removed/copied : always leads 196 | to a failure when copying collections. 197 | [Stephane Bonhomme] 198 | 199 | propfind.py: missing a return at the end of the createResponse method (case of a 200 | propfind without xml body, should act as a allprops). 201 | [Stephane Bonhomme] 202 | 203 | 0.7 204 | --- 205 | 206 | Added MySQL auth support brought by Vince Spicer 207 | Added INI file support also introduced by Vince 208 | Some minor bugfixes and integration changes. 209 | Added instance counter to make multiple instances possible 210 | Extended --help text a bit 211 | [Simon Pamies] 212 | 213 | 0.6 214 | --- 215 | 216 | Added bugfixes for buggy Mac OS X Finder implementation 217 | Finder tries to stat .DS_Store without checking if it exists 218 | Cleaned up readme and install files 219 | Moved license to extra file 220 | Added distutils support 221 | Refactored module layout 222 | Refactored class and module names 223 | Added commandline support 224 | Added daemonize support 225 | Added logging facilities 226 | Added extended arguments 227 | 228 | some more things I can't remember 229 | [Simon Pamies] 230 | 231 | Changes since 0.5.1 232 | ------------------- 233 | Updated to work with latest 4Suite 234 | 235 | Changes since 0.5 236 | ----------------- 237 | 238 | added constants.py 239 | data.py must now return COLLECTION or OBJECT when getting asked for 240 | resourcetype. propfind.py will automatically generate the right xml 241 | element. 242 | now only contains the path 243 | changed HTTP/1.0 header to HTTP/1.1 which makes it work with WebFolders 244 | added DO_AUTH constant to AuthServer.py to control whether authentication 245 | should be done or not. 246 | added chunked responses in davserver.py 247 | One step in order to get a server with keep-alive one day. 248 | we now use 4DOM instead if PyDOM 249 | the URI in a href is quoted 250 | complete rewrite of the PROPFIND stuff: 251 | error responses are now generated when a property if not found or not accessible 252 | namespace handling is now better. We forget any prefix and create them ourselves later in the response. 253 | added superclass iface.py in DAV/ in order to make implementing 254 | interface classes easier. See data.py for how to use it. 255 | Also note that the way data.py handles things might have changed from 256 | the previous release (if you don't like it wait for 1.0!) 257 | added functions to iface.py which format creationdate and lastmodified 258 | implemented HEAD 259 | 260 | lots of bugfixes 261 | 262 | Changes since 0.3 263 | ----------------- 264 | 265 | removed hard coded base uri from davserver.py and replaced by 266 | a reference to the dataclass. Added this to iface.py where you 267 | have to define it in your subclass. 268 | added davcmd.py which contains utility functions for copy and move 269 | reimplemented DELETE and removed dependencies to pydom. move actual 270 | delete method to davcmd. 271 | implemented COPY 272 | implemented MOVE 273 | fixed bugs in errors.py, needs revisiting anyway.. 274 | URIs are now unquoted in davserver.py before being used 275 | paths in data.py are quoted in system calls in order to support 276 | blanks in pathnames (e.g. mkdir '%s' ) 277 | switched to exceptions when catching errors from the interface class 278 | added exists() method to data.py 279 | added more uri utility functions to utils.py 280 | millenium bugfixes ;-) 281 | -------------------------------------------------------------------------------- /doc/INSTALL: -------------------------------------------------------------------------------- 1 | How to install python WebDAV server 2 | ------------------------------- 3 | 4 | 1. Check prerequisites 5 | 6 | + *nix OS (including Mac OS X) 7 | Windows seems to work 8 | 9 | + Python >=2.4.x 10 | 11 | 2. Run setup.py 12 | 13 | $ python setup.py install 14 | 15 | 4. Change to the PyDAVServer directory and start server with 16 | 17 | > ./server.py -h 18 | 19 | 5. Enjoy 20 | 21 | Please send bugs and feature requests to 22 | s.pamies@banality.de 23 | -------------------------------------------------------------------------------- /doc/LICENSE: -------------------------------------------------------------------------------- 1 | GNU LIBRARY GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1991 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the library GPL. It is 10 | numbered 2 because it goes with version 2 of the ordinary GPL.] 11 | 12 | Preamble 13 | 14 | The licenses for most software are designed to take away your 15 | freedom to share and change it. By contrast, the GNU General Public 16 | Licenses are intended to guarantee your freedom to share and change 17 | free software--to make sure the software is free for all its users. 18 | 19 | This license, the Library General Public License, applies to some 20 | specially designated Free Software Foundation software, and to any 21 | other libraries whose authors decide to use it. You can use it for 22 | your libraries, too. 23 | 24 | When we speak of free software, we are referring to freedom, not 25 | price. Our General Public Licenses are designed to make sure that you 26 | have the freedom to distribute copies of free software (and charge for 27 | this service if you wish), that you receive source code or can get it 28 | if you want it, that you can change the software or use pieces of it 29 | in new free programs; and that you know you can do these things. 30 | 31 | To protect your rights, we need to make restrictions that forbid 32 | anyone to deny you these rights or to ask you to surrender the rights. 33 | These restrictions translate to certain responsibilities for you if 34 | you distribute copies of the library, or if you modify it. 35 | 36 | For example, if you distribute copies of the library, whether gratis 37 | or for a fee, you must give the recipients all the rights that we gave 38 | you. You must make sure that they, too, receive or can get the source 39 | code. If you link a program with the library, you must provide 40 | complete object files to the recipients so that they can relink them 41 | with the library, after making changes to the library and recompiling 42 | it. And you must show them these terms so they know their rights. 43 | 44 | Our method of protecting your rights has two steps: (1) copyright 45 | the library, and (2) offer you this license which gives you legal 46 | permission to copy, distribute and/or modify the library. 47 | 48 | Also, for each distributor's protection, we want to make certain 49 | that everyone understands that there is no warranty for this free 50 | library. If the library is modified by someone else and passed on, we 51 | want its recipients to know that what they have is not the original 52 | version, so that any problems introduced by others will not reflect on 53 | the original authors' reputations. 54 | 55 | Finally, any free program is threatened constantly by software 56 | patents. We wish to avoid the danger that companies distributing free 57 | software will individually obtain patent licenses, thus in effect 58 | transforming the program into proprietary software. To prevent this, 59 | we have made it clear that any patent must be licensed for everyone's 60 | free use or not licensed at all. 61 | 62 | Most GNU software, including some libraries, is covered by the ordinary 63 | GNU General Public License, which was designed for utility programs. This 64 | license, the GNU Library General Public License, applies to certain 65 | designated libraries. This license is quite different from the ordinary 66 | one; be sure to read it in full, and don't assume that anything in it is 67 | the same as in the ordinary license. 68 | 69 | The reason we have a separate public license for some libraries is that 70 | they blur the distinction we usually make between modifying or adding to a 71 | program and simply using it. Linking a program with a library, without 72 | changing the library, is in some sense simply using the library, and is 73 | analogous to running a utility program or application program. However, in 74 | a textual and legal sense, the linked executable is a combined work, a 75 | derivative of the original library, and the ordinary General Public License 76 | treats it as such. 77 | 78 | Because of this blurred distinction, using the ordinary General 79 | Public License for libraries did not effectively promote software 80 | sharing, because most developers did not use the libraries. We 81 | concluded that weaker conditions might promote sharing better. 82 | 83 | However, unrestricted linking of non-free programs would deprive the 84 | users of those programs of all benefit from the free status of the 85 | libraries themselves. This Library General Public License is intended to 86 | permit developers of non-free programs to use free libraries, while 87 | preserving your freedom as a user of such programs to change the free 88 | libraries that are incorporated in them. (We have not seen how to achieve 89 | this as regards changes in header files, but we have achieved it as regards 90 | changes in the actual functions of the Library.) The hope is that this 91 | will lead to faster development of free libraries. 92 | 93 | The precise terms and conditions for copying, distribution and 94 | modification follow. Pay close attention to the difference between a 95 | "work based on the library" and a "work that uses the library". The 96 | former contains code derived from the library, while the latter only 97 | works together with the library. 98 | 99 | Note that it is possible for a library to be covered by the ordinary 100 | General Public License rather than by this special one. 101 | 102 | GNU LIBRARY GENERAL PUBLIC LICENSE 103 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 104 | 105 | 0. This License Agreement applies to any software library which 106 | contains a notice placed by the copyright holder or other authorized 107 | party saying it may be distributed under the terms of this Library 108 | General Public License (also called "this License"). Each licensee is 109 | addressed as "you". 110 | 111 | A "library" means a collection of software functions and/or data 112 | prepared so as to be conveniently linked with application programs 113 | (which use some of those functions and data) to form executables. 114 | 115 | The "Library", below, refers to any such software library or work 116 | which has been distributed under these terms. A "work based on the 117 | Library" means either the Library or any derivative work under 118 | copyright law: that is to say, a work containing the Library or a 119 | portion of it, either verbatim or with modifications and/or translated 120 | straightforwardly into another language. (Hereinafter, translation is 121 | included without limitation in the term "modification".) 122 | 123 | "Source code" for a work means the preferred form of the work for 124 | making modifications to it. For a library, complete source code means 125 | all the source code for all modules it contains, plus any associated 126 | interface definition files, plus the scripts used to control compilation 127 | and installation of the library. 128 | 129 | Activities other than copying, distribution and modification are not 130 | covered by this License; they are outside its scope. The act of 131 | running a program using the Library is not restricted, and output from 132 | such a program is covered only if its contents constitute a work based 133 | on the Library (independent of the use of the Library in a tool for 134 | writing it). Whether that is true depends on what the Library does 135 | and what the program that uses the Library does. 136 | 137 | 1. You may copy and distribute verbatim copies of the Library's 138 | complete source code as you receive it, in any medium, provided that 139 | you conspicuously and appropriately publish on each copy an 140 | appropriate copyright notice and disclaimer of warranty; keep intact 141 | all the notices that refer to this License and to the absence of any 142 | warranty; and distribute a copy of this License along with the 143 | Library. 144 | 145 | You may charge a fee for the physical act of transferring a copy, 146 | and you may at your option offer warranty protection in exchange for a 147 | fee. 148 | 149 | 2. You may modify your copy or copies of the Library or any portion 150 | of it, thus forming a work based on the Library, and copy and 151 | distribute such modifications or work under the terms of Section 1 152 | above, provided that you also meet all of these conditions: 153 | 154 | a) The modified work must itself be a software library. 155 | 156 | b) You must cause the files modified to carry prominent notices 157 | stating that you changed the files and the date of any change. 158 | 159 | c) You must cause the whole of the work to be licensed at no 160 | charge to all third parties under the terms of this License. 161 | 162 | d) If a facility in the modified Library refers to a function or a 163 | table of data to be supplied by an application program that uses 164 | the facility, other than as an argument passed when the facility 165 | is invoked, then you must make a good faith effort to ensure that, 166 | in the event an application does not supply such function or 167 | table, the facility still operates, and performs whatever part of 168 | its purpose remains meaningful. 169 | 170 | (For example, a function in a library to compute square roots has 171 | a purpose that is entirely well-defined independent of the 172 | application. Therefore, Subsection 2d requires that any 173 | application-supplied function or table used by this function must 174 | be optional: if the application does not supply it, the square 175 | root function must still compute square roots.) 176 | 177 | These requirements apply to the modified work as a whole. If 178 | identifiable sections of that work are not derived from the Library, 179 | and can be reasonably considered independent and separate works in 180 | themselves, then this License, and its terms, do not apply to those 181 | sections when you distribute them as separate works. But when you 182 | distribute the same sections as part of a whole which is a work based 183 | on the Library, the distribution of the whole must be on the terms of 184 | this License, whose permissions for other licensees extend to the 185 | entire whole, and thus to each and every part regardless of who wrote 186 | it. 187 | 188 | Thus, it is not the intent of this section to claim rights or contest 189 | your rights to work written entirely by you; rather, the intent is to 190 | exercise the right to control the distribution of derivative or 191 | collective works based on the Library. 192 | 193 | In addition, mere aggregation of another work not based on the Library 194 | with the Library (or with a work based on the Library) on a volume of 195 | a storage or distribution medium does not bring the other work under 196 | the scope of this License. 197 | 198 | 3. You may opt to apply the terms of the ordinary GNU General Public 199 | License instead of this License to a given copy of the Library. To do 200 | this, you must alter all the notices that refer to this License, so 201 | that they refer to the ordinary GNU General Public License, version 2, 202 | instead of to this License. (If a newer version than version 2 of the 203 | ordinary GNU General Public License has appeared, then you can specify 204 | that version instead if you wish.) Do not make any other change in 205 | these notices. 206 | 207 | Once this change is made in a given copy, it is irreversible for 208 | that copy, so the ordinary GNU General Public License applies to all 209 | subsequent copies and derivative works made from that copy. 210 | 211 | This option is useful when you wish to copy part of the code of 212 | the Library into a program that is not a library. 213 | 214 | 4. You may copy and distribute the Library (or a portion or 215 | derivative of it, under Section 2) in object code or executable form 216 | under the terms of Sections 1 and 2 above provided that you accompany 217 | it with the complete corresponding machine-readable source code, which 218 | must be distributed under the terms of Sections 1 and 2 above on a 219 | medium customarily used for software interchange. 220 | 221 | If distribution of object code is made by offering access to copy 222 | from a designated place, then offering equivalent access to copy the 223 | source code from the same place satisfies the requirement to 224 | distribute the source code, even though third parties are not 225 | compelled to copy the source along with the object code. 226 | 227 | 5. A program that contains no derivative of any portion of the 228 | Library, but is designed to work with the Library by being compiled or 229 | linked with it, is called a "work that uses the Library". Such a 230 | work, in isolation, is not a derivative work of the Library, and 231 | therefore falls outside the scope of this License. 232 | 233 | However, linking a "work that uses the Library" with the Library 234 | creates an executable that is a derivative of the Library (because it 235 | contains portions of the Library), rather than a "work that uses the 236 | library". The executable is therefore covered by this License. 237 | Section 6 states terms for distribution of such executables. 238 | 239 | When a "work that uses the Library" uses material from a header file 240 | that is part of the Library, the object code for the work may be a 241 | derivative work of the Library even though the source code is not. 242 | Whether this is true is especially significant if the work can be 243 | linked without the Library, or if the work is itself a library. The 244 | threshold for this to be true is not precisely defined by law. 245 | 246 | If such an object file uses only numerical parameters, data 247 | structure layouts and accessors, and small macros and small inline 248 | functions (ten lines or less in length), then the use of the object 249 | file is unrestricted, regardless of whether it is legally a derivative 250 | work. (Executables containing this object code plus portions of the 251 | Library will still fall under Section 6.) 252 | 253 | Otherwise, if the work is a derivative of the Library, you may 254 | distribute the object code for the work under the terms of Section 6. 255 | Any executables containing that work also fall under Section 6, 256 | whether or not they are linked directly with the Library itself. 257 | 258 | 6. As an exception to the Sections above, you may also compile or 259 | link a "work that uses the Library" with the Library to produce a 260 | work containing portions of the Library, and distribute that work 261 | under terms of your choice, provided that the terms permit 262 | modification of the work for the customer's own use and reverse 263 | engineering for debugging such modifications. 264 | 265 | You must give prominent notice with each copy of the work that the 266 | Library is used in it and that the Library and its use are covered by 267 | this License. You must supply a copy of this License. If the work 268 | during execution displays copyright notices, you must include the 269 | copyright notice for the Library among them, as well as a reference 270 | directing the user to the copy of this License. Also, you must do one 271 | of these things: 272 | 273 | a) Accompany the work with the complete corresponding 274 | machine-readable source code for the Library including whatever 275 | changes were used in the work (which must be distributed under 276 | Sections 1 and 2 above); and, if the work is an executable linked 277 | with the Library, with the complete machine-readable "work that 278 | uses the Library", as object code and/or source code, so that the 279 | user can modify the Library and then relink to produce a modified 280 | executable containing the modified Library. (It is understood 281 | that the user who changes the contents of definitions files in the 282 | Library will not necessarily be able to recompile the application 283 | to use the modified definitions.) 284 | 285 | b) Accompany the work with a written offer, valid for at 286 | least three years, to give the same user the materials 287 | specified in Subsection 6a, above, for a charge no more 288 | than the cost of performing this distribution. 289 | 290 | c) If distribution of the work is made by offering access to copy 291 | from a designated place, offer equivalent access to copy the above 292 | specified materials from the same place. 293 | 294 | d) Verify that the user has already received a copy of these 295 | materials or that you have already sent this user a copy. 296 | 297 | For an executable, the required form of the "work that uses the 298 | Library" must include any data and utility programs needed for 299 | reproducing the executable from it. However, as a special exception, 300 | the source code distributed need not include anything that is normally 301 | distributed (in either source or binary form) with the major 302 | components (compiler, kernel, and so on) of the operating system on 303 | which the executable runs, unless that component itself accompanies 304 | the executable. 305 | 306 | It may happen that this requirement contradicts the license 307 | restrictions of other proprietary libraries that do not normally 308 | accompany the operating system. Such a contradiction means you cannot 309 | use both them and the Library together in an executable that you 310 | distribute. 311 | 312 | 7. You may place library facilities that are a work based on the 313 | Library side-by-side in a single library together with other library 314 | facilities not covered by this License, and distribute such a combined 315 | library, provided that the separate distribution of the work based on 316 | the Library and of the other library facilities is otherwise 317 | permitted, and provided that you do these two things: 318 | 319 | a) Accompany the combined library with a copy of the same work 320 | based on the Library, uncombined with any other library 321 | facilities. This must be distributed under the terms of the 322 | Sections above. 323 | 324 | b) Give prominent notice with the combined library of the fact 325 | that part of it is a work based on the Library, and explaining 326 | where to find the accompanying uncombined form of the same work. 327 | 328 | 8. You may not copy, modify, sublicense, link with, or distribute 329 | the Library except as expressly provided under this License. Any 330 | attempt otherwise to copy, modify, sublicense, link with, or 331 | distribute the Library is void, and will automatically terminate your 332 | rights under this License. However, parties who have received copies, 333 | or rights, from you under this License will not have their licenses 334 | terminated so long as such parties remain in full compliance. 335 | 336 | 9. You are not required to accept this License, since you have not 337 | signed it. However, nothing else grants you permission to modify or 338 | distribute the Library or its derivative works. These actions are 339 | prohibited by law if you do not accept this License. Therefore, by 340 | modifying or distributing the Library (or any work based on the 341 | Library), you indicate your acceptance of this License to do so, and 342 | all its terms and conditions for copying, distributing or modifying 343 | the Library or works based on it. 344 | 345 | 10. Each time you redistribute the Library (or any work based on the 346 | Library), the recipient automatically receives a license from the 347 | original licensor to copy, distribute, link with or modify the Library 348 | subject to these terms and conditions. You may not impose any further 349 | restrictions on the recipients' exercise of the rights granted herein. 350 | You are not responsible for enforcing compliance by third parties to 351 | this License. 352 | 353 | 11. If, as a consequence of a court judgment or allegation of patent 354 | infringement or for any other reason (not limited to patent issues), 355 | conditions are imposed on you (whether by court order, agreement or 356 | otherwise) that contradict the conditions of this License, they do not 357 | excuse you from the conditions of this License. If you cannot 358 | distribute so as to satisfy simultaneously your obligations under this 359 | License and any other pertinent obligations, then as a consequence you 360 | may not distribute the Library at all. For example, if a patent 361 | license would not permit royalty-free redistribution of the Library by 362 | all those who receive copies directly or indirectly through you, then 363 | the only way you could satisfy both it and this License would be to 364 | refrain entirely from distribution of the Library. 365 | 366 | If any portion of this section is held invalid or unenforceable under any 367 | particular circumstance, the balance of the section is intended to apply, 368 | and the section as a whole is intended to apply in other circumstances. 369 | 370 | It is not the purpose of this section to induce you to infringe any 371 | patents or other property right claims or to contest validity of any 372 | such claims; this section has the sole purpose of protecting the 373 | integrity of the free software distribution system which is 374 | implemented by public license practices. Many people have made 375 | generous contributions to the wide range of software distributed 376 | through that system in reliance on consistent application of that 377 | system; it is up to the author/donor to decide if he or she is willing 378 | to distribute software through any other system and a licensee cannot 379 | impose that choice. 380 | 381 | This section is intended to make thoroughly clear what is believed to 382 | be a consequence of the rest of this License. 383 | 384 | 12. If the distribution and/or use of the Library is restricted in 385 | certain countries either by patents or by copyrighted interfaces, the 386 | original copyright holder who places the Library under this License may add 387 | an explicit geographical distribution limitation excluding those countries, 388 | so that distribution is permitted only in or among countries not thus 389 | excluded. In such case, this License incorporates the limitation as if 390 | written in the body of this License. 391 | 392 | 13. The Free Software Foundation may publish revised and/or new 393 | versions of the Library General Public License from time to time. 394 | Such new versions will be similar in spirit to the present version, 395 | but may differ in detail to address new problems or concerns. 396 | 397 | Each version is given a distinguishing version number. If the Library 398 | specifies a version number of this License which applies to it and 399 | "any later version", you have the option of following the terms and 400 | conditions either of that version or of any later version published by 401 | the Free Software Foundation. If the Library does not specify a 402 | license version number, you may choose any version ever published by 403 | the Free Software Foundation. 404 | 405 | 14. If you wish to incorporate parts of the Library into other free 406 | programs whose distribution conditions are incompatible with these, 407 | write to the author to ask for permission. For software which is 408 | copyrighted by the Free Software Foundation, write to the Free 409 | Software Foundation; we sometimes make exceptions for this. Our 410 | decision will be guided by the two goals of preserving the free status 411 | of all derivatives of our free software and of promoting the sharing 412 | and reuse of software generally. 413 | 414 | NO WARRANTY 415 | 416 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 417 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 418 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 419 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 420 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 421 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 422 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 423 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 424 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 425 | 426 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 427 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 428 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 429 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 430 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 431 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 432 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 433 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 434 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 435 | DAMAGES. 436 | 437 | END OF TERMS AND CONDITIONS 438 | 439 | How to Apply These Terms to Your New Libraries 440 | 441 | If you develop a new library, and you want it to be of the greatest 442 | possible use to the public, we recommend making it free software that 443 | everyone can redistribute and change. You can do so by permitting 444 | redistribution under these terms (or, alternatively, under the terms of the 445 | ordinary General Public License). 446 | 447 | To apply these terms, attach the following notices to the library. It is 448 | safest to attach them to the start of each source file to most effectively 449 | convey the exclusion of warranty; and each file should have at least the 450 | "copyright" line and a pointer to where the full notice is found. 451 | 452 | 453 | Copyright (C) 454 | 455 | This library is free software; you can redistribute it and/or 456 | modify it under the terms of the GNU Library General Public 457 | License as published by the Free Software Foundation; either 458 | version 2 of the License, or (at your option) any later version. 459 | 460 | This library is distributed in the hope that it will be useful, 461 | but WITHOUT ANY WARRANTY; without even the implied warranty of 462 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 463 | Library General Public License for more details. 464 | 465 | You should have received a copy of the GNU Library General Public 466 | License along with this library; if not, write to the Free Software 467 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 468 | 469 | Also add information on how to contact you by electronic and paper mail. 470 | 471 | You should also get your employer (if you work as a programmer) or your 472 | school, if any, to sign a "copyright disclaimer" for the library, if 473 | necessary. Here is a sample; alter the names: 474 | 475 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 476 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 477 | 478 | , 1 April 1990 479 | Ty Coon, President of Vice 480 | 481 | That's all there is to it! 482 | -------------------------------------------------------------------------------- /doc/TODO: -------------------------------------------------------------------------------- 1 | GENERAL 2 | ------- 3 | 4 | - web page needs to get done: 5 | - Download 6 | - News 7 | - TODO list 8 | - Changes 9 | - Name 10 | 11 | - use a better solution than DAV/INI_Parse.py [Stephane Klein] 12 | - create WSGI server version [Stephane Klein] 13 | 14 | MOVE 15 | ---- 16 | 17 | needs to get implemented (will get used for renaming files and dirs) 18 | 19 | 20 | PROPFIND 21 | -------- 22 | 23 | - PROPNAMES need to get implemented 24 | - only DAV properties are possible right now 25 | - Depth=infinity should be implemented 26 | 27 | 28 | GET 29 | --- 30 | 31 | - guessing mimetype? 32 | 33 | 34 | PROPPATCH 35 | --------- 36 | 37 | to be done (not that important as only DAV props are supported right now which 38 | you cannot change) 39 | 40 | 41 | TEST 42 | ---- 43 | 44 | - create some unit test 45 | - maybe use davclient package to make the tests 46 | -------------------------------------------------------------------------------- /doc/interface_class: -------------------------------------------------------------------------------- 1 | How to write an interface class 2 | ------------------------------- 3 | 4 | (this information might be a little out of date. See data.py for more 5 | details). 6 | 7 | The interface class of davserver is the interface between the actual data 8 | and the davserver. The davserver will ask this class every time it needs 9 | information about the underlying data (e.g. a filesystem or a database). 10 | 11 | So how do you write such a class? 12 | 13 | Simply take the existing class which models a normal unix filesystem 14 | and change it. You actually have implement the following methods: 15 | 16 | 17 | 18 | get_childs(self, uri, filter=None) 19 | 20 | This method should return a list of all childs for the 21 | object specified by the given uri. 22 | 23 | The childs should be specified as normal URIs. 24 | 25 | 26 | get_props(self,uri,values=None,all=None,proplist=[]) 27 | 28 | This method will be called when the davserver needs information 29 | about properties for the object specified with the given uri. 30 | The parameters are as follows: 31 | 32 | values -- ?? cannot remember ;-) 33 | all -- if set to 1 return all properties 34 | proplist -- alternatively you can give get a list of 35 | properties to return 36 | 37 | The result of this method should be a dictionary of the form 38 | 39 | props[propname]=propvalue 40 | 41 | Note that in the example class this one is simply a dummy class 42 | as only DAV properties are handled which have their own methods 43 | (see below). 44 | 45 | 46 | get_data(self,uri) 47 | 48 | This method will be called when the content of an object is needed. 49 | Thus this method should return a data string. 50 | 51 | 52 | get_dav(self,uri,propname) 53 | 54 | This method will be called when the server needs access to a DAV 55 | property. In the example implementation it will simply delegate it 56 | to the corresponding _get_dav_ method. You maybe should 57 | handle it the same way. 58 | 59 | 60 | _get_dav_(uri) 61 | 62 | These methods will be called by get_dav() when the value of a DAV 63 | property is needed. The defined properties are: 64 | 65 | - resourcetype (empty or if the object is a collection) 66 | - getcontentlength 67 | - getcontenttype 68 | - getlastmodified 69 | - creationdate 70 | 71 | 72 | 73 | put(self,uri,data) 74 | 75 | This method will write data into the given object. 76 | It should return the result code (e.g. 424 if an error occured and 77 | None if everythin was ok). 78 | 79 | 80 | mkcol(self,uri) 81 | 82 | This method will be called when the MKCOL WEBDAV method was received 83 | by the server. The interface class has to test 84 | - if the parents of the uri all exists. If not, return 409 85 | - if the object already exists. If so, return 405 86 | - if it is allowed to create the collection. If not, return 403 87 | If everything is ok, then create the new collection (aka directory) 88 | and return 201. 89 | 90 | 91 | rmcol(self,uri) 92 | 93 | This method is called when a collection needs to be removed. 94 | Only the collection should be removed, no children as the davserver 95 | is automatically iterating over all children and calling rm/rmcol 96 | for each of them (because it needs a result code for every deleted 97 | object). 98 | If the user is not allowed to delete the collection then an 99 | 403 should be returned 100 | 101 | 102 | rm(self,uri) 103 | 104 | This is the same for single objects, the same as above applies. 105 | 106 | 107 | is_collection(self,uri) 108 | 109 | This one simply returns 1 if the object specified by the uri 110 | is an object or 0 if it isn't. 111 | 112 | 113 | 114 | So these are basically the methods which need to get implemented. While writing 115 | this I also noticed some problems: 116 | 117 | - the actual user is not know to the interface class. This should be changed as 118 | it might be important when testing if an action is allowed or not. Also some 119 | implementations might need a user in order to decide what to return (e.g. 120 | GROUP.lounge will need this.) 121 | 122 | - the return of result codes is not standardized throughout the interface class. 123 | This should be changed. 124 | 125 | - The should be a super interface class to derive from in order to handle some 126 | common things like get_dav() or property handling in general. Some things 127 | then also might me moved from propfind.py/devserver.py into this class. 128 | 129 | 130 | 131 | As the changes above might break existing code you have been warned with this 132 | message :) 133 | 134 | -------------------------------------------------------------------------------- /doc/walker: -------------------------------------------------------------------------------- 1 | Walker methods 2 | -------------- 3 | 4 | In the COPY, DELETE and MOVE methods we need to walk over 5 | a tree of resources and collections in order to copy, delete 6 | or move them. 7 | 8 | The difference between all these walks is only that we perform 9 | a different action on the resources we visit. Thus it might be 10 | the simplest solution to provide a walker class or method to 11 | do that work and give it a function to perform before starting. 12 | 13 | 14 | Way of walking 15 | -------------- 16 | 17 | When we delete things we should do it bottom up but when we copy 18 | or move things we should create resources top down. Thus we actually 19 | need 2 methods. 20 | 21 | But the following method might work: We create a list of all the nodes 22 | in the tree in tree order (means top down, left to right). When 23 | we walk over this list from begin to end we can copy and when we 24 | move backwards we can delete. 25 | 26 | Thus we need an indicator for the direction and the method to 27 | perform on it. 28 | 29 | 30 | Here the iterative approach (in order to save memory): 31 | dc = dataclass 32 | queue = list = [start_uri] 33 | while len(queue): 34 | element = queue[-1] 35 | childs = dc.get_childs(element) 36 | if childs: 37 | list = list + childs 38 | # update queue 39 | del queue[-1] 40 | if childs: 41 | queue = queue + childs 42 | 43 | 44 | (first try..) 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /pywebdav/__init__.py: -------------------------------------------------------------------------------- 1 | """WebDAV library including a standalone server for python 3""" 2 | __version__ = '0.9.14' 3 | __author__ = 'Andrew Leech (previously Simon Pamies)' 4 | __email__ = 'andrew@alelec.net' 5 | __license__ = 'LGPL v2' 6 | -------------------------------------------------------------------------------- /pywebdav/__main__.py: -------------------------------------------------------------------------------- 1 | from pywebdav.server import server 2 | server.run() 3 | -------------------------------------------------------------------------------- /pywebdav/lib/AuthServer.py: -------------------------------------------------------------------------------- 1 | """Authenticating HTTP Server 2 | 3 | This module builds on BaseHTTPServer and implements basic authentication 4 | 5 | """ 6 | 7 | import base64 8 | import binascii 9 | import six.moves.BaseHTTPServer 10 | 11 | 12 | DEFAULT_AUTH_ERROR_MESSAGE = """ 13 | 14 | %(code)d - %(message)s 15 | 16 | 17 |

Authorization Required

18 | this server could not verify that you 19 | are authorized to access the document 20 | requested. Either you supplied the wrong 21 | credentials (e.g., bad password), or your 22 | browser doesn't understand how to supply 23 | the credentials required. 24 | """ 25 | 26 | 27 | def _quote_html(html): 28 | return html.replace("&", "&").replace("<", "<").replace(">", ">") 29 | 30 | 31 | class AuthRequestHandler(six.moves.BaseHTTPServer.BaseHTTPRequestHandler): 32 | """ 33 | Simple handler that can check for auth headers 34 | 35 | In your subclass you have to define the method get_userinfo(user, password) 36 | which should return 1 or None depending on whether the password was 37 | ok or not. None means that the user is not authorized. 38 | """ 39 | 40 | # False means no authentiation 41 | DO_AUTH = 1 42 | 43 | def parse_request(self): 44 | if not six.moves.BaseHTTPServer.BaseHTTPRequestHandler.parse_request(self): 45 | return False 46 | 47 | if self.DO_AUTH: 48 | authorization = self.headers.get('Authorization', '') 49 | if not authorization: 50 | self.send_autherror(401, "Authorization Required") 51 | return False 52 | scheme, credentials = authorization.split() 53 | if scheme != 'Basic': 54 | self.send_error(501) 55 | return False 56 | credentials = base64.decodebytes(credentials.encode()).decode() 57 | user, password = credentials.split(':') 58 | if not self.get_userinfo(user, password, self.command): 59 | self.send_autherror(401, "Authorization Required") 60 | return False 61 | return True 62 | 63 | def send_autherror(self, code, message=None): 64 | """Send and log an auth error reply. 65 | 66 | Arguments are the error code, and a detailed message. 67 | The detailed message defaults to the short entry matching the 68 | response code. 69 | 70 | This sends an error response (so it must be called before any 71 | output has been generated), logs the error, and finally sends 72 | a piece of HTML explaining the error to the user. 73 | 74 | """ 75 | try: 76 | short, long = self.responses[code] 77 | except KeyError: 78 | short, long = '???', '???' 79 | if message is None: 80 | message = short 81 | explain = long 82 | self.log_error("code %d, message %s", code, message) 83 | 84 | # using _quote_html to prevent Cross Site Scripting attacks (see bug 85 | # #1100201) 86 | content = (self.error_auth_message_format % {'code': code, 'message': 87 | _quote_html(message), 'explain': explain}) 88 | self.send_response(code, message) 89 | self.send_header('Content-Type', self.error_content_type) 90 | self.send_header('WWW-Authenticate', 'Basic realm="PyWebDAV"') 91 | self.send_header('Connection', 'close') 92 | self.end_headers() 93 | self.wfile.write(content.encode('utf-8')) 94 | 95 | error_auth_message_format = DEFAULT_AUTH_ERROR_MESSAGE 96 | 97 | def get_userinfo(self, user, password, command): 98 | """Checks if the given user and the given 99 | password are allowed to access. 100 | """ 101 | # Always reject 102 | return None 103 | -------------------------------------------------------------------------------- /pywebdav/lib/INI_Parse.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | 3 | class Configuration: 4 | def __init__(self, fileName): 5 | cp = ConfigParser() 6 | cp.read(fileName) 7 | self.__parser = cp 8 | self.fileName = fileName 9 | 10 | def __getattr__(self, name): 11 | if name in self.__parser.sections(): 12 | return Section(name, self.__parser) 13 | else: 14 | return None 15 | 16 | def __str__(self): 17 | p = self.__parser 18 | result = [] 19 | result.append('' % self.fileName) 20 | for s in p.sections(): 21 | result.append('[%s]' % s) 22 | for o in p.options(s): 23 | result.append('%s=%s' % (o, p.get(s, o))) 24 | return '\n'.join(result) 25 | 26 | class Section: 27 | def __init__(self, name, parser): 28 | self.name = name 29 | self.__parser = parser 30 | 31 | def __getattr__(self, name): 32 | return self.__parser.get(self.name, name) 33 | 34 | def __str__(self): 35 | return str(self.__repr__()) 36 | 37 | def __repr__(self): 38 | return self.__parser.items(self.name) 39 | 40 | def getboolean(self, name): 41 | return self.__parser.getboolean(self.name, name) 42 | 43 | def __contains__(self, name): 44 | return self.__parser.has_option(self.name, name) 45 | 46 | def get(self, name, default): 47 | if name in self: 48 | return self.__getattr__(name) 49 | else: 50 | return default 51 | 52 | def set(self, name, value): 53 | self.__parser.set(self.name, name, str(value)) 54 | 55 | # Test 56 | if __name__ == '__main__': 57 | c = Configuration('Importador.ini') 58 | print(c.Origem.host, c.Origem.port) 59 | -------------------------------------------------------------------------------- /pywebdav/lib/WebDAVServer.py: -------------------------------------------------------------------------------- 1 | """DAV HTTP Server 2 | 3 | This module builds on BaseHTTPServer and implements DAV commands 4 | 5 | """ 6 | from . import AuthServer 7 | from six.moves import urllib 8 | import logging 9 | 10 | from .propfind import PROPFIND 11 | from .report import REPORT 12 | from .delete import DELETE 13 | from .davcopy import COPY 14 | from .davmove import MOVE 15 | 16 | from .utils import rfc1123_date, IfParser, tokenFinder 17 | from .errors import DAV_Error, DAV_NotFound 18 | 19 | from .constants import DAV_VERSION_1, DAV_VERSION_2 20 | from .locks import LockManager 21 | import gzip 22 | import io 23 | 24 | from pywebdav import __version__ 25 | 26 | from xml.parsers.expat import ExpatError 27 | 28 | log = logging.getLogger(__name__) 29 | 30 | BUFFER_SIZE = 128 * 1000 # 128 Ko 31 | 32 | 33 | class DAVRequestHandler(AuthServer.AuthRequestHandler, LockManager): 34 | """Simple DAV request handler with 35 | 36 | - GET 37 | - HEAD 38 | - PUT 39 | - OPTIONS 40 | - PROPFIND 41 | - PROPPATCH 42 | - MKCOL 43 | - REPORT 44 | 45 | experimental 46 | - LOCK 47 | - UNLOCK 48 | 49 | It uses the resource/collection classes for serving and 50 | storing content. 51 | 52 | """ 53 | 54 | server_version = "DAV/" + __version__ 55 | encode_threshold = 1400 # common MTU 56 | 57 | def send_body(self, DATA, code=None, msg=None, desc=None, 58 | ctype='application/octet-stream', headers={}): 59 | """ send a body in one part """ 60 | log.debug("Use send_body method") 61 | 62 | self.send_response(code, message=msg) 63 | self.send_header("Connection", "close") 64 | self.send_header("Accept-Ranges", "bytes") 65 | self.send_header('Date', rfc1123_date()) 66 | 67 | self._send_dav_version() 68 | 69 | for a, v in headers.items(): 70 | self.send_header(a, v) 71 | 72 | if DATA: 73 | try: 74 | if 'gzip' in self.headers.get('Accept-Encoding', '').split(',') \ 75 | and len(DATA) > self.encode_threshold: 76 | buffer = io.BytesIO() 77 | output = gzip.GzipFile(mode='wb', fileobj=buffer) 78 | if isinstance(DATA, str): 79 | output.write(DATA) 80 | else: 81 | for buf in DATA: 82 | output.write(buf) 83 | output.close() 84 | buffer.seek(0) 85 | DATA = buffer.getvalue() 86 | self.send_header('Content-Encoding', 'gzip') 87 | 88 | self.send_header('Content-Length', len(DATA)) 89 | self.send_header('Content-Type', ctype) 90 | except Exception as ex: 91 | log.exception(ex) 92 | else: 93 | self.send_header('Content-Length', 0) 94 | 95 | self.end_headers() 96 | if DATA: 97 | if isinstance(DATA, str): 98 | DATA = DATA.encode('utf-8') 99 | if isinstance(DATA, str) or isinstance(DATA, bytes): 100 | log.debug("Don't use iterator") 101 | self.wfile.write(DATA) 102 | else: 103 | if self._config.DAV.getboolean('http_response_use_iterator'): 104 | # Use iterator to reduce using memory 105 | log.debug("Use iterator") 106 | for buf in DATA: 107 | self.wfile.write(buf) 108 | self.wfile.flush() 109 | else: 110 | # Don't use iterator, it's a compatibility option 111 | log.debug("Don't use iterator") 112 | res = DATA.read() 113 | if isinstance(res,bytes): 114 | self.wfile.write(res) 115 | else: 116 | self.wfile.write(res.encode('utf8')) 117 | return None 118 | 119 | def send_body_chunks_if_http11(self, DATA, code, msg=None, desc=None, 120 | ctype='text/xml; encoding="utf-8"', 121 | headers={}): 122 | if (self.request_version == 'HTTP/1.0' or 123 | not self._config.DAV.getboolean('chunked_http_response')): 124 | self.send_body(DATA, code, msg, desc, ctype, headers) 125 | else: 126 | self.send_body_chunks(DATA, code, msg, desc, ctype, headers) 127 | 128 | def send_body_chunks(self, DATA, code, msg=None, desc=None, 129 | ctype='text/xml"', headers={}): 130 | """ send a body in chunks """ 131 | 132 | self.responses[207] = (msg, desc) 133 | self.send_response(code, message=msg) 134 | self.send_header("Content-type", ctype) 135 | self.send_header("Transfer-Encoding", "chunked") 136 | self.send_header('Date', rfc1123_date()) 137 | 138 | self._send_dav_version() 139 | 140 | for a, v in headers.items(): 141 | self.send_header(a, v) 142 | 143 | GZDATA = None 144 | if DATA: 145 | if ('gzip' in self.headers.get('Accept-Encoding', '').split(',') 146 | and len(DATA) > self.encode_threshold): 147 | buffer = io.BytesIO() 148 | output = gzip.GzipFile(mode='wb', fileobj=buffer) 149 | if isinstance(DATA, bytes): 150 | output.write(DATA) 151 | else: 152 | for buf in DATA: 153 | buf = buf.encode() if isinstance(buf, str) else buf 154 | output.write(buf) 155 | output.close() 156 | buffer.seek(0) 157 | GZDATA = buffer.getvalue() 158 | self.send_header('Content-Encoding', 'gzip') 159 | 160 | self.send_header('Content-Length', len(DATA)) 161 | self.send_header('Content-Type', ctype) 162 | 163 | else: 164 | self.send_header('Content-Length', 0) 165 | 166 | self.end_headers() 167 | 168 | if GZDATA: 169 | self.wfile.write(GZDATA) 170 | 171 | elif DATA: 172 | DATA = DATA.encode() if isinstance(DATA, str) else DATA 173 | if isinstance(DATA, bytes): 174 | self.wfile.write(b"%s\r\n" % hex(len(DATA))[2:].encode()) 175 | self.wfile.write(DATA) 176 | self.wfile.write(b"\r\n") 177 | self.wfile.write(b"0\r\n") 178 | self.wfile.write(b"\r\n") 179 | else: 180 | if self._config.DAV.getboolean('http_response_use_iterator'): 181 | # Use iterator to reduce using memory 182 | for buf in DATA: 183 | buf = buf.encode() if isinstance(buf, str) else buf 184 | self.wfile.write((hex(len(buf))[2:] + "\r\n").encode()) 185 | self.wfile.write(buf) 186 | self.wfile.write(b"\r\n") 187 | 188 | self.wfile.write(b"0\r\n") 189 | self.wfile.write(b"\r\n") 190 | else: 191 | # Don't use iterator, it's a compatibility option 192 | self.wfile.write((hex(len(DATA))[2:] + "\r\n").encode()) 193 | self.wfile.write(DATA.read()) 194 | self.wfile.write(b"\r\n") 195 | self.wfile.write(b"0\r\n") 196 | self.wfile.write(b"\r\n") 197 | 198 | def _send_dav_version(self): 199 | if self._config.DAV.getboolean('lockemulation'): 200 | self.send_header('DAV', DAV_VERSION_2['version']) 201 | else: 202 | self.send_header('DAV', DAV_VERSION_1['version']) 203 | 204 | ### HTTP METHODS called by the server 205 | 206 | def do_OPTIONS(self): 207 | """return the list of capabilities """ 208 | 209 | self.send_response(200) 210 | self.send_header("Content-Length", 0) 211 | 212 | if self._config.DAV.getboolean('lockemulation'): 213 | self.send_header('Allow', DAV_VERSION_2['options']) 214 | else: 215 | self.send_header('Allow', DAV_VERSION_1['options']) 216 | 217 | self._send_dav_version() 218 | 219 | self.send_header('MS-Author-Via', 'DAV') # this is for M$ 220 | self.end_headers() 221 | 222 | def _HEAD_GET(self, with_body=False): 223 | """ Returns headers and body for given resource """ 224 | 225 | dc = self.IFACE_CLASS 226 | uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) 227 | 228 | headers = {} 229 | 230 | # get the last modified date (RFC 1123!) 231 | try: 232 | headers['Last-Modified'] = dc.get_prop( 233 | uri, "DAV:", "getlastmodified") 234 | except DAV_NotFound: 235 | pass 236 | 237 | # get the ETag if any 238 | try: 239 | headers['Etag'] = dc.get_prop(uri, "DAV:", "getetag") 240 | except DAV_NotFound: 241 | pass 242 | 243 | # get the content type 244 | try: 245 | if uri.endswith('/'): 246 | # we could do away with this very non-local workaround for 247 | # _get_listing if the data could have a type attached 248 | content_type = 'text/html;charset=utf-8' 249 | else: 250 | content_type = dc.get_prop(uri, "DAV:", "getcontenttype") 251 | except DAV_NotFound: 252 | content_type = "application/octet-stream" 253 | 254 | range = None 255 | status_code = 200 256 | if 'Range' in self.headers: 257 | p = self.headers['Range'].find("bytes=") 258 | if p != -1: 259 | range = self.headers['Range'][p + 6:].split("-") 260 | status_code = 206 261 | 262 | # get the data 263 | try: 264 | data = dc.get_data(uri, range) 265 | except DAV_Error as error: 266 | (ec, dd) = error.args 267 | self.send_status(ec) 268 | return ec 269 | 270 | # send the data 271 | if with_body is False: 272 | data = None 273 | 274 | if isinstance(data, str): 275 | self.send_body(data, status_code, None, None, content_type, 276 | headers) 277 | else: 278 | headers['Keep-Alive'] = 'timeout=15, max=86' 279 | headers['Connection'] = 'Keep-Alive' 280 | self.send_body_chunks_if_http11(data, status_code, None, None, 281 | content_type, headers) 282 | 283 | return status_code 284 | 285 | def do_HEAD(self): 286 | """ Send a HEAD response: Retrieves resource information w/o body """ 287 | 288 | return self._HEAD_GET(with_body=False) 289 | 290 | def do_GET(self): 291 | """Serve a GET request.""" 292 | 293 | log.debug(self.headers) 294 | 295 | try: 296 | status_code = self._HEAD_GET(with_body=True) 297 | self.log_request(status_code) 298 | return status_code 299 | except IOError as e: 300 | if e.errno == 32: 301 | self.log_request(206) 302 | else: 303 | raise 304 | 305 | def do_TRACE(self): 306 | """ This will always fail because we can not reproduce HTTP requests. 307 | We send back a 405=Method Not Allowed. """ 308 | 309 | self.send_body(None, 405, 'Method Not Allowed', 'Method Not Allowed') 310 | 311 | def do_POST(self): 312 | """ Replacement for GET response. Not implemented here. """ 313 | 314 | self.send_body(None, 405, 'Method Not Allowed', 'Method Not Allowed') 315 | 316 | def do_PROPPATCH(self): 317 | # currently unsupported 318 | return self.send_status(423) 319 | 320 | def do_PROPFIND(self): 321 | """ Retrieve properties on defined resource. """ 322 | 323 | dc = self.IFACE_CLASS 324 | 325 | # read the body containing the xml request 326 | # iff there is no body then this is an ALLPROP request 327 | body = None 328 | if 'Content-Length' in self.headers: 329 | l = self.headers['Content-Length'] 330 | body = self.rfile.read(int(l)) 331 | 332 | uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) 333 | 334 | try: 335 | pf = PROPFIND(uri, dc, self.headers.get('Depth', 'infinity'), body) 336 | except ExpatError: 337 | # parse error 338 | return self.send_status(400) 339 | 340 | try: 341 | DATA = pf.createResponse() 342 | except DAV_Error as error: 343 | (ec, dd) = error.args 344 | return self.send_status(ec) 345 | 346 | # work around MSIE DAV bug for creation and modified date 347 | # taken from Resource.py @ Zope webdav 348 | if (self.headers.get('User-Agent') == 349 | 'Microsoft Data Access Internet Publishing Provider DAV 1.1'): 350 | DATA = DATA.replace(b'', 351 | b'') 355 | DATA = DATA.replace(b'', 356 | b'') 360 | 361 | self.send_body_chunks_if_http11(DATA, 207, 'Multi-Status', 362 | 'Multiple responses') 363 | 364 | def do_REPORT(self): 365 | """ Query properties on defined resource. """ 366 | 367 | dc = self.IFACE_CLASS 368 | 369 | # read the body containing the xml request 370 | # iff there is no body then this is an ALLPROP request 371 | body = None 372 | if 'Content-Length' in self.headers: 373 | l = self.headers['Content-Length'] 374 | body = self.rfile.read(int(l)) 375 | 376 | uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) 377 | 378 | rp = REPORT(uri, dc, self.headers.get('Depth', '0'), body) 379 | 380 | try: 381 | DATA = '%s\n' % rp.createResponse() 382 | except DAV_Error as error: 383 | (ec, dd) = error.args 384 | return self.send_status(ec) 385 | 386 | self.send_body_chunks_if_http11(DATA, 207, 'Multi-Status', 387 | 'Multiple responses') 388 | 389 | def do_MKCOL(self): 390 | """ create a new collection """ 391 | 392 | # according to spec body must be empty 393 | body = None 394 | if 'Content-Length' in self.headers: 395 | l = self.headers['Content-Length'] 396 | body = self.rfile.read(int(l)) 397 | 398 | if body: 399 | return self.send_status(415) 400 | 401 | dc = self.IFACE_CLASS 402 | uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) 403 | 404 | try: 405 | dc.mkcol(uri) 406 | self.send_status(201) 407 | self.log_request(201) 408 | except DAV_Error as error: 409 | (ec, dd) = error.args 410 | self.log_request(ec) 411 | return self.send_status(ec) 412 | 413 | def do_DELETE(self): 414 | """ delete an resource """ 415 | 416 | dc = self.IFACE_CLASS 417 | uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) 418 | 419 | # hastags not allowed 420 | if uri.find('#') >= 0: 421 | return self.send_status(404) 422 | 423 | # locked resources are not allowed to delete 424 | if self._l_isLocked(uri): 425 | return self.send_body(None, 423, 'Locked', 'Locked') 426 | 427 | # Handle If-Match 428 | if 'If-Match' in self.headers: 429 | test = False 430 | etag = None 431 | try: 432 | etag = dc.get_prop(uri, "DAV:", "getetag") 433 | except: 434 | pass 435 | for match in self.headers['If-Match'].split(','): 436 | if match == '*': 437 | if dc.exists(uri): 438 | test = True 439 | break 440 | else: 441 | if match == etag: 442 | test = True 443 | break 444 | if not test: 445 | self.send_status(412) 446 | self.log_request(412) 447 | return 448 | 449 | # Handle If-None-Match 450 | if 'If-None-Match' in self.headers: 451 | test = True 452 | etag = None 453 | try: 454 | etag = dc.get_prop(uri, "DAV:", "getetag") 455 | except: 456 | pass 457 | for match in self.headers['If-None-Match'].split(','): 458 | if match == '*': 459 | if dc.exists(uri): 460 | test = False 461 | break 462 | else: 463 | if match == etag: 464 | test = False 465 | break 466 | if not test: 467 | self.send_status(412) 468 | self.log_request(412) 469 | return 470 | 471 | try: 472 | dl = DELETE(uri, dc) 473 | if dc.is_collection(uri): 474 | res = dl.delcol() 475 | if res: 476 | self.send_status(207, body=res) 477 | else: 478 | self.send_status(204) 479 | else: 480 | res = dl.delone() or 204 481 | self.send_status(res) 482 | except DAV_NotFound: 483 | self.send_body(None, 404, 'Not Found', 'Not Found') 484 | 485 | def do_PUT(self): 486 | dc = self.IFACE_CLASS 487 | uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) 488 | 489 | log.debug("do_PUT: uri = %s" % uri) 490 | log.debug('do_PUT: headers = %s' % self.headers) 491 | # Handle If-Match 492 | if 'If-Match' in self.headers: 493 | log.debug("do_PUT: If-Match %s" % self.headers['If-Match']) 494 | test = False 495 | etag = None 496 | try: 497 | etag = dc.get_prop(uri, "DAV:", "getetag") 498 | except: 499 | pass 500 | 501 | log.debug("do_PUT: etag = %s" % etag) 502 | 503 | for match in self.headers['If-Match'].split(','): 504 | if match == '*': 505 | if dc.exists(uri): 506 | test = True 507 | break 508 | else: 509 | if match == etag: 510 | test = True 511 | break 512 | if not test: 513 | self.send_status(412) 514 | self.log_request(412) 515 | return 516 | 517 | # Handle If-None-Match 518 | if 'If-None-Match' in self.headers: 519 | log.debug("do_PUT: If-None-Match %s" % 520 | self.headers['If-None-Match']) 521 | 522 | test = True 523 | etag = None 524 | try: 525 | etag = dc.get_prop(uri, "DAV:", "getetag") 526 | except: 527 | pass 528 | 529 | log.debug("do_PUT: etag = %s" % etag) 530 | 531 | for match in self.headers['If-None-Match'].split(','): 532 | if match == '*': 533 | if dc.exists(uri): 534 | test = False 535 | break 536 | else: 537 | if match == etag: 538 | test = False 539 | break 540 | if not test: 541 | self.send_status(412) 542 | self.log_request(412) 543 | return 544 | 545 | # locked resources are not allowed to be overwritten 546 | ifheader = self.headers.get('If') 547 | if ( 548 | (self._l_isLocked(uri)) and 549 | (not ifheader) 550 | ): 551 | return self.send_body(None, 423, 'Locked', 'Locked') 552 | 553 | if self._l_isLocked(uri) and ifheader: 554 | uri_token = self._l_getLockForUri(uri) 555 | taglist = IfParser(ifheader) 556 | found = False 557 | for tag in taglist: 558 | for listitem in tag.list: 559 | token = tokenFinder(listitem) 560 | if ( 561 | token and 562 | (self._l_hasLock(token)) and 563 | (self._l_getLock(token) == uri_token) 564 | ): 565 | found = True 566 | break 567 | if found: 568 | break 569 | if not found: 570 | res = self.send_body(None, 423, 'Locked', 'Locked') 571 | self.log_request(423) 572 | return res 573 | 574 | # Handle expect 575 | expect = self.headers.get('Expect', '') 576 | if (expect.lower() == '100-continue' and 577 | self.protocol_version >= 'HTTP/1.1' and 578 | self.request_version >= 'HTTP/1.1'): 579 | self.send_status(100) 580 | 581 | content_type = None 582 | if 'Content-Type' in self.headers: 583 | content_type = self.headers['Content-Type'] 584 | 585 | headers = {} 586 | headers['Location'] = urllib.parse.quote(uri) 587 | 588 | try: 589 | etag = dc.get_prop(uri, "DAV:", "getetag") 590 | headers['ETag'] = etag 591 | except: 592 | pass 593 | 594 | expect = self.headers.get('transfer-encoding', '') 595 | if ( 596 | expect.lower() == 'chunked' and 597 | self.protocol_version >= 'HTTP/1.1' and 598 | self.request_version >= 'HTTP/1.1' 599 | ): 600 | self.send_body(None, 201, 'Created', '', headers=headers) 601 | 602 | dc.put(uri, self._readChunkedData(), content_type) 603 | else: 604 | # read the body 605 | body = None 606 | if 'Content-Length' in self.headers: 607 | l = self.headers['Content-Length'] 608 | log.debug("do_PUT: Content-Length = %s" % l) 609 | body = self._readNoChunkedData(int(l)) 610 | else: 611 | log.debug("do_PUT: Content-Length = empty") 612 | 613 | try: 614 | dc.put(uri, body, content_type) 615 | except DAV_Error as error: 616 | (ec, dd) = error.args 617 | return self.send_status(ec) 618 | 619 | self.send_body(None, 201, 'Created', '', headers=headers) 620 | self.log_request(201) 621 | 622 | def _readChunkedData(self): 623 | l = int(self.rfile.readline(), 16) 624 | while l > 0: 625 | buf = self.rfile.read(l) 626 | yield buf 627 | self.rfile.readline() 628 | l = int(self.rfile.readline(), 16) 629 | 630 | def _readNoChunkedData(self, content_length): 631 | if self._config.DAV.getboolean('http_request_use_iterator'): 632 | # Use iterator to reduce using memory 633 | return self.__readNoChunkedDataWithIterator(content_length) 634 | else: 635 | # Don't use iterator, it's a compatibility option 636 | return self.__readNoChunkedDataWithoutIterator(content_length) 637 | 638 | def __readNoChunkedDataWithIterator(self, content_length): 639 | while True: 640 | if content_length > BUFFER_SIZE: 641 | buf = self.rfile.read(BUFFER_SIZE) 642 | content_length -= BUFFER_SIZE 643 | yield buf 644 | else: 645 | buf = self.rfile.read(content_length) 646 | yield buf 647 | break 648 | 649 | def __readNoChunkedDataWithoutIterator(self, content_length): 650 | return self.rfile.read(content_length) 651 | 652 | def do_COPY(self): 653 | """ copy one resource to another """ 654 | try: 655 | self.copymove(COPY) 656 | except DAV_Error as error: 657 | (ec, dd) = error.args 658 | return self.send_status(ec) 659 | 660 | def do_MOVE(self): 661 | """ move one resource to another """ 662 | try: 663 | self.copymove(MOVE) 664 | except DAV_Error as error: 665 | (ec, dd) = error.args 666 | return self.send_status(ec) 667 | 668 | def copymove(self, CLASS): 669 | """ common method for copying or moving objects """ 670 | dc = self.IFACE_CLASS 671 | 672 | # get the source URI 673 | source_uri = urllib.parse.unquote(urllib.parse.urljoin(self.get_baseuri(dc), self.path)) 674 | 675 | # get the destination URI 676 | dest_uri = self.headers['Destination'] 677 | dest_uri = urllib.parse.unquote(dest_uri) 678 | 679 | # check locks on source and dest 680 | if self._l_isLocked(source_uri) or self._l_isLocked(dest_uri): 681 | return self.send_body(None, 423, 'Locked', 'Locked') 682 | 683 | # Overwrite? 684 | overwrite = 1 685 | result_code = 204 686 | if 'Overwrite' in self.headers: 687 | if self.headers['Overwrite'] == "F": 688 | overwrite = None 689 | result_code = 201 690 | 691 | # instanciate ACTION class 692 | cp = CLASS(dc, source_uri, dest_uri, overwrite) 693 | 694 | # Depth? 695 | d = "infinity" 696 | if 'Depth' in self.headers: 697 | d = self.headers['Depth'] 698 | 699 | if d != "0" and d != "infinity": 700 | self.send_status(400) 701 | return 702 | 703 | if d == "0": 704 | res = cp.single_action() 705 | self.send_status(res or 201) 706 | return 707 | 708 | # now it only can be "infinity" but we nevertheless check for a 709 | # collection 710 | if dc.is_collection(source_uri): 711 | try: 712 | res = cp.tree_action() 713 | except DAV_Error as error: 714 | (ec, dd) = error.args 715 | self.send_status(ec) 716 | return 717 | else: 718 | try: 719 | res = cp.single_action() 720 | except DAV_Error as error: 721 | (ec, dd) = error.args 722 | self.send_status(ec) 723 | return 724 | 725 | if res: 726 | self.send_body_chunks_if_http11(res, 207, self.responses[207][0], 727 | self.responses[207][1], 728 | ctype='text/xml; charset="utf-8"') 729 | else: 730 | self.send_status(result_code) 731 | 732 | def get_userinfo(self, user, pw): 733 | """ Dummy method which lets all users in """ 734 | return 1 735 | 736 | def send_status(self, code=200, mediatype='text/xml; charset="utf-8"', 737 | msg=None, body=None): 738 | 739 | if not msg: 740 | msg = self.responses.get(code, ['', ''])[1] 741 | 742 | self.send_body(body, code, self.responses.get(code, [''])[0], msg, 743 | mediatype) 744 | 745 | def get_baseuri(self, dc): 746 | baseuri = dc.baseuri 747 | if 'Host' in self.headers: 748 | uparts = list(urllib.parse.urlparse(dc.baseuri)) 749 | uparts[1] = self.headers['Host'] 750 | baseuri = urllib.parse.urlunparse(uparts) 751 | return baseuri 752 | -------------------------------------------------------------------------------- /pywebdav/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewleech/PyWebDAV3/4d469487b968df55e2bfdf70f7f5276ac7f93153/pywebdav/lib/__init__.py -------------------------------------------------------------------------------- /pywebdav/lib/constants.py: -------------------------------------------------------------------------------- 1 | # definition for resourcetype 2 | COLLECTION=1 3 | OBJECT=None 4 | 5 | # attributes for resources 6 | DAV_PROPS=['creationdate', 'displayname', 'getcontentlanguage', 'getcontentlength', 'getcontenttype', 'getetag', 'getlastmodified', 'lockdiscovery', 'resourcetype', 'source', 'supportedlock'] 7 | 8 | # Request classes in propfind 9 | RT_ALLPROP=1 10 | RT_PROPNAME=2 11 | RT_PROP=3 12 | 13 | # server mode 14 | DAV_VERSION_1 = { 15 | 'version' : '1', 16 | 'options' : 17 | 'GET, HEAD, COPY, MOVE, POST, PUT, PROPFIND, PROPPATCH, OPTIONS, MKCOL, DELETE, TRACE, REPORT' 18 | } 19 | 20 | DAV_VERSION_2 = { 21 | 'version' : '1,2', 22 | 'options' : 23 | DAV_VERSION_1['options'] + ', LOCK, UNLOCK' 24 | } 25 | -------------------------------------------------------------------------------- /pywebdav/lib/davcmd.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | davcmd.py 4 | --------- 5 | 6 | containts commands like copy, move, delete for normal 7 | resources and collections 8 | 9 | """ 10 | 11 | from six.moves import urllib 12 | 13 | from .utils import create_treelist, is_prefix 14 | from .errors import * 15 | from six.moves import range 16 | import os 17 | 18 | def deltree(dc,uri,exclude={}): 19 | """ delete a tree of resources 20 | 21 | dc -- dataclass to use 22 | uri -- root uri to delete 23 | exclude -- an optional list of uri:error_code pairs which should not 24 | be deleted. 25 | 26 | returns dict of uri:error_code tuples from which 27 | another method can create a multistatus xml element. 28 | 29 | Also note that we only know Depth=infinity thus we don't have 30 | to test for it. 31 | 32 | """ 33 | 34 | tlist=create_treelist(dc,uri) 35 | result={} 36 | 37 | for i in range(len(tlist),0,-1): 38 | problem_uris=list(result.keys()) 39 | element=tlist[i-1] 40 | 41 | # test here, if an element is a prefix of an uri which 42 | # generated an error before. 43 | # note that we walk here from childs to parents, thus 44 | # we cannot delete a parent if a child made a problem. 45 | # (see example in 8.6.2.1) 46 | ok=1 47 | for p in problem_uris: 48 | if is_prefix(element,p): 49 | ok=None 50 | break 51 | 52 | if not ok: continue 53 | 54 | # here we test for the exclude list which is the other way round! 55 | for p in exclude.keys(): 56 | if is_prefix(p,element): 57 | ok=None 58 | break 59 | 60 | if not ok: continue 61 | 62 | # now delete stuff 63 | try: 64 | delone(dc,element) 65 | except DAV_Error as error: 66 | (ec,dd) = error.args 67 | result[element]=ec 68 | 69 | return result 70 | 71 | def delone(dc,uri): 72 | """ delete a single object """ 73 | if dc.is_collection(uri): 74 | return dc.rmcol(uri) # should be empty 75 | else: 76 | return dc.rm(uri) 77 | 78 | ### 79 | ### COPY 80 | ### 81 | 82 | # helper function 83 | 84 | def copy(dc,src,dst): 85 | """ only copy the element 86 | 87 | This is just a helper method factored out from copy and 88 | copytree. It will not handle the overwrite or depth header. 89 | 90 | """ 91 | 92 | # destination should have been deleted before 93 | if dc.exists(dst): 94 | raise DAV_Error(412) 95 | 96 | # source should exist also 97 | if not dc.exists(src): 98 | raise DAV_NotFound 99 | 100 | if dc.is_collection(src): 101 | dc.copycol(src, dst) # an exception will be passed thru 102 | else: 103 | dc.copy(src, dst) # an exception will be passed thru 104 | 105 | # the main functions 106 | 107 | def copyone(dc,src,dst,overwrite=None): 108 | """ copy one resource to a new destination """ 109 | 110 | if overwrite and dc.exists(dst): 111 | delres = deltree(dc, dst) 112 | else: 113 | delres={} 114 | 115 | # if we cannot delete everything, then do not copy! 116 | if delres: 117 | return delres 118 | 119 | try: 120 | copy(dc, src, dst) # pass thru exceptions 121 | except DAV_Error as error: 122 | (ec, dd) = error.args 123 | return ec 124 | 125 | def copytree(dc,src,dst,overwrite=None): 126 | """ copy a tree of resources to another location 127 | 128 | dc -- dataclass to use 129 | src -- src uri from where to copy 130 | dst -- dst uri 131 | overwrite -- if True then delete dst uri before 132 | 133 | returns dict of uri:error_code tuples from which 134 | another method can create a multistatus xml element. 135 | 136 | """ 137 | 138 | # first delete the destination resource 139 | if overwrite and dc.exists(dst): 140 | delres=deltree(dc,dst) 141 | else: 142 | delres={} 143 | 144 | # if we cannot delete everything, then do not copy! 145 | if delres: 146 | return delres 147 | 148 | # get the tree we have to copy 149 | tlist = create_treelist(dc,src) 150 | result = {} 151 | 152 | # Extract the path out of the source URI. 153 | src_path = urllib.parse.urlparse(src).path 154 | 155 | # Parse the destination URI. 156 | # We'll be using it to construct destination URIs, 157 | # so we don't just retain the path, like we did with 158 | # the source. 159 | dst_parsed = urllib.parse.urlparse(dst) 160 | 161 | for element in tlist: 162 | problem_uris = list(result.keys()) 163 | 164 | # now URIs get longer and longer thus we have 165 | # to test if we had a parent URI which we were not 166 | # able to copy in problem_uris which is the prefix 167 | # of the actual element. If it is, then we cannot 168 | # copy this as well but do not generate another error. 169 | ok=True 170 | for p in problem_uris: 171 | if is_prefix(p,element): 172 | ok=False 173 | break 174 | 175 | if not ok: 176 | continue 177 | 178 | # Find the element's path relative to the source. 179 | element_path = urllib.parse.urlparse(element).path 180 | element_path_rel = os.path.relpath(element_path, start=src_path) 181 | # Append this relative path to the destination. 182 | if element_path_rel == '.': 183 | # os.path.relpath("/somedir", start="/somedir") returns 184 | # a result of ".", which we don't want to append to the 185 | # destination path. 186 | dst_path = dst_parsed.path 187 | else: 188 | dst_path = os.path.join(dst_parsed.path, element_path_rel) 189 | 190 | # Generate destination URI using our derived destination path. 191 | dst_uri = urllib.parse.urlunparse(dst_parsed._replace(path=os.path.join(dst_parsed.path, element_path_rel))) 192 | 193 | 194 | # now copy stuff 195 | try: 196 | copy(dc,element,dst_uri) 197 | except DAV_Error as error: 198 | (ec,dd) = error.args 199 | result[element]=ec 200 | 201 | return result 202 | 203 | 204 | ### 205 | ### MOVE 206 | ### 207 | 208 | 209 | def moveone(dc,src,dst,overwrite=None): 210 | """ move a single resource 211 | 212 | This is done by first copying it and then deleting 213 | the original. 214 | """ 215 | 216 | # first copy it 217 | copyone(dc, src, dst, overwrite) 218 | 219 | # then delete it 220 | dc.rm(src) 221 | 222 | def movetree(dc,src,dst,overwrite=None): 223 | """ move a collection 224 | 225 | This is done by first copying it and then deleting 226 | the original. 227 | 228 | PROBLEM: if something did not copy then we have a problem 229 | when deleting as the original might get deleted! 230 | """ 231 | 232 | # first copy it 233 | res = copytree(dc,src,dst,overwrite) 234 | 235 | # TODO: shouldn't we check res for errors and bail out before 236 | # the delete if we find any? 237 | # TODO: or, better yet, is there anything preventing us from 238 | # reimplementing this using `shutil.move()`? 239 | 240 | # then delete it 241 | res = deltree(dc,src,exclude=res) 242 | 243 | return res 244 | 245 | -------------------------------------------------------------------------------- /pywebdav/lib/davcopy.py: -------------------------------------------------------------------------------- 1 | import xml.dom.minidom 2 | domimpl = xml.dom.minidom.getDOMImplementation() 3 | 4 | import string 5 | from six.moves import urllib 6 | from io import StringIO 7 | 8 | from . import utils 9 | from .constants import COLLECTION, OBJECT, DAV_PROPS, RT_ALLPROP, RT_PROPNAME, RT_PROP 10 | from .errors import * 11 | from .utils import create_treelist, quote_uri, gen_estring 12 | 13 | class COPY: 14 | """ copy resources and eventually create multistatus responses 15 | 16 | This module implements the COPY class which is responsible for 17 | copying resources. Usually the normal copy work is done in the 18 | interface class. This class only creates error messages if error 19 | occur. 20 | 21 | """ 22 | 23 | 24 | def __init__(self,dataclass,src_uri,dst_uri,overwrite): 25 | self.__dataclass=dataclass 26 | self.__src=src_uri 27 | self.__dst=dst_uri 28 | self.__overwrite=overwrite 29 | 30 | 31 | def single_action(self): 32 | """ copy a normal resources. 33 | 34 | We try to copy it and return the result code. 35 | This is for Depth==0 36 | 37 | """ 38 | 39 | dc=self.__dataclass 40 | base=self.__src 41 | 42 | ### some basic tests 43 | # test if dest exists and overwrite is false 44 | if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error(412) 45 | # test if src and dst are the same 46 | # (we assume that both uris are on the same server!) 47 | ps=urllib.parse.urlparse(self.__src)[2] 48 | pd=urllib.parse.urlparse(self.__dst)[2] 49 | if ps==pd: raise DAV_Error(403) 50 | 51 | return dc.copyone(self.__src,self.__dst,self.__overwrite) 52 | 53 | #return copyone(dc,self.__src,self.__dst,self.__overwrite) 54 | 55 | def tree_action(self): 56 | """ copy a tree of resources (a collection) 57 | 58 | Here we return a multistatus xml element. 59 | 60 | """ 61 | dc=self.__dataclass 62 | base=self.__src 63 | 64 | ### some basic tests 65 | # test if dest exists and overwrite is false 66 | if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error(412) 67 | # test if src and dst are the same 68 | # (we assume that both uris are on the same server!) 69 | ps=urllib.parse.urlparse(self.__src)[2] 70 | pd=urllib.parse.urlparse(self.__dst)[2] 71 | if ps==pd: raise DAV_Error(403) 72 | 73 | 74 | result=dc.copytree(self.__src,self.__dst,self.__overwrite) 75 | #result=copytree(dc,self.__src,self.__dst,self.__overwrite) 76 | 77 | if not result: return None 78 | 79 | ### 80 | ### create the multistatus XML element 81 | ### (this is also the same as in delete.py. 82 | ### we might make a common method out of it) 83 | ### 84 | 85 | doc = domimpl.createDocument(None, "D:multistatus", None) 86 | doc.documentElement.setAttribute("xmlns:D","DAV:") 87 | 88 | for el,ec in result.items(): 89 | re=doc.createElement("D:response") 90 | hr=doc.createElement("D:href") 91 | st=doc.createElement("D:status") 92 | huri=doc.createTextNode(quote_uri(el)) 93 | t=doc.createTextNode(gen_estring(ec)) 94 | st.appendChild(t) 95 | hr.appendChild(huri) 96 | re.appendChild(hr) 97 | re.appendChild(st) 98 | ms.appendChild(re) 99 | 100 | return doc.toxml(encoding="utf-8") + b"\n" 101 | -------------------------------------------------------------------------------- /pywebdav/lib/davmove.py: -------------------------------------------------------------------------------- 1 | from six.moves import urllib 2 | 3 | from . import utils 4 | from .constants import COLLECTION, OBJECT, DAV_PROPS 5 | from .constants import RT_ALLPROP, RT_PROPNAME, RT_PROP 6 | from .errors import * 7 | from .utils import create_treelist, quote_uri, gen_estring, make_xmlresponse, is_prefix 8 | from .davcmd import moveone, movetree 9 | 10 | class MOVE: 11 | """ move resources and eventually create multistatus responses 12 | 13 | This module implements the MOVE class which is responsible for 14 | moving resources. 15 | 16 | MOVE is implemented by a COPY followed by a DELETE of the old 17 | resource. 18 | 19 | """ 20 | 21 | 22 | def __init__(self,dataclass,src_uri,dst_uri,overwrite): 23 | self.__dataclass=dataclass 24 | self.__src=src_uri 25 | self.__dst=dst_uri 26 | self.__overwrite=overwrite 27 | 28 | 29 | def single_action(self): 30 | """ move a normal resources. 31 | 32 | We try to move it and return the result code. 33 | This is for Depth==0 34 | 35 | """ 36 | 37 | dc=self.__dataclass 38 | base=self.__src 39 | 40 | ### some basic tests 41 | # test if dest exists and overwrite is false 42 | if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error(412) 43 | # test if src and dst are the same 44 | # (we assume that both uris are on the same server!) 45 | ps=urllib.parse.urlparse(self.__src)[2] 46 | pd=urllib.parse.urlparse(self.__dst)[2] 47 | if ps==pd: raise DAV_Error(403) 48 | 49 | return dc.moveone(self.__src,self.__dst,self.__overwrite) 50 | 51 | def tree_action(self): 52 | """ move a tree of resources (a collection) 53 | 54 | Here we return a multistatus xml element. 55 | 56 | """ 57 | dc=self.__dataclass 58 | base=self.__src 59 | 60 | ### some basic tests 61 | # test if dest exists and overwrite is false 62 | if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error(412) 63 | # test if src and dst are the same 64 | # (we assume that both uris are on the same server!) 65 | ps=urllib.parse.urlparse(self.__src)[2] 66 | pd=urllib.parse.urlparse(self.__dst)[2] 67 | if is_prefix(ps, pd): 68 | raise DAV_Error(403) 69 | 70 | result=dc.movetree(self.__src,self.__dst,self.__overwrite) 71 | if not result: return None 72 | 73 | # create the multistatus XML element 74 | return make_xmlresponse(result) 75 | 76 | -------------------------------------------------------------------------------- /pywebdav/lib/dbconn.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | log = logging.getLogger(__name__) 4 | 5 | try: 6 | import MySQLdb 7 | except ImportError: 8 | log.info('No SQL support - MySQLdb missing...') 9 | pass 10 | 11 | 12 | class Mconn: 13 | def connect(self,username,userpasswd,host,port,db): 14 | try: connection = MySQLdb.connect(host=host, port=int(port), user=username, passwd=userpasswd,db=db) 15 | except MySQLdb.OperationalError as message: 16 | log.error("%d:\n%s" % (message[ 0 ], message[ 1 ] )) 17 | return 0 18 | else: 19 | self.db = connection.cursor() 20 | 21 | return 1 22 | 23 | def execute(self,qry): 24 | if self.db: 25 | try: res=self.db.execute(qry) 26 | except MySQLdb.OperationalError as message: 27 | log.error("Error %d:\n%s" % (message[ 0 ], message[ 1 ] )) 28 | return 0 29 | 30 | except MySQLdb.ProgrammingError as message: 31 | log.error("Error %d:\n%s" % (message[ 0 ], message[ 1 ] )) 32 | return 0 33 | 34 | else: 35 | log.debug('Query Returned '+str(res)+' results') 36 | return self.db.fetchall() 37 | 38 | def create_user(self,user,passwd): 39 | qry="select * from Users where User='%s'"%(user) 40 | res=self.execute(qry) 41 | if not res or len(res) ==0: 42 | qry="insert into Users (User,Pass) values('%s','%s')"%(user,passwd) 43 | res=self.execute(qry) 44 | else: 45 | log.debug("Username already in use") 46 | 47 | def create_table(self): 48 | qry="""CREATE TABLE `Users` ( 49 | `uid` int(10) NOT NULL auto_increment, 50 | `User` varchar(60) default NULL, 51 | `Pass` varchar(60) default NULL, 52 | `Write` tinyint(1) default '0', 53 | PRIMARY KEY (`uid`) 54 | ) ENGINE=MyISAM DEFAULT CHARSET=latin1""" 55 | self.execute(qry) 56 | 57 | 58 | def first_run(self,user,passwd): 59 | res= self.execute('select * from Users') 60 | if res or type(res)==type(()) : 61 | pass 62 | else: 63 | self.create_table() 64 | self.create_user(user,passwd) 65 | 66 | 67 | def __init__(self,user,password,host,port,db): 68 | self.db=0 69 | self.connect(user,password,host,port,db) 70 | 71 | -------------------------------------------------------------------------------- /pywebdav/lib/delete.py: -------------------------------------------------------------------------------- 1 | from .utils import gen_estring, quote_uri, make_xmlresponse 2 | from .davcmd import deltree 3 | 4 | class DELETE: 5 | 6 | def __init__(self,uri,dataclass): 7 | self.__dataclass=dataclass 8 | self.__uri=uri 9 | 10 | def delcol(self): 11 | """ delete a collection """ 12 | 13 | dc=self.__dataclass 14 | result=dc.deltree(self.__uri) 15 | 16 | if not len(list(result.items())): 17 | return None # everything ok 18 | 19 | # create the result element 20 | return make_xmlresponse(result) 21 | 22 | def delone(self): 23 | """ delete a resource """ 24 | 25 | dc=self.__dataclass 26 | return dc.delone(self.__uri) 27 | 28 | -------------------------------------------------------------------------------- /pywebdav/lib/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Exceptions for the DAVserver implementation 4 | 5 | """ 6 | 7 | class DAV_Error(Exception): 8 | """ in general we can have the following arguments: 9 | 10 | 1. the error code 11 | 2. the error result element, e.g. a element 12 | """ 13 | 14 | def __init__(self,*args): 15 | if len(args)==1: 16 | self.args=(args[0],"") 17 | else: 18 | self.args=args 19 | 20 | class DAV_Secret(DAV_Error): 21 | """ the user is not allowed to know anything about it 22 | 23 | returning this for a property value means to exclude it 24 | from the response xml element. 25 | """ 26 | 27 | def __init__(self): 28 | DAV_Error.__init__(self,0) 29 | pass 30 | 31 | class DAV_NotFound(DAV_Error): 32 | """ a requested property was not found for a resource """ 33 | 34 | def __init__(self,*args): 35 | if len(args): 36 | DAV_Error.__init__(self,404,args[0]) 37 | else: 38 | DAV_Error.__init__(self,404) 39 | 40 | pass 41 | 42 | class DAV_Forbidden(DAV_Error): 43 | """ a method on a resource is not allowed """ 44 | 45 | def __init__(self,*args): 46 | if len(args): 47 | DAV_Error.__init__(self,403,args[0]) 48 | else: 49 | DAV_Error.__init__(self,403) 50 | pass 51 | 52 | class DAV_Requested_Range_Not_Satisfiable(DAV_Error): 53 | """ none of the range-specifier values overlap the current extent 54 | of the selected resource """ 55 | 56 | def __init__(self, *args): 57 | if len(args): 58 | DAV_Error.__init__(self, 416, args[0]) 59 | else: 60 | DAV_Error.__init__(self, 416) 61 | pass 62 | 63 | -------------------------------------------------------------------------------- /pywebdav/lib/iface.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | basic interface class 4 | 5 | use this for subclassing when writing your own interface 6 | class. 7 | 8 | """ 9 | 10 | from xml.dom import minidom 11 | from .locks import LockManager 12 | from .errors import * 13 | 14 | import time 15 | 16 | class dav_interface: 17 | """ interface class for implementing DAV servers """ 18 | 19 | ### defined properties (modify this but let the DAV stuff there!) 20 | ### the format is namespace: [list of properties] 21 | 22 | PROPS={"DAV:" : ('creationdate', 23 | 'displayname', 24 | 'getcontentlanguage', 25 | 'getcontentlength', 26 | 'getcontenttype', 27 | 'getetag', 28 | 'getlastmodified', 29 | 'lockdiscovery', 30 | 'resourcetype', 31 | 'source', 32 | 'supportedlock'), 33 | "NS2" : ("p1","p2") 34 | } 35 | 36 | # here we define which methods handle which namespace 37 | # the first item is the namespace URI and the second one 38 | # the method prefix 39 | # e.g. for DAV:getcontenttype we call dav_getcontenttype() 40 | M_NS={"DAV:" : "_get_dav", 41 | "NS2" : "ns2" } 42 | 43 | def get_propnames(self,uri): 44 | """ return the property names allowed for the given URI 45 | 46 | In this method we simply return the above defined properties 47 | assuming that they are valid for any resource. 48 | You can override this in order to return a different set 49 | of property names for each resource. 50 | 51 | """ 52 | return self.PROPS 53 | 54 | def get_prop2(self,uri,ns,pname): 55 | """ return the value of a property 56 | 57 | """ 58 | if ns.lower() == "dav:": 59 | return self.get_dav(uri,pname) 60 | 61 | raise DAV_NotFound 62 | 63 | def get_prop(self,uri,ns,propname): 64 | """ return the value of a given property 65 | 66 | uri -- uri of the object to get the property of 67 | ns -- namespace of the property 68 | pname -- name of the property 69 | """ 70 | if ns in self.M_NS: 71 | prefix=self.M_NS[ns] 72 | else: 73 | raise DAV_NotFound 74 | mname=prefix+"_"+propname.replace('-', '_') 75 | try: 76 | m=getattr(self,mname) 77 | r=m(uri) 78 | return r 79 | except AttributeError: 80 | raise DAV_NotFound 81 | 82 | ### 83 | ### DATA methods (for GET and PUT) 84 | ### 85 | 86 | def get_data(self, uri, range=None): 87 | """ return the content of an object 88 | 89 | return data or raise an exception 90 | 91 | """ 92 | raise DAV_NotFound 93 | 94 | def put(self, uri, data, content_type=None): 95 | """ write an object to the repository 96 | 97 | return the location uri or raise an exception 98 | """ 99 | 100 | raise DAV_Forbidden 101 | 102 | ### 103 | ### LOCKing information 104 | ### 105 | def _get_dav_supportedlock(self, uri): 106 | 107 | txt = ('
\n' 108 | '\n' 109 | '\n' 110 | '
\n') 111 | xml = minidom.parseString(txt) 112 | return xml.firstChild.firstChild 113 | 114 | def _get_dav_lockdiscovery(self, uri): 115 | lcm = LockManager() 116 | if lcm._l_isLocked(uri): 117 | lock = lcm._l_getLockForUri(uri) 118 | txt = lock.asXML(discover=True, namespace='D') 119 | 120 | txtwithns = '
%s
' 121 | xml = minidom.parseString(txtwithns % txt) 122 | return xml.firstChild.firstChild 123 | 124 | return '' 125 | 126 | ### 127 | ### Methods for DAV properties 128 | ### 129 | 130 | def _get_dav_creationdate(self,uri): 131 | """ return the creationdate of a resource """ 132 | d=self.get_creationdate(uri) 133 | # format it 134 | return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(d)) 135 | 136 | def _get_dav_getlastmodified(self,uri): 137 | """ return the last modified date of a resource """ 138 | d=self.get_lastmodified(uri) 139 | # format it 140 | return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(d)) 141 | 142 | 143 | ### 144 | ### OVERRIDE THESE! 145 | ### 146 | 147 | def get_creationdate(self,uri): 148 | """ return the creationdate of the resource """ 149 | return time.time() 150 | 151 | def get_lastmodified(self,uri): 152 | """ return the last modification date of the resource """ 153 | return time.time() 154 | 155 | 156 | ### 157 | ### COPY MOVE DELETE 158 | ### 159 | 160 | ### methods for deleting a resource 161 | 162 | def rmcol(self,uri): 163 | """ delete a collection 164 | 165 | This should not delete any children! This is automatically done 166 | before by the DELETE class in DAV/delete.py 167 | 168 | return a success code or raise an exception 169 | 170 | """ 171 | raise DAV_NotFound 172 | 173 | def rm(self,uri): 174 | """ delete a single resource 175 | 176 | return a success code or raise an exception 177 | 178 | """ 179 | raise DAV_NotFound 180 | 181 | """ 182 | 183 | COPY/MOVE HANDLER 184 | 185 | These handler are called when a COPY or MOVE method is invoked by 186 | a client. In the default implementation it works as follows: 187 | 188 | - the davserver receives a COPY/MOVE method 189 | - the davcopy or davmove module will be loaded and the corresponding 190 | class will be initialized 191 | - this class parses the query and decides which method of the interface class 192 | to call: 193 | 194 | copyone for a single resource to copy 195 | copytree for a tree to copy (collection) 196 | (the same goes for move of course). 197 | 198 | - the interface class has now two options: 199 | 1. to handle the action directly (e.g. cp or mv on filesystems) 200 | 2. to let it handle via the copy/move methods in davcmd. 201 | 202 | ad 1) The first approach can be used when we know that no error can 203 | happen inside a tree or when the action can exactly tell which 204 | element made which error. We have to collect these and return 205 | it in a dict of the form {uri: error_code, ...} 206 | 207 | ad 2) The copytree/movetree/... methods of davcmd.py will do the recursion 208 | themselves and call for each resource the copy/move method of the 209 | interface class. Thus method will then only act on a single resource. 210 | (Thus a copycol on a normal unix filesystem actually only needs to do 211 | an mkdir as the content will be copied by the davcmd.py function. 212 | The davcmd.py method will also automatically collect all errors and 213 | return the dictionary described above. 214 | When you use 2) you also have to implement the copy() and copycol() 215 | methods in your interface class. See the example for details. 216 | 217 | To decide which approach is the best you have to decide if your application 218 | is able to generate errors inside a tree. E.g. a function which completely 219 | fails on a tree if one of the tree's childs fail is not what we need. Then 220 | 2) would be your way of doing it. 221 | Actually usually 2) is the better solution and should only be replaced by 222 | 1) if you really need it. 223 | 224 | The remaining question is if we should do the same for the DELETE method. 225 | 226 | """ 227 | 228 | ### MOVE handlers 229 | 230 | def moveone(self,src,dst,overwrite): 231 | """ move one resource with Depth=0 """ 232 | return moveone(self,src,dst,overwrite) 233 | 234 | def movetree(self,src,dst,overwrite): 235 | """ move a collection with Depth=infinity """ 236 | return movetree(self,src,dst,overwrite) 237 | 238 | ### COPY handlers 239 | 240 | def copyone(self,src,dst,overwrite): 241 | """ copy one resource with Depth=0 """ 242 | return copyone(self,src,dst,overwrite) 243 | 244 | def copytree(self,src,dst,overwrite): 245 | """ copy a collection with Depth=infinity """ 246 | return copytree(self,src,dst,overwrite) 247 | 248 | 249 | ### low level copy methods (you only need these for method 2) 250 | def copy(self,src,dst): 251 | """ copy a resource with depth==0 252 | 253 | You don't need to bother about overwrite or not. 254 | This has been done already. 255 | 256 | return a success code or raise an exception if something fails 257 | """ 258 | return 201 259 | 260 | 261 | def copycol(self,src,dst): 262 | """ copy a resource with depth==infinity 263 | 264 | You don't need to bother about overwrite or not. 265 | This has been done already. 266 | 267 | return a success code or raise an exception if something fails 268 | """ 269 | return 201 270 | 271 | ### some utility functions you need to implement 272 | 273 | def exists(self,uri): 274 | """ return 1 or None depending on if a resource exists """ 275 | return None # no 276 | 277 | def is_collection(self,uri): 278 | """ return 1 or None depending on if a resource is a collection """ 279 | return None # no 280 | 281 | -------------------------------------------------------------------------------- /pywebdav/lib/locks.py: -------------------------------------------------------------------------------- 1 | import time 2 | from six.moves import urllib 3 | import uuid 4 | 5 | import logging 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | import xml.dom 10 | from xml.dom import minidom 11 | 12 | from .utils import rfc1123_date, IfParser, tokenFinder 13 | from .errors import * 14 | 15 | tokens_to_lock = {} 16 | uris_to_token = {} 17 | 18 | class LockManager: 19 | """ Implements the locking backend and serves as MixIn for DAVRequestHandler """ 20 | 21 | def _init_locks(self): 22 | return tokens_to_lock, uris_to_token 23 | 24 | def _l_isLocked(self, uri): 25 | tokens, uris = self._init_locks() 26 | return uri in uris 27 | 28 | def _l_hasLock(self, token): 29 | tokens, uris = self._init_locks() 30 | return token in tokens 31 | 32 | def _l_getLockForUri(self, uri): 33 | tokens, uris = self._init_locks() 34 | return uris.get(uri, None) 35 | 36 | def _l_getLock(self, token): 37 | tokens, uris = self._init_locks() 38 | return tokens.get(token, None) 39 | 40 | def _l_delLock(self, token): 41 | tokens, uris = self._init_locks() 42 | if token in tokens: 43 | del uris[tokens[token].uri] 44 | del tokens[token] 45 | 46 | def _l_setLock(self, lock): 47 | tokens, uris = self._init_locks() 48 | tokens[lock.token] = lock 49 | uris[lock.uri] = lock 50 | 51 | def _lock_unlock_parse(self, body): 52 | doc = minidom.parseString(body) 53 | 54 | data = {} 55 | info = doc.getElementsByTagNameNS('DAV:', 'lockinfo')[0] 56 | data['lockscope'] = info.getElementsByTagNameNS('DAV:', 'lockscope')[0]\ 57 | .firstChild.localName 58 | data['locktype'] = info.getElementsByTagNameNS('DAV:', 'locktype')[0]\ 59 | .firstChild.localName 60 | data['lockowner'] = info.getElementsByTagNameNS('DAV:', 'owner') 61 | return data 62 | 63 | def _lock_unlock_create(self, uri, creator, depth, data): 64 | lock = LockItem(uri, creator, **data) 65 | iscollection = uri[-1] == '/' # very dumb collection check 66 | 67 | result = '' 68 | if depth == 'infinity' and iscollection: 69 | # locking of children/collections not yet supported 70 | pass 71 | 72 | if not self._l_isLocked(uri): 73 | self._l_setLock(lock) 74 | 75 | # because we do not handle children we leave result empty 76 | return lock.token, result 77 | 78 | def do_UNLOCK(self): 79 | """ Unlocks given resource """ 80 | 81 | dc = self.IFACE_CLASS 82 | 83 | if self._config.DAV.getboolean('verbose') is True: 84 | log.info('UNLOCKing resource %s' % self.headers) 85 | 86 | uri = urllib.parse.urljoin(self.get_baseuri(dc), self.path) 87 | uri = urllib.parse.unquote(uri) 88 | 89 | # check lock token - must contain a dash 90 | if not self.headers.get('Lock-Token', '').find('-')>0: 91 | return self.send_status(400) 92 | 93 | token = tokenFinder(self.headers.get('Lock-Token')) 94 | if self._l_isLocked(uri): 95 | self._l_delLock(token) 96 | 97 | self.send_body(None, 204, 'OK', 'OK') 98 | 99 | def do_LOCK(self): 100 | """ Locking is implemented via in-memory caches. No data is written to disk. """ 101 | 102 | dc = self.IFACE_CLASS 103 | 104 | log.info('LOCKing resource %s' % self.headers) 105 | 106 | body = None 107 | if 'Content-Length' in self.headers: 108 | l = self.headers['Content-Length'] 109 | body = self.rfile.read(int(l)) 110 | 111 | depth = self.headers.get('Depth', 'infinity') 112 | 113 | uri = urllib.parse.urljoin(self.get_baseuri(dc), self.path) 114 | uri = urllib.parse.unquote(uri) 115 | log.info('do_LOCK: uri = %s' % uri) 116 | 117 | ifheader = self.headers.get('If') 118 | alreadylocked = self._l_isLocked(uri) 119 | log.info('do_LOCK: alreadylocked = %s' % alreadylocked) 120 | 121 | if body and alreadylocked: 122 | # Full LOCK request but resource already locked 123 | self.responses[423] = ('Locked', 'Already locked') 124 | return self.send_status(423) 125 | 126 | elif body and not ifheader: 127 | # LOCK with XML information 128 | data = self._lock_unlock_parse(body) 129 | token, result = self._lock_unlock_create(uri, 'unknown', depth, data) 130 | 131 | if result: 132 | self.send_body(bytes(result, 'utf-8'), 207, 'Error', 'Error', 133 | 'text/xml; charset="utf-8"') 134 | 135 | else: 136 | lock = self._l_getLock(token) 137 | self.send_body(bytes(lock.asXML(), 'utf-8'), 200, 'OK', 'OK', 138 | 'text/xml; charset="utf-8"', 139 | {'Lock-Token' : '' % token}) 140 | 141 | 142 | else: 143 | # refresh request - refresh lock timeout 144 | taglist = IfParser(ifheader) 145 | found = 0 146 | for tag in taglist: 147 | for listitem in tag.list: 148 | token = tokenFinder(listitem) 149 | if token and self._l_hasLock(token): 150 | lock = self._l_getLock(token) 151 | timeout = self.headers.get('Timeout', 'Infinite') 152 | lock.setTimeout(timeout) # automatically refreshes 153 | found = 1 154 | 155 | self.send_body(bytes(lock.asXML(), 'utf-8'), 156 | 200, 'OK', 'OK', 'text/xml; encoding="utf-8"') 157 | break 158 | if found: 159 | break 160 | 161 | # we didn't find any of the tokens mentioned - means 162 | # that table was cleared or another error 163 | if not found: 164 | self.send_status(412) # precondition failed 165 | 166 | class LockItem: 167 | """ Lock with support for exclusive write locks. Some code taken from 168 | webdav.LockItem from the Zope project. """ 169 | 170 | def __init__(self, uri, creator, lockowner, depth=0, timeout='Infinite', 171 | locktype='write', lockscope='exclusive', token=None, **kw): 172 | 173 | self.uri = uri 174 | self.creator = creator 175 | self.owner = lockowner 176 | self.depth = depth 177 | self.timeout = timeout 178 | self.locktype = locktype 179 | self.lockscope = lockscope 180 | self.token = token and token or self.generateToken() 181 | self.modified = time.time() 182 | 183 | def getModifiedTime(self): 184 | return self.modified 185 | 186 | def refresh(self): 187 | self.modified = time.time() 188 | 189 | def isValid(self): 190 | now = time.time() 191 | modified = self.modified 192 | timeout = self.timeout 193 | return (modified + timeout) > now 194 | 195 | def generateToken(self): 196 | return str(uuid.uuid4()) 197 | 198 | def getTimeoutString(self): 199 | t = str(self.timeout) 200 | if t[-1] == 'L': t = t[:-1] 201 | return 'Second-%s' % t 202 | 203 | def setTimeout(self, timeout): 204 | self.timeout = timeout 205 | self.modified = time.time() 206 | 207 | def asXML(self, namespace='d', discover=False): 208 | owner_str = '' 209 | if isinstance(self.owner, str): 210 | owner_str = self.owner 211 | elif isinstance(self.owner, xml.dom.minicompat.NodeList) and len(self.owner): 212 | owner_str = "".join([node.toxml() for node in self.owner[0].childNodes]) 213 | 214 | token = self.token 215 | base = ('<%(ns)s:activelock>\n' 216 | ' <%(ns)s:locktype><%(ns)s:%(locktype)s/>\n' 217 | ' <%(ns)s:lockscope><%(ns)s:%(lockscope)s/>\n' 218 | ' <%(ns)s:depth>%(depth)s\n' 219 | ' <%(ns)s:owner>%(owner)s\n' 220 | ' <%(ns)s:timeout>%(timeout)s\n' 221 | ' <%(ns)s:locktoken>\n' 222 | ' <%(ns)s:href>opaquelocktoken:%(locktoken)s\n' 223 | ' \n' 224 | ' \n' 225 | ) % { 226 | 'ns': namespace, 227 | 'locktype': self.locktype, 228 | 'lockscope': self.lockscope, 229 | 'depth': self.depth, 230 | 'owner': owner_str, 231 | 'timeout': self.getTimeoutString(), 232 | 'locktoken': token, 233 | } 234 | 235 | if discover is True: 236 | return base 237 | 238 | s = """ 239 | 240 | 241 | %s 242 | 243 | """ % base 244 | 245 | return s 246 | -------------------------------------------------------------------------------- /pywebdav/lib/propfind.py: -------------------------------------------------------------------------------- 1 | import xml.dom.minidom 2 | domimpl = xml.dom.minidom.getDOMImplementation() 3 | 4 | import logging 5 | from six.moves import urllib 6 | 7 | from . import utils 8 | from .constants import RT_ALLPROP, RT_PROPNAME, RT_PROP 9 | from .errors import DAV_Error, DAV_NotFound 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class PROPFIND: 15 | """ parse a propfind xml element and extract props 16 | 17 | It will set the following instance vars: 18 | 19 | request_class : ALLPROP | PROPNAME | PROP 20 | proplist : list of properties 21 | nsmap : map of namespaces 22 | 23 | The list of properties will contain tuples of the form 24 | (element name, ns_prefix, ns_uri) 25 | 26 | 27 | """ 28 | 29 | def __init__(self, uri, dataclass, depth, body): 30 | self.request_type = None 31 | self.nsmap = {} 32 | self.proplist = {} 33 | self.default_ns = None 34 | self._dataclass = dataclass 35 | self._depth = str(depth) 36 | self._uri = uri.rstrip('/') 37 | self._has_body = None # did we parse a body? 38 | 39 | if dataclass.verbose: 40 | log.info('PROPFIND: Depth is %s, URI is %s' % (depth, uri)) 41 | 42 | if body: 43 | self.request_type, self.proplist, self.namespaces = \ 44 | utils.parse_propfind(body) 45 | self._has_body = True 46 | 47 | def createResponse(self): 48 | """ Create the multistatus response 49 | 50 | This will be delegated to the specific method 51 | depending on which request (allprop, propname, prop) 52 | was found. 53 | 54 | If we get a PROPNAME then we simply return the list with empty 55 | values which we get from the interface class 56 | 57 | If we get an ALLPROP we first get the list of properties and then 58 | we do the same as with a PROP method. 59 | 60 | """ 61 | 62 | # check if resource exists 63 | if not self._dataclass.exists(self._uri): 64 | raise DAV_NotFound 65 | 66 | df = None 67 | if self.request_type == RT_ALLPROP: 68 | df = self.create_allprop() 69 | 70 | if self.request_type == RT_PROPNAME: 71 | df = self.create_propname() 72 | 73 | if self.request_type == RT_PROP: 74 | df = self.create_prop() 75 | 76 | if df is not None: 77 | return df 78 | 79 | # no body means ALLPROP! 80 | df = self.create_allprop() 81 | return df 82 | 83 | def create_propname(self): 84 | """ create a multistatus response for the prop names """ 85 | 86 | dc = self._dataclass 87 | # create the document generator 88 | doc = domimpl.createDocument(None, "multistatus", None) 89 | ms = doc.documentElement 90 | ms.setAttribute("xmlns:D", "DAV:") 91 | ms.tagName = 'D:multistatus' 92 | 93 | if self._depth == "0": 94 | pnames = dc.get_propnames(self._uri) 95 | re = self.mk_propname_response(self._uri, pnames, doc) 96 | ms.appendChild(re) 97 | 98 | elif self._depth == "1": 99 | pnames = dc.get_propnames(self._uri) 100 | re = self.mk_propname_response(self._uri, pnames, doc) 101 | ms.appendChild(re) 102 | 103 | for newuri in dc.get_childs(self._uri): 104 | pnames = dc.get_propnames(newuri) 105 | re = self.mk_propname_response(newuri, pnames, doc) 106 | ms.appendChild(re) 107 | elif self._depth == 'infinity': 108 | uri_list = [self._uri] 109 | while uri_list: 110 | uri = uri_list.pop() 111 | pnames = dc.get_propnames(uri) 112 | re = self.mk_propname_response(uri, pnames, doc) 113 | ms.appendChild(re) 114 | uri_childs = self._dataclass.get_childs(uri) 115 | if uri_childs: 116 | uri_list.extend(uri_childs) 117 | 118 | return doc.toxml(encoding="utf-8") + b"\n" 119 | 120 | def create_allprop(self): 121 | """ return a list of all properties """ 122 | self.proplist = {} 123 | self.namespaces = [] 124 | for ns, plist in self._dataclass.get_propnames(self._uri).items(): 125 | self.proplist[ns] = plist 126 | self.namespaces.append(ns) 127 | 128 | return self.create_prop() 129 | 130 | def create_prop(self): 131 | """ handle a request 132 | 133 | This will 134 | 135 | 1. set up the -Framework 136 | 137 | 2. read the property values for each URI 138 | (which is dependant on the Depth header) 139 | This is done by the get_propvalues() method. 140 | 141 | 3. For each URI call the append_result() method 142 | to append the actual -Tag to the result 143 | document. 144 | 145 | We differ between "good" properties, which have been 146 | assigned a value by the interface class and "bad" 147 | properties, which resulted in an error, either 404 148 | (Not Found) or 403 (Forbidden). 149 | 150 | """ 151 | # create the document generator 152 | doc = domimpl.createDocument(None, "multistatus", None) 153 | ms = doc.documentElement 154 | ms.setAttribute("xmlns:D", "DAV:") 155 | ms.tagName = 'D:multistatus' 156 | 157 | if self._depth == "0": 158 | gp, bp = self.get_propvalues(self._uri) 159 | res = self.mk_prop_response(self._uri, gp, bp, doc) 160 | ms.appendChild(res) 161 | 162 | elif self._depth == "1": 163 | gp, bp = self.get_propvalues(self._uri) 164 | res = self.mk_prop_response(self._uri, gp, bp, doc) 165 | ms.appendChild(res) 166 | 167 | for newuri in self._dataclass.get_childs(self._uri): 168 | gp, bp = self.get_propvalues(newuri) 169 | res = self.mk_prop_response(newuri, gp, bp, doc) 170 | ms.appendChild(res) 171 | elif self._depth == 'infinity': 172 | uri_list = [self._uri] 173 | while uri_list: 174 | uri = uri_list.pop() 175 | gp, bp = self.get_propvalues(uri) 176 | res = self.mk_prop_response(uri, gp, bp, doc) 177 | ms.appendChild(res) 178 | uri_childs = self._dataclass.get_childs(uri) 179 | if uri_childs: 180 | uri_list.extend(uri_childs) 181 | 182 | return doc.toxml(encoding="utf-8") + b"\n" 183 | 184 | def mk_propname_response(self, uri, propnames, doc): 185 | """ make a new result element for a PROPNAME request 186 | 187 | This will simply format the propnames list. 188 | propnames should have the format {NS1 : [prop1, prop2, ...], NS2: ...} 189 | 190 | """ 191 | re = doc.createElement("D:response") 192 | 193 | if self._dataclass.baseurl: 194 | uri = self._dataclass.baseurl + '/' + '/'.join(uri.split('/')[3:]) 195 | 196 | # write href information 197 | uparts = urllib.parse.urlparse(uri) 198 | fileloc = uparts[2] 199 | href = doc.createElement("D:href") 200 | 201 | huri = doc.createTextNode(uparts[0] + '://' + 202 | '/'.join(uparts[1:2]) + 203 | urllib.parse.quote(fileloc)) 204 | href.appendChild(huri) 205 | re.appendChild(href) 206 | 207 | ps = doc.createElement("D:propstat") 208 | nsnum = 0 209 | 210 | for ns, plist in propnames.items(): 211 | # write prop element 212 | pr = doc.createElement("D:prop") 213 | nsp = "ns" + str(nsnum) 214 | pr.setAttribute("xmlns:" + nsp, ns) 215 | nsnum += 1 216 | 217 | # write propertynames 218 | for p in plist: 219 | pe = doc.createElement(nsp + ":" + p) 220 | pr.appendChild(pe) 221 | 222 | ps.appendChild(pr) 223 | re.appendChild(ps) 224 | 225 | return re 226 | 227 | def mk_prop_response(self, uri, good_props, bad_props, doc): 228 | """ make a new result element 229 | 230 | We differ between the good props and the bad ones for 231 | each generating an extra -Node (for each error 232 | one, that means). 233 | 234 | """ 235 | re = doc.createElement("D:response") 236 | # append namespaces to response 237 | nsnum = 0 238 | for nsname in self.namespaces: 239 | if nsname != 'DAV:': 240 | re.setAttribute("xmlns:ns" + str(nsnum), nsname) 241 | nsnum += 1 242 | 243 | if self._dataclass.baseurl: 244 | uri = self._dataclass.baseurl + '/' + '/'.join(uri.split('/')[3:]) 245 | 246 | # write href information 247 | uparts = urllib.parse.urlparse(uri) 248 | fileloc = uparts[2] 249 | href = doc.createElement("D:href") 250 | 251 | huri = doc.createTextNode(uparts[0] + '://' + 252 | '/'.join(uparts[1:2]) + 253 | urllib.parse.quote(fileloc)) 254 | href.appendChild(huri) 255 | re.appendChild(href) 256 | 257 | # write good properties 258 | ps = doc.createElement("D:propstat") 259 | if good_props: 260 | re.appendChild(ps) 261 | 262 | gp = doc.createElement("D:prop") 263 | for ns in good_props.keys(): 264 | if ns != 'DAV:': 265 | ns_prefix = "ns" + str(self.namespaces.index(ns)) + ":" 266 | else: 267 | ns_prefix = 'D:' 268 | for p, v in good_props[ns].items(): 269 | 270 | pe = doc.createElement(ns_prefix + str(p)) 271 | if isinstance(v, xml.dom.minidom.Element): 272 | pe.appendChild(v) 273 | elif isinstance(v, list): 274 | for val in v: 275 | pe.appendChild(val) 276 | else: 277 | if p == "resourcetype": 278 | if v == 1: 279 | ve = doc.createElement("D:collection") 280 | pe.appendChild(ve) 281 | else: 282 | ve = doc.createTextNode(v) 283 | pe.appendChild(ve) 284 | 285 | gp.appendChild(pe) 286 | 287 | ps.appendChild(gp) 288 | s = doc.createElement("D:status") 289 | t = doc.createTextNode("HTTP/1.1 200 OK") 290 | s.appendChild(t) 291 | ps.appendChild(s) 292 | re.appendChild(ps) 293 | 294 | # now write the errors! 295 | if len(list(bad_props.items())): 296 | 297 | # write a propstat for each error code 298 | for ecode in bad_props.keys(): 299 | ps = doc.createElement("D:propstat") 300 | re.appendChild(ps) 301 | bp = doc.createElement("D:prop") 302 | ps.appendChild(bp) 303 | 304 | for ns in bad_props[ecode].keys(): 305 | if ns != 'DAV:': 306 | ns_prefix = "ns" + str(self.namespaces.index(ns)) + ":" 307 | else: 308 | ns_prefix = 'D:' 309 | 310 | for p in bad_props[ecode][ns]: 311 | pe = doc.createElement(ns_prefix + str(p)) 312 | bp.appendChild(pe) 313 | 314 | s = doc.createElement("D:status") 315 | t = doc.createTextNode(utils.gen_estring(ecode)) 316 | s.appendChild(t) 317 | ps.appendChild(s) 318 | re.appendChild(ps) 319 | 320 | # return the new response element 321 | return re 322 | 323 | def get_propvalues(self, uri): 324 | """ create lists of property values for an URI 325 | 326 | We create two lists for an URI: the properties for 327 | which we found a value and the ones for which we 328 | only got an error, either because they haven't been 329 | found or the user is not allowed to read them. 330 | 331 | """ 332 | good_props = {} 333 | bad_props = {} 334 | 335 | ddc = self._dataclass 336 | for ns, plist in self.proplist.items(): 337 | good_props[ns] = {} 338 | for prop in plist: 339 | ec = 0 340 | try: 341 | r = ddc.get_prop(uri, ns, prop) 342 | good_props[ns][prop] = r 343 | except DAV_Error as error_code: 344 | ec = error_code.args[0] 345 | 346 | # ignore props with error_code if 0 (invisible) 347 | if ec == 0: 348 | continue 349 | 350 | if ec in bad_props: 351 | if ns in bad_props[ec]: 352 | bad_props[ec][ns].append(prop) 353 | else: 354 | bad_props[ec][ns] = [prop] 355 | else: 356 | bad_props[ec] = {ns: [prop]} 357 | 358 | return good_props, bad_props 359 | -------------------------------------------------------------------------------- /pywebdav/lib/report.py: -------------------------------------------------------------------------------- 1 | from .propfind import PROPFIND 2 | from xml.dom import minidom 3 | domimpl = minidom.getDOMImplementation() 4 | 5 | from .utils import get_parenturi 6 | 7 | class REPORT(PROPFIND): 8 | 9 | def __init__(self, uri, dataclass, depth, body): 10 | PROPFIND.__init__(self, uri, dataclass, depth, body) 11 | 12 | doc = minidom.parseString(body) 13 | 14 | self.filter = doc.documentElement 15 | 16 | def create_propname(self): 17 | """ create a multistatus response for the prop names """ 18 | 19 | dc=self._dataclass 20 | # create the document generator 21 | doc = domimpl.createDocument(None, "multistatus", None) 22 | ms = doc.documentElement 23 | ms.setAttribute("xmlns:D", "DAV:") 24 | ms.tagName = 'D:multistatus' 25 | 26 | if self._depth=="0": 27 | if self._uri in self._dataclass.get_childs(get_parenturi(self._uri), 28 | self.filter): 29 | pnames=dc.get_propnames(self._uri) 30 | re=self.mk_propname_response(self._uri,pnames, doc) 31 | ms.appendChild(re) 32 | 33 | elif self._depth=="1": 34 | if self._uri in self._dataclass.get_childs(get_parenturi(self._uri), 35 | self.filter): 36 | pnames=dc.get_propnames(self._uri) 37 | re=self.mk_propname_response(self._uri,pnames, doc) 38 | ms.appendChild(re) 39 | 40 | for newuri in dc.get_childs(self._uri, self.filter): 41 | pnames=dc.get_propnames(newuri) 42 | re=self.mk_propname_response(newuri,pnames, doc) 43 | ms.appendChild(re) 44 | elif self._depth=='infinity': 45 | uri_list = [self._uri] 46 | while uri_list: 47 | uri = uri_list.pop() 48 | if uri in self._dataclass.get_childs(get_parenturi(uri), 49 | self.filter): 50 | pnames=dc.get_propnames(uri) 51 | re=self.mk_propname_response(uri,pnames, doc) 52 | ms.appendChild(re) 53 | uri_childs = self._dataclass.get_childs(uri) 54 | if uri_childs: 55 | uri_list.extend(uri_childs) 56 | 57 | return doc.toxml(encoding="utf-8") + b"\n" 58 | 59 | def create_prop(self): 60 | """ handle a request 61 | 62 | This will 63 | 64 | 1. set up the -Framework 65 | 66 | 2. read the property values for each URI 67 | (which is dependant on the Depth header) 68 | This is done by the get_propvalues() method. 69 | 70 | 3. For each URI call the append_result() method 71 | to append the actual -Tag to the result 72 | document. 73 | 74 | We differ between "good" properties, which have been 75 | assigned a value by the interface class and "bad" 76 | properties, which resulted in an error, either 404 77 | (Not Found) or 403 (Forbidden). 78 | 79 | """ 80 | 81 | 82 | # create the document generator 83 | doc = domimpl.createDocument(None, "multistatus", None) 84 | ms = doc.documentElement 85 | ms.setAttribute("xmlns:D", "DAV:") 86 | ms.tagName = 'D:multistatus' 87 | 88 | if self._depth=="0": 89 | if self._uri in self._dataclass.get_childs(get_parenturi(self._uri), 90 | self.filter): 91 | gp,bp=self.get_propvalues(self._uri) 92 | res=self.mk_prop_response(self._uri,gp,bp,doc) 93 | ms.appendChild(res) 94 | 95 | elif self._depth=="1": 96 | if self._uri in self._dataclass.get_childs(get_parenturi(self._uri), 97 | self.filter): 98 | gp,bp=self.get_propvalues(self._uri) 99 | res=self.mk_prop_response(self._uri,gp,bp,doc) 100 | ms.appendChild(res) 101 | 102 | for newuri in self._dataclass.get_childs(self._uri, self.filter): 103 | gp,bp=self.get_propvalues(newuri) 104 | res=self.mk_prop_response(newuri,gp,bp,doc) 105 | ms.appendChild(res) 106 | elif self._depth=='infinity': 107 | uri_list = [self._uri] 108 | while uri_list: 109 | uri = uri_list.pop() 110 | if uri in self._dataclass.get_childs(get_parenturi(uri), 111 | self.filter): 112 | gp,bp=self.get_propvalues(uri) 113 | res=self.mk_prop_response(uri,gp,bp,doc) 114 | ms.appendChild(res) 115 | uri_childs = self._dataclass.get_childs(uri) 116 | if uri_childs: 117 | uri_list.extend(uri_childs) 118 | 119 | return doc.toxml(encoding="utf-8") + b"\n" 120 | 121 | -------------------------------------------------------------------------------- /pywebdav/lib/status.py: -------------------------------------------------------------------------------- 1 | 2 | STATUS_CODES={ 3 | 100: "Continue", 4 | 102: "Processing", 5 | 200: "Ok", 6 | 201: "Created", 7 | 204: "No Content", 8 | 207: "Multi-Status", 9 | 400: "Bad Request", 10 | 403: "Forbidden", 11 | 404: "Not Found", 12 | 405: "Method Not Allowed", 13 | 409: "Conflict", 14 | 412: "Precondition failed", 15 | 422: "Unprocessable Entity", 16 | 423: "Locked", 17 | 424: "Failed Dependency", 18 | 502: "Bad Gateway", 19 | 507: "Insufficient Storage" 20 | } 21 | -------------------------------------------------------------------------------- /pywebdav/lib/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | import re 3 | import os 4 | 5 | from xml.dom import minidom 6 | from six.moves import urllib 7 | from .constants import RT_ALLPROP, RT_PROPNAME, RT_PROP 8 | from six.moves.BaseHTTPServer import BaseHTTPRequestHandler 9 | 10 | def gen_estring(ecode): 11 | """ generate an error string from the given code """ 12 | ec=int(ecode) 13 | if ec in BaseHTTPRequestHandler.responses: 14 | return "HTTP/1.1 %s %s" %(ec, BaseHTTPRequestHandler.responses[ec][0]) 15 | else: 16 | return "HTTP/1.1 %s" %(ec) 17 | 18 | def parse_propfind(xml_doc): 19 | """ 20 | Parse an propfind xml file and return a list of props 21 | """ 22 | 23 | doc = minidom.parseString(xml_doc) 24 | 25 | request_type=None 26 | props={} 27 | namespaces=[] 28 | 29 | if doc.getElementsByTagNameNS("DAV:", "allprop"): 30 | request_type = RT_ALLPROP 31 | elif doc.getElementsByTagNameNS("DAV:", "propname"): 32 | request_type = RT_PROPNAME 33 | else: 34 | request_type = RT_PROP 35 | for i in doc.getElementsByTagNameNS("DAV:", "prop"): 36 | for e in i.childNodes: 37 | if e.nodeType != minidom.Node.ELEMENT_NODE: 38 | continue 39 | ns = e.namespaceURI 40 | ename = e.localName 41 | if ns in props: 42 | props[ns].append(ename) 43 | else: 44 | props[ns]=[ename] 45 | namespaces.append(ns) 46 | 47 | return request_type,props,namespaces 48 | 49 | 50 | def create_treelist(dataclass,uri): 51 | """ create a list of resources out of a tree 52 | 53 | This function is used for the COPY, MOVE and DELETE methods 54 | 55 | uri - the root of the subtree to flatten 56 | 57 | It will return the flattened tree as list 58 | 59 | """ 60 | queue=[uri] 61 | list=[uri] 62 | while len(queue): 63 | element=queue[-1] 64 | if dataclass.is_collection(element): 65 | childs=dataclass.get_childs(element) 66 | else: 67 | childs=[] 68 | if len(childs): 69 | list=list+childs 70 | # update queue 71 | del queue[-1] 72 | if len(childs): 73 | queue=queue+childs 74 | return list 75 | 76 | def is_prefix(uri1,uri2): 77 | """ returns True if uri1 is a prefix of uri2 """ 78 | path1 = urllib.parse.urlparse(uri1).path 79 | path2 = urllib.parse.urlparse(uri2).path 80 | return os.path.commonpath([path1, path2]) == path1 81 | 82 | def quote_uri(uri): 83 | """ quote an URL but not the protocol part """ 84 | up=urllib.parse.urlparse(uri) 85 | np=urllib.parse.quote(up[2]) 86 | return urllib.parse.urlunparse((up[0],up[1],np,up[3],up[4],up[5])) 87 | 88 | def get_uriparentpath(uri): 89 | """ extract the uri path and remove the last element """ 90 | up=urllib.parse.urlparse(uri) 91 | return "/".join(up[2].split("/")[:-1]) 92 | 93 | def get_urifilename(uri): 94 | """ extract the uri path and return the last element """ 95 | up=urllib.parse.urlparse(uri) 96 | return up[2].split("/")[-1] 97 | 98 | def get_parenturi(uri): 99 | """ return the parent of the given resource""" 100 | up=urllib.parse.urlparse(uri) 101 | np="/".join(up[2].split("/")[:-1]) 102 | return urllib.parse.urlunparse((up[0],up[1],np,up[3],up[4],up[5])) 103 | 104 | ### XML utilities 105 | 106 | def make_xmlresponse(result): 107 | """ construct a response from a dict of uri:error_code elements """ 108 | doc = minidom.getDOMImplementation().createDocument(None, "multistatus", None) 109 | doc.documentElement.setAttribute("xmlns:D","DAV:") 110 | doc.documentElement.tagName = "D:multistatus" 111 | 112 | for el,ec in result.items(): 113 | re=doc.createElementNS("DAV:","response") 114 | hr=doc.createElementNS("DAV:","href") 115 | st=doc.createElementNS("DAV:","status") 116 | huri=doc.createTextNode(quote_uri(el)) 117 | t=doc.createTextNode(gen_estring(ec)) 118 | st.appendChild(t) 119 | hr.appendChild(huri) 120 | re.appendChild(hr) 121 | re.appendChild(st) 122 | doc.documentElement.appendChild(re) 123 | 124 | return doc.toxml(encoding="utf-8") + b"\n" 125 | 126 | # taken from App.Common 127 | 128 | weekday_abbr = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] 129 | weekday_full = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 130 | 'Friday', 'Saturday', 'Sunday'] 131 | monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 132 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 133 | 134 | def rfc1123_date(ts=None): 135 | # Return an RFC 1123 format date string, required for 136 | # use in HTTP Date headers per the HTTP 1.1 spec. 137 | # 'Fri, 10 Nov 2000 16:21:09 GMT' 138 | if ts is None: ts=time.time() 139 | year, month, day, hh, mm, ss, wd, y, z = time.gmtime(ts) 140 | return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (weekday_abbr[wd], 141 | day, monthname[month], 142 | year, 143 | hh, mm, ss) 144 | def iso8601_date(ts=None): 145 | # Return an ISO 8601 formatted date string, required 146 | # for certain DAV properties. 147 | # '2000-11-10T16:21:09-08:00 148 | if ts is None: ts=time.time() 149 | return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(ts)) 150 | 151 | def rfc850_date(ts=None): 152 | # Return an HTTP-date formatted date string. 153 | # 'Friday, 10-Nov-00 16:21:09 GMT' 154 | if ts is None: ts=time.time() 155 | year, month, day, hh, mm, ss, wd, y, z = time.gmtime(ts) 156 | return "%s, %02d-%3s-%2s %02d:%02d:%02d GMT" % ( 157 | weekday_full[wd], 158 | day, monthname[month], 159 | str(year)[2:], 160 | hh, mm, ss) 161 | 162 | ### If: header handling support. IfParser returns a sequence of 163 | ### TagList objects in the order they were parsed which can then 164 | ### be used in WebDAV methods to decide whether an operation can 165 | ### proceed or to raise HTTP Error 412 (Precondition failed) 166 | IfHdr = re.compile( 167 | r"(?P<.+?>)?\s*\((?P[^)]+)\)" 168 | ) 169 | 170 | ListItem = re.compile( 171 | r"(?Pnot)?\s*(?P<[a-zA-Z]+:[^>]*>|\[.*?\])", 172 | re.I) 173 | 174 | class TagList: 175 | def __init__(self): 176 | self.resource = None 177 | self.list = [] 178 | self.NOTTED = 0 179 | 180 | def IfParser(hdr): 181 | out = [] 182 | i = 0 183 | while 1: 184 | m = IfHdr.search(hdr[i:]) 185 | if not m: break 186 | 187 | i = i + m.end() 188 | tag = TagList() 189 | tag.resource = m.group('resource') 190 | if tag.resource: # We need to delete < > 191 | tag.resource = tag.resource[1:-1] 192 | listitem = m.group('listitem') 193 | tag.NOTTED, tag.list = ListParser(listitem) 194 | out.append(tag) 195 | 196 | return out 197 | 198 | def tokenFinder(token): 199 | # takes a string like ' and returns the token 200 | # part. 201 | if not token: return None # An empty string was passed in 202 | if token[0] == '[': return None # An Etag was passed in 203 | if token[0] == '<': token = token[1:-1] 204 | return token[token.find(':')+1:] 205 | 206 | def ListParser(listitem): 207 | out = [] 208 | NOTTED = 0 209 | i = 0 210 | while 1: 211 | m = ListItem.search(listitem[i:]) 212 | if not m: break 213 | 214 | i = i + m.end() 215 | out.append(m.group('listitem')) 216 | if m.group('not'): NOTTED = 1 217 | 218 | return NOTTED, out 219 | -------------------------------------------------------------------------------- /pywebdav/server/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pywebdav/server/config.ini: -------------------------------------------------------------------------------- 1 | 2 | # PyWebDAV config.ini 3 | # Read documents before editing this file 4 | 5 | # Created 11:48 10-08-2006 By Vince Spicer 6 | 7 | 8 | [MySQL] 9 | 10 | # Mysql server information 11 | host=localhost 12 | port=3306 13 | user=root 14 | passwd=rootpw 15 | 16 | # Auth Database Table, Must exists in database prior to firstrun 17 | dbtable=webDav 18 | 19 | # Create User Database Table and Insert system user 20 | # Disable after the Table is created; for performance reasons 21 | firstrun=0 22 | 23 | [DAV] 24 | 25 | # Verbose? 26 | # verbose enabled is like loglevel = INFO 27 | verbose = 1 28 | 29 | #log level : DEBUG, INFO, WARNING, ERROR, CRITICAL (Default is WARNING) 30 | #loglevel = WARNING 31 | 32 | # main directory 33 | directory = /home/spamies/tmp 34 | 35 | # Server address 36 | port = 8081 37 | host = localhost 38 | 39 | # disable auth 40 | noauth = 0 41 | 42 | # Enable mysql auth 43 | mysql_auth=0 44 | 45 | # admin user 46 | user = test 47 | password = test00 48 | 49 | # daemonize? 50 | daemonize = 0 51 | daemonaction = start 52 | 53 | # instance counter 54 | counter = 0 55 | 56 | # mimetypes support 57 | mimecheck = 1 58 | 59 | # webdav level (1 = webdav level 2) 60 | lockemulation = 1 61 | 62 | # dav server base url 63 | baseurl = 64 | 65 | # internal features 66 | #chunked_http_response = 1 67 | #http_request_use_iterator = 0 68 | #http_response_use_iterator = 0 69 | 70 | -------------------------------------------------------------------------------- /pywebdav/server/daemonize.py: -------------------------------------------------------------------------------- 1 | #Copyright (c) 2005 Simon Pamies (s.pamies@banality.de) 2 | #Copyright (c) 2003 Clark Evans 3 | #Copyright (c) 2002 Noah Spurrier 4 | #Copyright (c) 2001 Juergen Hermann 5 | # 6 | #This library is free software; you can redistribute it and/or 7 | #modify it under the terms of the GNU Library General Public 8 | #License as published by the Free Software Foundation; either 9 | #version 2 of the License, or (at your option) any later version. 10 | # 11 | #This library is distributed in the hope that it will be useful, 12 | #but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | #Library General Public License for more details. 15 | # 16 | #You should have received a copy of the GNU Library General Public 17 | #License along with this library; if not, write to the Free 18 | #Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, 19 | #MA 02111-1307, USA 20 | 21 | 22 | ''' 23 | This module is used to fork the current process into a daemon. 24 | Almost none of this is necessary (or advisable) if your daemon 25 | is being started by inetd. In that case, stdin, stdout and stderr are 26 | all set up for you to refer to the network connection, and the fork()s 27 | and session manipulation should not be done (to avoid confusing inetd). 28 | Only the chdir() and umask() steps remain as useful. 29 | References: 30 | UNIX Programming FAQ 31 | 1.7 How do I get my program to act like a daemon? 32 | http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 33 | Advanced Programming in the Unix Environment 34 | W. Richard Stevens, 1992, Addison-Wesley, ISBN 0-201-56317-7. 35 | 36 | History: 37 | 2005/06/23 by Simon Pamies 38 | 2001/07/10 by Juergen Hermann 39 | 2002/08/28 by Noah Spurrier 40 | 2003/02/24 by Clark Evans 41 | 42 | http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 43 | ''' 44 | import sys, os, time 45 | from signal import SIGTERM 46 | 47 | def deamonize(stdout='/dev/null', stderr=None, stdin='/dev/null', 48 | pidfile=None, startmsg = 'started with pid %s' ): 49 | ''' 50 | This forks the current process into a daemon. 51 | The stdin, stdout, and stderr arguments are file names that 52 | will be opened and be used to replace the standard file descriptors 53 | in sys.stdin, sys.stdout, and sys.stderr. 54 | These arguments are optional and default to /dev/null. 55 | Note that stderr is opened unbuffered, so 56 | if it shares a file with stdout then interleaved output 57 | may not appear in the order that you expect. 58 | ''' 59 | # Do first fork. 60 | try: 61 | pid = os.fork() 62 | if pid > 0: sys.exit(0) # Exit first parent. 63 | except OSError as e: 64 | sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror)) 65 | sys.exit(1) 66 | 67 | # Decouple from parent environment. 68 | os.chdir("/") 69 | os.umask(0) 70 | os.setsid() 71 | 72 | # Do second fork. 73 | try: 74 | pid = os.fork() 75 | if pid > 0: sys.exit(0) # Exit second parent. 76 | except OSError as e: 77 | sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror)) 78 | sys.exit(1) 79 | 80 | # Open file descriptors and print start message 81 | if not stderr: stderr = stdout 82 | si = open(stdin, 'r') 83 | so = open(stdout, 'a+') 84 | se = open(stderr, 'a+', 0) 85 | pid = str(os.getpid()) 86 | sys.stderr.write("\n%s\n" % startmsg % pid) 87 | sys.stderr.flush() 88 | if pidfile: open(pidfile,'w+').write("%s\n" % pid) 89 | 90 | if sys.stdin.closed: sys.stdin = open('/dev/null', 'r') 91 | if sys.stdout.closed: sys.stdout = open('/dev/null', 'a+') 92 | if sys.stderr.closed: sys.stderr = open('/dev/null', 'a+') 93 | 94 | # Redirect standard file descriptors. 95 | os.dup2(si.fileno(), sys.stdin.fileno()) 96 | os.dup2(so.fileno(), sys.stdout.fileno()) 97 | os.dup2(se.fileno(), sys.stderr.fileno()) 98 | 99 | def startstop(stdout='/dev/null', stderr=None, stdin='/dev/null', 100 | pidfile='pid.txt', startmsg = 'started with pid %s', action='start' ): 101 | if action: 102 | try: 103 | pf = open(pidfile,'r') 104 | pid = int(pf.read().strip()) 105 | pf.close() 106 | except IOError: 107 | pid = None 108 | 109 | if 'stop' == action or 'restart' == action: 110 | if not pid: 111 | mess = "Could not stop, pid file '%s' missing.\n" 112 | sys.stderr.write(mess % pidfile) 113 | if 'stop' == action: 114 | sys.exit(1) 115 | action = 'start' 116 | pid = None 117 | else: 118 | try: 119 | while 1: 120 | os.kill(pid,SIGTERM) 121 | time.sleep(1) 122 | except OSError as err: 123 | err = str(err) 124 | if err.find("No such process") > 0: 125 | os.remove(pidfile) 126 | if 'stop' == action: 127 | sys.exit(0) 128 | action = 'start' 129 | pid = None 130 | else: 131 | print(str(err)) 132 | sys.exit(1) 133 | 134 | if 'start' == action: 135 | if pid: 136 | mess = "Start aborted since pid file '%s' exists.\n" 137 | sys.stderr.write(mess % pidfile) 138 | sys.exit(1) 139 | 140 | deamonize(stdout,stderr,stdin,pidfile,startmsg) 141 | return 142 | 143 | if 'status' == action: 144 | if not pid: 145 | sys.stderr.write('Status: Stopped\n') 146 | 147 | else: sys.stderr.write('Status: Running\n') 148 | sys.exit(0) 149 | 150 | -------------------------------------------------------------------------------- /pywebdav/server/fileauth.py: -------------------------------------------------------------------------------- 1 | #Copyright (c) 1999 Christian Scholz (ruebe@aachen.heimat.de) 2 | # 3 | #This library is free software; you can redistribute it and/or 4 | #modify it under the terms of the GNU Library General Public 5 | #License as published by the Free Software Foundation; either 6 | #version 2 of the License, or (at your option) any later version. 7 | # 8 | #This library is distributed in the hope that it will be useful, 9 | #but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | #Library General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU Library General Public 14 | #License along with this library; if not, write to the Free 15 | #Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, 16 | #MA 02111-1307, USA 17 | 18 | """ 19 | Python WebDAV Server. 20 | 21 | This is an example implementation of a DAVserver using the DAV package. 22 | 23 | """ 24 | 25 | import logging 26 | 27 | from pywebdav.lib.WebDAVServer import DAVRequestHandler 28 | from pywebdav.lib.dbconn import Mconn 29 | 30 | from .fshandler import FilesystemHandler 31 | 32 | log = logging.getLogger() 33 | 34 | class DAVAuthHandler(DAVRequestHandler): 35 | """ 36 | Provides authentication based on parameters. The calling 37 | class has to inject password and username into this. 38 | (Variables: auth_user and auth_pass) 39 | """ 40 | 41 | # Do not forget to set IFACE_CLASS by caller 42 | # ex.: IFACE_CLASS = FilesystemHandler('/tmp', 'http://localhost/') 43 | verbose = False 44 | 45 | def _log(self, message): 46 | if self.verbose: 47 | log.info(message) 48 | 49 | def get_userinfo(self,user,pw,command): 50 | """ authenticate user """ 51 | 52 | if user == self._config.DAV.user and pw == self._config.DAV.password: 53 | log.info('Successfully authenticated user %s' % user) 54 | return 1 55 | 56 | log.info('Authentication failed for user %s' % user) 57 | return 0 58 | 59 | -------------------------------------------------------------------------------- /pywebdav/server/fshandler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import textwrap 3 | import logging 4 | import types 5 | import shutil 6 | from io import StringIO 7 | from six.moves import urllib 8 | from pywebdav.lib.constants import COLLECTION, OBJECT 9 | from pywebdav.lib.errors import * 10 | from pywebdav.lib.iface import * 11 | from pywebdav.lib.davcmd import copyone, copytree, moveone, movetree, delone, deltree 12 | from html import escape 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | BUFFER_SIZE = 128 * 1000 17 | # include magic support to correctly determine mimetypes 18 | MAGIC_AVAILABLE = False 19 | try: 20 | import mimetypes 21 | MAGIC_AVAILABLE = True 22 | log.info('Mimetype support ENABLED') 23 | except ImportError: 24 | log.info('Mimetype support DISABLED') 25 | pass 26 | 27 | class Resource: 28 | # XXX this class is ugly 29 | def __init__(self, fp, file_size): 30 | self.__fp = fp 31 | self.__file_size = file_size 32 | 33 | def __len__(self): 34 | return self.__file_size 35 | 36 | def __iter__(self): 37 | while 1: 38 | data = self.__fp.read(BUFFER_SIZE) 39 | if not data: 40 | break 41 | yield data 42 | time.sleep(0.005) 43 | self.__fp.close() 44 | 45 | def read(self, length = 0): 46 | if length == 0: 47 | length = self.__file_size 48 | 49 | data = self.__fp.read(length) 50 | return data 51 | 52 | 53 | class FilesystemHandler(dav_interface): 54 | """ 55 | Model a filesystem for DAV 56 | 57 | This class models a regular filesystem for the DAV server 58 | 59 | The basic URL will be http://localhost/ 60 | And the underlying filesystem will be /tmp 61 | 62 | Thus http://localhost/gfx/pix will lead 63 | to /tmp/gfx/pix 64 | 65 | """ 66 | 67 | def __init__(self, directory, uri, verbose=False): 68 | self.setDirectory(directory) 69 | self.setBaseURI(uri) 70 | 71 | # should we be verbose? 72 | self.verbose = verbose 73 | log.info('Initialized with %s %s' % (directory, uri)) 74 | 75 | def setDirectory(self, path): 76 | """ Sets the directory """ 77 | 78 | if not os.path.isdir(path): 79 | raise Exception('%s not must be a directory!' % path) 80 | 81 | self.directory = path 82 | 83 | def setBaseURI(self, uri): 84 | """ Sets the base uri """ 85 | 86 | self.baseuri = uri 87 | 88 | def uri2local(self,uri): 89 | """ map uri in baseuri and local part """ 90 | uparts=urllib.parse.urlparse(uri) 91 | fileloc=uparts[2][1:] 92 | filename=os.path.join(self.directory, fileloc) 93 | filename=os.path.normpath(filename) 94 | return filename 95 | 96 | def local2uri(self,filename): 97 | """ map local filename to self.baseuri """ 98 | 99 | pnum=len(self.directory.replace("\\","/").split("/")) 100 | parts=filename.replace("\\","/").split("/")[pnum:] 101 | sparts="/"+"/".join(parts) 102 | uri=urllib.parse.urljoin(self.baseuri,sparts) 103 | return uri 104 | 105 | 106 | def get_childs(self, uri, filter=None): 107 | """ return the child objects as self.baseuris for the given URI """ 108 | 109 | fileloc=self.uri2local(uri) 110 | filelist=[] 111 | 112 | if os.path.exists(fileloc): 113 | if os.path.isdir(fileloc): 114 | try: 115 | files=os.listdir(fileloc) 116 | except: 117 | raise DAV_NotFound 118 | 119 | for file in files: 120 | newloc=os.path.join(fileloc,file) 121 | filelist.append(self.local2uri(newloc)) 122 | 123 | log.info('get_childs: Childs %s' % filelist) 124 | 125 | return filelist 126 | 127 | def _get_listing(self, path): 128 | """Return a directory listing similar to http.server's""" 129 | 130 | template = textwrap.dedent(""" 131 | 132 | Directory listing for {path} 133 | 134 |

Directory listing for {path}

135 |
136 |
    137 | {items} 138 |
139 |
140 | 141 | 142 | """) 143 | escapeditems = (escape(i) + ('/' if os.path.isdir(os.path.join(path, i)) else '') for i in os.listdir(path) if not i.startswith('.')) 144 | htmlitems = "\n".join('
  • {i}
  • '.format(i=i) for i in escapeditems) 145 | 146 | return template.format(items=htmlitems, path=path) 147 | 148 | def get_data(self,uri, range = None): 149 | """ return the content of an object """ 150 | 151 | path=self.uri2local(uri) 152 | if os.path.exists(path): 153 | if os.path.isfile(path): 154 | file_size = os.path.getsize(path) 155 | if range is None: 156 | fp=open(path,"rb") 157 | log.info('Serving content of %s' % uri) 158 | return Resource(fp, file_size) 159 | else: 160 | if range[1] == '': 161 | range[1] = file_size 162 | else: 163 | range[1] = int(range[1]) 164 | 165 | if range[0] == '': 166 | range[0] = file_size - range[1] 167 | else: 168 | range[0] = int(range[0]) 169 | 170 | if range[0] > file_size: 171 | raise DAV_Requested_Range_Not_Satisfiable 172 | 173 | if range[1] > file_size: 174 | range[1] = file_size 175 | 176 | fp=open(path,"rb") 177 | fp.seek(range[0]) 178 | log.info('Serving range %s -> %s content of %s' % (range[0], range[1], uri)) 179 | return Resource(fp, range[1] - range[0]) 180 | elif os.path.isdir(path): 181 | msg = self._get_listing(path) 182 | return Resource(StringIO(msg), len(msg)) 183 | else: 184 | # also raise an error for collections 185 | # don't know what should happen then.. 186 | log.info('get_data: %s not found' % path) 187 | 188 | raise DAV_NotFound 189 | 190 | def _get_dav_resourcetype(self,uri): 191 | """ return type of object """ 192 | path=self.uri2local(uri) 193 | if os.path.isfile(path): 194 | return OBJECT 195 | 196 | elif os.path.isdir(path): 197 | return COLLECTION 198 | 199 | raise DAV_NotFound 200 | 201 | def _get_dav_displayname(self,uri): 202 | raise DAV_Secret # do not show 203 | 204 | def _get_dav_getcontentlength(self,uri): 205 | """ return the content length of an object """ 206 | path=self.uri2local(uri) 207 | if os.path.exists(path): 208 | if os.path.isfile(path): 209 | s=os.stat(path) 210 | return str(s[6]) 211 | 212 | return '0' 213 | 214 | def get_lastmodified(self,uri): 215 | """ return the last modified date of the object """ 216 | path=self.uri2local(uri) 217 | if os.path.exists(path): 218 | s=os.stat(path) 219 | date=s[8] 220 | return date 221 | 222 | raise DAV_NotFound 223 | 224 | def get_creationdate(self,uri): 225 | """ return the creation date of the object """ 226 | path=self.uri2local(uri) 227 | if os.path.exists(path): 228 | s=os.stat(path) 229 | date=s[9] 230 | return date 231 | 232 | raise DAV_NotFound 233 | 234 | def _get_dav_getcontenttype(self, uri): 235 | """ find out yourself! """ 236 | 237 | path=self.uri2local(uri) 238 | if os.path.exists(path): 239 | if os.path.isfile(path): 240 | if MAGIC_AVAILABLE is False \ 241 | or self.mimecheck is False: 242 | return 'application/octet-stream' 243 | else: 244 | ret, encoding = mimetypes.guess_type(path) 245 | 246 | # for non mimetype related result we 247 | # simply return an appropriate type 248 | if ret.find('/')==-1: 249 | if ret.find('text')>=0: 250 | return 'text/plain' 251 | else: 252 | return 'application/octet-stream' 253 | else: 254 | return ret 255 | 256 | elif os.path.isdir(path): 257 | return "httpd/unix-directory" 258 | 259 | raise DAV_NotFound('Could not find %s' % path) 260 | 261 | def put(self, uri, data, content_type=None): 262 | """ put the object into the filesystem """ 263 | path=self.uri2local(uri) 264 | try: 265 | with open(path, "bw+") as fp: 266 | if isinstance(data, types.GeneratorType): 267 | for d in data: 268 | fp.write(d) 269 | else: 270 | if data: 271 | fp.write(data) 272 | log.info('put: Created %s' % uri) 273 | except Exception as e: 274 | log.info('put: Could not create %s, %r', uri, e) 275 | raise DAV_Error(424) 276 | 277 | return None 278 | 279 | def mkcol(self,uri): 280 | """ create a new collection """ 281 | path=self.uri2local(uri) 282 | 283 | # remove trailing slash 284 | if path[-1]=="/": path=path[:-1] 285 | 286 | # test if file already exists 287 | if os.path.exists(path): 288 | raise DAV_Error(405) 289 | 290 | # test if parent exists 291 | h,t=os.path.split(path) 292 | if not os.path.exists(h): 293 | raise DAV_Error(409) 294 | 295 | # test, if we are allowed to create it 296 | try: 297 | os.mkdir(path) 298 | log.info('mkcol: Created new collection %s' % path) 299 | return 201 300 | except: 301 | log.info('mkcol: Creation of %s denied' % path) 302 | raise DAV_Forbidden 303 | 304 | ### ?? should we do the handler stuff for DELETE, too ? 305 | ### (see below) 306 | 307 | def rmcol(self,uri): 308 | """ delete a collection """ 309 | path=self.uri2local(uri) 310 | if not os.path.exists(path): 311 | raise DAV_NotFound 312 | 313 | try: 314 | shutil.rmtree(path) 315 | except OSError: 316 | raise DAV_Forbidden # forbidden 317 | 318 | return 204 319 | 320 | def rm(self,uri): 321 | """ delete a normal resource """ 322 | path=self.uri2local(uri) 323 | if not os.path.exists(path): 324 | raise DAV_NotFound 325 | 326 | try: 327 | os.unlink(path) 328 | except OSError as ex: 329 | log.info('rm: Forbidden (%s)' % ex) 330 | raise DAV_Forbidden # forbidden 331 | 332 | return 204 333 | 334 | ### 335 | ### DELETE handlers (examples) 336 | ### (we use the predefined methods in davcmd instead of doing 337 | ### a rm directly 338 | ### 339 | 340 | def delone(self,uri): 341 | """ delete a single resource 342 | 343 | You have to return a result dict of the form 344 | uri:error_code 345 | or None if everything's ok 346 | 347 | """ 348 | return delone(self,uri) 349 | 350 | def deltree(self,uri): 351 | """ delete a collection 352 | 353 | You have to return a result dict of the form 354 | uri:error_code 355 | or None if everything's ok 356 | """ 357 | 358 | return deltree(self,uri) 359 | 360 | 361 | ### 362 | ### MOVE handlers (examples) 363 | ### 364 | 365 | def moveone(self,src,dst,overwrite): 366 | """ move one resource with Depth=0 367 | """ 368 | 369 | return moveone(self,src,dst,overwrite) 370 | 371 | def movetree(self,src,dst,overwrite): 372 | """ move a collection with Depth=infinity 373 | """ 374 | 375 | return movetree(self,src,dst,overwrite) 376 | 377 | ### 378 | ### COPY handlers 379 | ### 380 | 381 | def copyone(self,src,dst,overwrite): 382 | """ copy one resource with Depth=0 383 | """ 384 | 385 | return copyone(self,src,dst,overwrite) 386 | 387 | def copytree(self,src,dst,overwrite): 388 | """ copy a collection with Depth=infinity 389 | """ 390 | 391 | return copytree(self,src,dst,overwrite) 392 | 393 | ### 394 | ### copy methods. 395 | ### This methods actually copy something. low-level 396 | ### They are called by the davcmd utility functions 397 | ### copytree and copyone (not the above!) 398 | ### Look in davcmd.py for further details. 399 | ### 400 | 401 | def copy(self,src,dst): 402 | """ copy a resource from src to dst """ 403 | 404 | srcfile=self.uri2local(src) 405 | dstfile=self.uri2local(dst) 406 | try: 407 | shutil.copy(srcfile, dstfile) 408 | except (OSError, IOError): 409 | log.info('copy: forbidden') 410 | raise DAV_Error(409) 411 | 412 | def copycol(self, src, dst): 413 | """ copy a collection. 414 | 415 | As this is not recursive (the davserver recurses itself) 416 | we will only create a new directory here. For some more 417 | advanced systems we might also have to copy properties from 418 | the source to the destination. 419 | """ 420 | 421 | return self.mkcol(dst) 422 | 423 | def exists(self,uri): 424 | """ test if a resource exists """ 425 | path=self.uri2local(uri) 426 | if os.path.exists(path): 427 | return 1 428 | return None 429 | 430 | def is_collection(self,uri): 431 | """ test if the given uri is a collection """ 432 | path=self.uri2local(uri) 433 | if os.path.isdir(path): 434 | return 1 435 | else: 436 | return 0 437 | -------------------------------------------------------------------------------- /pywebdav/server/mysqlauth.py: -------------------------------------------------------------------------------- 1 | #Copyright (c) 1999 Christian Scholz (ruebe@aachen.heimat.de) 2 | # 3 | #This library is free software; you can redistribute it and/or 4 | #modify it under the terms of the GNU Library General Public 5 | #License as published by the Free Software Foundation; either 6 | #version 2 of the License, or (at your option) any later version. 7 | # 8 | #This library is distributed in the hope that it will be useful, 9 | #but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | #Library General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU Library General Public 14 | #License along with this library; if not, write to the Free 15 | #Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, 16 | #MA 02111-1307, USA 17 | 18 | from .fileauth import DAVAuthHandler 19 | import sys 20 | 21 | class MySQLAuthHandler(DAVAuthHandler): 22 | """ 23 | Provides authentication based on a mysql table 24 | """ 25 | 26 | def get_userinfo(self,user,pw,command): 27 | """ authenticate user """ 28 | 29 | # Commands that need write access 30 | nowrite=['OPTIONS','PROPFIND','GET'] 31 | 32 | Mysql=self._config.MySQL 33 | DB=Mconn(Mysql.user,Mysql.passwd,Mysql.host,Mysql.port,Mysql.dbtable) 34 | if self.verbose: 35 | print(user,command, file=sys.stderr) 36 | 37 | qry="select * from %s.Users where User='%s' and Pass='%s'"%(Mysql.dbtable,user,pw) 38 | Auth=DB.execute(qry) 39 | 40 | if len(Auth) == 1: 41 | can_write=Auth[0][3] 42 | if not can_write and not command in nowrite: 43 | self._log('Authentication failed for user %s using command %s' %(user,command)) 44 | return 0 45 | else: 46 | self._log('Successfully authenticated user %s writable=%s' % (user,can_write)) 47 | return 1 48 | else: 49 | self._log('Authentication failed for user %s' % user) 50 | return 0 51 | 52 | self._log('Authentication failed for user %s' % user) 53 | return 0 54 | 55 | -------------------------------------------------------------------------------- /pywebdav/server/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Python WebDAV Server. 5 | 6 | This is an example implementation of a DAVserver using the DAV package. 7 | 8 | """ 9 | 10 | import getopt, sys, os 11 | import logging 12 | 13 | logging.basicConfig(level=logging.WARNING) 14 | log = logging.getLogger('pywebdav') 15 | 16 | from six.moves.BaseHTTPServer import HTTPServer 17 | from six.moves.socketserver import ThreadingMixIn 18 | 19 | try: 20 | import pywebdav.lib 21 | import pywebdav.server 22 | except ImportError: 23 | print('pywebdav.lib package not found! Please install into site-packages or set PYTHONPATH!') 24 | sys.exit(2) 25 | 26 | from pywebdav.server.fileauth import DAVAuthHandler 27 | from pywebdav.server.mysqlauth import MySQLAuthHandler 28 | from pywebdav.server.fshandler import FilesystemHandler 29 | from pywebdav.server.daemonize import startstop 30 | 31 | from pywebdav.lib.INI_Parse import Configuration 32 | from pywebdav import __version__, __author__ 33 | 34 | LEVELS = {'debug': logging.DEBUG, 35 | 'info': logging.INFO, 36 | 'warning': logging.WARNING, 37 | 'error': logging.ERROR, 38 | 'critical': logging.CRITICAL} 39 | 40 | class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): 41 | """Handle requests in a separate thread.""" 42 | 43 | def runserver( 44 | port = 8008, host='localhost', 45 | directory='/tmp', 46 | verbose = False, 47 | noauth = False, 48 | user = '', 49 | password = '', 50 | handler = DAVAuthHandler, 51 | server = ThreadedHTTPServer): 52 | 53 | directory = directory.strip() 54 | directory = directory.rstrip('/') 55 | host = host.strip() 56 | 57 | if not os.path.isdir(directory): 58 | os.makedirs(directory) 59 | # log.error('%s is not a valid directory!' % directory) 60 | # return sys.exit(233) 61 | 62 | # basic checks against wrong hosts 63 | if host.find('/') != -1 or host.find(':') != -1: 64 | log.error('Malformed host %s' % host) 65 | return sys.exit(233) 66 | 67 | # no root directory 68 | if directory == '/': 69 | log.error('Root directory not allowed!') 70 | sys.exit(233) 71 | 72 | # dispatch directory and host to the filesystem handler 73 | # This handler is responsible from where to take the data 74 | handler.IFACE_CLASS = FilesystemHandler(directory, 'http://%s:%s/' % (host, port), verbose ) 75 | 76 | # put some extra vars 77 | handler.verbose = verbose 78 | if noauth: 79 | log.warning('Authentication disabled!') 80 | handler.DO_AUTH = False 81 | 82 | log.info('Serving data from %s' % directory) 83 | 84 | if handler._config.DAV.getboolean('lockemulation') is False: 85 | log.info('Deactivated LOCK, UNLOCK (WebDAV level 2) support') 86 | 87 | handler.IFACE_CLASS.mimecheck = True 88 | if handler._config.DAV.getboolean('mimecheck') is False: 89 | handler.IFACE_CLASS.mimecheck = False 90 | log.info('Disabled mimetype sniffing (All files will have type application/octet-stream)') 91 | 92 | if handler._config.DAV.baseurl: 93 | log.info('Using %s as base url for PROPFIND requests' % handler._config.DAV.baseurl) 94 | handler.IFACE_CLASS.baseurl = handler._config.DAV.baseurl 95 | 96 | # initialize server on specified port 97 | runner = server( (host, port), handler ) 98 | print(('Listening on %s (%i)' % (host, port))) 99 | 100 | try: 101 | runner.serve_forever() 102 | except KeyboardInterrupt: 103 | log.info('Killed by user') 104 | 105 | usage = """PyWebDAV server (version %s) 106 | Standalone WebDAV server 107 | 108 | Make sure to activate LOCK, UNLOCK using parameter -J if you want 109 | to use clients like Windows Explorer or Mac OS X Finder that expect 110 | LOCK working for write support. 111 | 112 | Usage: ./server.py [OPTIONS] 113 | Parameters: 114 | -c, --config Specify a file where configuration is specified. In this 115 | file you can specify options for a running server. 116 | For an example look at the config.ini in this directory. 117 | -D, --directory Directory where to serve data from 118 | The user that runs this server must have permissions 119 | on that directory. NEVER run as root! 120 | Default directory is /tmp 121 | -B, --baseurl Behind a proxy pywebdav needs to generate other URIs for PROPFIND. 122 | If you are experiencing problems with links or such when behind 123 | a proxy then just set this to a sensible default (e.g. http://dav.domain.com). 124 | Make sure that you include the protocol. 125 | -H, --host Host where to listen on (default: localhost) 126 | -P, --port Port to bind server to (default: 8008) 127 | -u, --user Username for authentication 128 | -p, --password Password for given user 129 | -n, --noauth Pass parameter if server should not ask for authentication 130 | This means that every user has access 131 | -m, --mysql Pass this parameter if you want MySQL based authentication. 132 | If you want to use MySQL then the usage of a configuration 133 | file is mandatory. 134 | -J, --nolock Deactivate LOCK and UNLOCK mode (WebDAV Version 2). 135 | -M, --nomime Deactivate mimetype sniffing. Sniffing is based on magic numbers 136 | detection but can be slow under heavy load. If you are experiencing 137 | speed problems try to use this parameter. 138 | -T, --noiter Deactivate iterator. Use this if you encounter file corruption during 139 | download. Also disables chunked body response. 140 | -i, --icounter If you want to run multiple instances then you have to 141 | give each instance it own number so that logfiles and such 142 | can be identified. Default is 0 143 | -d, --daemon Make server act like a daemon. That means that it is going 144 | to background mode. All messages are redirected to 145 | logfiles (default: /tmp/pydav.log and /tmp/pydav.err). 146 | You need to pass one of the following values to this parameter 147 | start - Start daemon 148 | stop - Stop daemon 149 | restart - Restart complete server 150 | status - Returns status of server 151 | 152 | -v, --verbose Be verbose. 153 | -l, --loglevel Select the log level : DEBUG, INFO, WARNING, ERROR, CRITICAL 154 | Default is WARNING 155 | -h, --help Show this screen 156 | 157 | Please send bug reports and feature requests to %s 158 | """ % (__version__, __author__) 159 | 160 | def setupDummyConfig(**kw): 161 | 162 | class DummyConfigDAV: 163 | def __init__(self, **kw): 164 | self.__dict__.update(**kw) 165 | 166 | def getboolean(self, name): 167 | return (str(getattr(self, name, 0)) in ('1', "yes", "true", "on", "True")) 168 | 169 | class DummyConfig: 170 | DAV = DummyConfigDAV(**kw) 171 | 172 | return DummyConfig() 173 | 174 | def run(): 175 | verbose = False 176 | directory = '/tmp' 177 | port = 8008 178 | host = 'localhost' 179 | noauth = False 180 | user = '' 181 | password = '' 182 | daemonize = False 183 | daemonaction = 'start' 184 | counter = 0 185 | mysql = False 186 | lockemulation = True 187 | http_response_use_iterator = True 188 | chunked_http_response = True 189 | configfile = '' 190 | mimecheck = True 191 | loglevel = 'warning' 192 | baseurl = '' 193 | 194 | # parse commandline 195 | try: 196 | opts, args = getopt.getopt(sys.argv[1:], 'P:D:H:d:u:p:nvhmJi:c:Ml:TB:', 197 | ['host=', 'port=', 'directory=', 'user=', 'password=', 198 | 'daemon=', 'noauth', 'help', 'verbose', 'mysql', 199 | 'icounter=', 'config=', 'nolock', 'nomime', 'loglevel', 'noiter', 200 | 'baseurl=']) 201 | except getopt.GetoptError as e: 202 | print(usage) 203 | print('>>>> ERROR: %s' % str(e)) 204 | sys.exit(2) 205 | 206 | for o,a in opts: 207 | if o in ['-i', '--icounter']: 208 | counter = int(str(a).strip()) 209 | 210 | if o in ['-m', '--mysql']: 211 | mysql = True 212 | 213 | if o in ['-M', '--nomime']: 214 | mimecheck = False 215 | 216 | if o in ['-J', '--nolock']: 217 | lockemulation = False 218 | 219 | if o in ['-T', '--noiter']: 220 | http_response_use_iterator = False 221 | chunked_http_response = False 222 | 223 | if o in ['-c', '--config']: 224 | configfile = a 225 | 226 | if o in ['-D', '--directory']: 227 | directory = a 228 | 229 | if o in ['-H', '--host']: 230 | host = a 231 | 232 | if o in ['-P', '--port']: 233 | port = a 234 | 235 | if o in ['-v', '--verbose']: 236 | verbose = True 237 | 238 | if o in ['-l', '--loglevel']: 239 | loglevel = a.lower() 240 | 241 | if o in ['-h', '--help']: 242 | print(usage) 243 | sys.exit(2) 244 | 245 | if o in ['-n', '--noauth']: 246 | noauth = True 247 | 248 | if o in ['-u', '--user']: 249 | user = a 250 | 251 | if o in ['-p', '--password']: 252 | password = a 253 | 254 | if o in ['-d', '--daemon']: 255 | daemonize = True 256 | daemonaction = a 257 | 258 | if o in ['-B', '--baseurl']: 259 | baseurl = a.lower() 260 | 261 | # This feature are disabled because they are unstable 262 | http_request_use_iterator = 0 263 | 264 | conf = None 265 | if configfile != '': 266 | log.info('Reading configuration from %s' % configfile) 267 | conf = Configuration(configfile) 268 | 269 | dv = conf.DAV 270 | verbose = bool(int(dv.verbose)) 271 | loglevel = dv.get('loglevel', loglevel).lower() 272 | directory = dv.directory 273 | port = dv.port 274 | host = dv.host 275 | noauth = bool(int(dv.noauth)) 276 | user = dv.user 277 | password = dv.password 278 | daemonize = bool(int(dv.daemonize)) 279 | if daemonaction != 'stop': 280 | daemonaction = dv.daemonaction 281 | counter = int(dv.counter) 282 | lockemulation = dv.lockemulation 283 | mimecheck = dv.mimecheck 284 | 285 | if 'chunked_http_response' not in dv: 286 | dv.set('chunked_http_response', chunked_http_response) 287 | 288 | if 'http_request_use_iterator' not in dv: 289 | dv.set('http_request_use_iterator', http_request_use_iterator) 290 | 291 | if 'http_response_use_iterator' not in dv: 292 | dv.set('http_response_use_iterator', http_response_use_iterator) 293 | 294 | else: 295 | 296 | _dc = { 'verbose' : verbose, 297 | 'directory' : directory, 298 | 'port' : port, 299 | 'host' : host, 300 | 'noauth' : noauth, 301 | 'user' : user, 302 | 'password' : password, 303 | 'daemonize' : daemonize, 304 | 'daemonaction' : daemonaction, 305 | 'counter' : counter, 306 | 'lockemulation' : lockemulation, 307 | 'mimecheck' : mimecheck, 308 | 'chunked_http_response': chunked_http_response, 309 | 'http_request_use_iterator': http_request_use_iterator, 310 | 'http_response_use_iterator': http_response_use_iterator, 311 | 'baseurl' : baseurl 312 | } 313 | 314 | conf = setupDummyConfig(**_dc) 315 | 316 | if verbose and (LEVELS[loglevel] > LEVELS['info']): 317 | loglevel = 'info' 318 | 319 | logging.getLogger().setLevel(LEVELS[loglevel]) 320 | 321 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 322 | for handler in logging.getLogger().handlers: 323 | handler.setFormatter(formatter) 324 | 325 | if mysql == True and configfile == '': 326 | log.error('You can only use MySQL with configuration file!') 327 | sys.exit(3) 328 | 329 | if daemonaction != 'stop': 330 | log.info('Starting up PyWebDAV server (version %s)' % __version__) 331 | else: 332 | log.info('Stopping PyWebDAV server (version %s)' % __version__) 333 | 334 | if not noauth and daemonaction not in ['status', 'stop']: 335 | if not user: 336 | print(usage) 337 | print('>> ERROR: No parameter specified!', file=sys.stderr) 338 | print('>> Example: davserver -D /tmp -n', file=sys.stderr) 339 | sys.exit(3) 340 | 341 | if daemonaction == 'status': 342 | log.info('Checking for state...') 343 | 344 | if type(port) == type(''): 345 | port = int(port.strip()) 346 | 347 | log.info('chunked_http_response feature %s' % (conf.DAV.getboolean('chunked_http_response') and 'ON' or 'OFF' )) 348 | log.info('http_request_use_iterator feature %s' % (conf.DAV.getboolean('http_request_use_iterator') and 'ON' or 'OFF' )) 349 | log.info('http_response_use_iterator feature %s' % (conf.DAV.getboolean('http_response_use_iterator') and 'ON' or 'OFF' )) 350 | 351 | if daemonize: 352 | 353 | # check if pid file exists 354 | if os.path.exists('/tmp/pydav%s.pid' % counter) and daemonaction not in ['status', 'stop']: 355 | log.error( 356 | 'Found another instance! Either use -i to specifiy another instance number or remove /tmp/pydav%s.pid!' % counter) 357 | sys.exit(3) 358 | 359 | startstop(stdout='/tmp/pydav%s.log' % counter, 360 | stderr='/tmp/pydav%s.err' % counter, 361 | pidfile='/tmp/pydav%s.pid' % counter, 362 | startmsg='>> Started PyWebDAV (PID: %s)', 363 | action=daemonaction) 364 | 365 | # start now 366 | handler = DAVAuthHandler 367 | if mysql == True: 368 | handler = MySQLAuthHandler 369 | 370 | # injecting options 371 | handler._config = conf 372 | 373 | runserver(port, host, directory, verbose, noauth, user, password, 374 | handler=handler) 375 | 376 | if __name__ == '__main__': 377 | run() 378 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | from io import open 5 | import os 6 | 7 | import pywebdav 8 | 9 | README = open(os.path.join(os.path.dirname(__file__), 'README.md'), 'r', encoding='utf-8').read() 10 | 11 | setup(name='PyWebDAV3', 12 | description=pywebdav.__doc__, 13 | author=pywebdav.__author__, 14 | author_email=pywebdav.__email__, 15 | maintainer=pywebdav.__author__, 16 | maintainer_email=pywebdav.__email__, 17 | use_git_versioner="gitlab:desc:snapshot", 18 | url='https://github.com/andrewleech/PyWebDAV3', 19 | platforms=['Unix', 'Windows'], 20 | license=pywebdav.__license__, 21 | version=pywebdav.__version__, 22 | long_description=README, 23 | classifiers=[ 24 | 'Development Status :: 5 - Production/Stable', 25 | 'Environment :: Console', 26 | 'Environment :: Web Environment', 27 | 'Intended Audience :: Developers', 28 | 'Intended Audience :: System Administrators', 29 | 'License :: OSI Approved :: GNU General Public License (GPL)', 30 | 'Operating System :: MacOS :: MacOS X', 31 | 'Operating System :: POSIX', 32 | 'Programming Language :: Python', 33 | 'Topic :: Software Development :: Libraries', 34 | ], 35 | keywords=['webdav', 36 | 'server', 37 | 'dav', 38 | 'standalone', 39 | 'library', 40 | 'gpl', 41 | 'http', 42 | 'rfc2518', 43 | 'rfc 2518' 44 | ], 45 | packages=find_packages(), 46 | entry_points={ 47 | 'console_scripts': ['davserver = pywebdav.server.server:run'] 48 | }, 49 | install_requires = ['six'], 50 | setup_requires=['git-versioner'], 51 | ) 52 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | /litmus-0.13/ 2 | -------------------------------------------------------------------------------- /test/litmus-0.13.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewleech/PyWebDAV3/4d469487b968df55e2bfdf70f7f5276ac7f93153/test/litmus-0.13.tar.gz -------------------------------------------------------------------------------- /test/test_litmus.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import sys 4 | import time 5 | import shutil 6 | import tarfile 7 | import tempfile 8 | import unittest 9 | import subprocess 10 | from subprocess import run 11 | 12 | testdir = os.path.abspath(os.path.dirname(__file__)) 13 | sys.path.insert(0, os.path.join(testdir, '..')) 14 | 15 | import pywebdav.server.server 16 | 17 | # Run davserver 18 | user = 'test' 19 | password = 'pass' 20 | port = 38028 21 | 22 | class TestFilter: 23 | _suites = ['props'] 24 | _skipping = True 25 | 26 | def skipLine(self, line): 27 | if line.startswith("<- summary"): 28 | self._skipping = False 29 | else: 30 | for suite in self._suites: 31 | if line.startswith(f"-> running `{suite}"): 32 | self._skipping = True 33 | break 34 | return self._skipping 35 | 36 | class Test(unittest.TestCase): 37 | def setUp(self): 38 | 39 | self.rundir = tempfile.mkdtemp() 40 | self._ensure_litmus() 41 | 42 | def _ensure_litmus(self): 43 | 44 | self.litmus_dist = os.path.join(testdir, 'litmus-0.13') 45 | self.litmus = os.path.join(self.litmus_dist, 'litmus') 46 | if not os.path.exists(self.litmus): 47 | print('Compiling litmus test suite') 48 | 49 | if os.path.exists(self.litmus_dist): 50 | shutil.rmtree(self.litmus_dist) 51 | with tarfile.open(self.litmus_dist + '.tar.gz') as tf: 52 | tf.extractall(path=testdir) 53 | ret = run(['sh', './configure'], cwd=self.litmus_dist) 54 | # assert ret == 0 55 | ret = run(['make'], cwd=self.litmus_dist) 56 | # assert ret == 0 57 | litmus = os.path.join(self.litmus_dist, 'litmus') 58 | # assert os.path.exists(litmus) 59 | 60 | def tearDown(self): 61 | print("Cleaning up tempdir") 62 | shutil.rmtree(self.rundir) 63 | 64 | def test_run_litmus(self): 65 | 66 | result = [] 67 | proc = None 68 | try: 69 | print('Starting davserver') 70 | davserver_cmd = [sys.executable, os.path.join(testdir, '..', 'pywebdav', 'server', 'server.py'), '-D', 71 | self.rundir, '-u', user, '-p', password, '-H', 'localhost', '--port', str(port)] 72 | self.davserver_proc = subprocess.Popen(davserver_cmd) 73 | # Ensure davserver has time to startup 74 | time.sleep(1) 75 | 76 | # Run Litmus 77 | print('Running litmus') 78 | try: 79 | ret = run(["make", "URL=http://localhost:%d" % port, 'CREDS=%s %s' % (user, password), "check"], cwd=self.litmus_dist, capture_output=True) 80 | results = ret.stdout 81 | except subprocess.CalledProcessError as ex: 82 | results = ex.output 83 | lines = results.decode().split('\n') 84 | assert len(lines), "No litmus output" 85 | filter = TestFilter() 86 | for line in lines: 87 | line = line.split('\r')[-1] 88 | result.append(line) 89 | if filter.skipLine(line): 90 | continue 91 | if len(re.findall(r'^ *\d+\.', line)): 92 | assert line.endswith('pass'), line 93 | 94 | finally: 95 | print('\n'.join(result)) 96 | 97 | print('Stopping davserver') 98 | self.davserver_proc.kill() 99 | 100 | 101 | def test_run_litmus_noauth(self): 102 | 103 | result = [] 104 | proc = None 105 | try: 106 | print('Starting davserver') 107 | davserver_cmd = [sys.executable, os.path.join(testdir, '..', 'pywebdav', 'server', 'server.py'), '-D', 108 | self.rundir, '-n', '-H', 'localhost', '--port', str(port)] 109 | self.davserver_proc = subprocess.Popen(davserver_cmd) 110 | # Ensure davserver has time to startup 111 | time.sleep(1) 112 | 113 | # Run Litmus 114 | print('Running litmus') 115 | try: 116 | ret = run(["make", "URL=http://localhost:%d" % port, "check"], cwd=self.litmus_dist, capture_output=True) 117 | results = ret.stdout 118 | 119 | except subprocess.CalledProcessError as ex: 120 | results = ex.output 121 | lines = results.decode().split('\n') 122 | assert len(lines), "No litmus output" 123 | filter = TestFilter() 124 | for line in lines: 125 | line = line.split('\r')[-1] 126 | result.append(line) 127 | if filter.skipLine(line): 128 | continue 129 | if len(re.findall(r'^ *\d+\.', line)): 130 | assert line.endswith('pass'), line 131 | 132 | finally: 133 | print('\n'.join(result)) 134 | 135 | print('Stopping davserver') 136 | self.davserver_proc.kill() 137 | 138 | if __name__ == "__main__": 139 | unittest.main() 140 | --------------------------------------------------------------------------------