Merged changes from wsgidav-dev

This commit is contained in:
Martin Wendt 2009-11-04 22:49:02 +01:00
parent 95f139c291
commit a0e805913e
53 changed files with 3994 additions and 2608 deletions

10
.hgignore Normal file
View file

@ -0,0 +1,10 @@
glob:*.pyc
glob:*.confc
glob:build
glob:WsgiDAV.egg-info/
glob:docs/api
glob:*.shelve
glob:wsgidav.conf
glob:dist
glob:*.orig
glob:.settings

17
.project Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>wsgidav_dev</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

10
.pydevproject Normal file
View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?>
<pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.4</pydev_property>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/wsgidav_dev</path>
</pydev_pathproperty>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
</pydev_project>

View file

@ -9,6 +9,6 @@ ADDONS GUIDE
This section describes the addons available:
+ windowsdomaincontroller_
+ nt_domain_controller_

9
CHANGES Normal file
View file

@ -0,0 +1,9 @@
=======
CHANGES
=======
0.4.0 alpha
===========
See http://code.google.com/p/wsgidav/wiki/ChangeLog04

View file

@ -3,10 +3,10 @@ WsgiDAV Developers Guide
========================
:Authors: - Ho Chun Wei, fuzzybr80(at)gmail.com (original PyFileServer)
- Martin Wendt
- Martin Wendt (WsgiDAV)
:Project: WsgiDAV, http://wsgidav.googlecode.com/
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
:Abstract: This document gives a brief introduction to the WsgiDAV application package.
:Abstract: This document gives a brief introduction to the WsgiDAV application package (targeted to developers).
.. contents:: Table Of Contents
@ -36,15 +36,17 @@ WSGI application stack::
|
error_printer.ErrorPrinter (middleware)
|
httpauthentication.HTTPAuthenticator (middleware)
http_authenticator.HTTPAuthenticator (middleware)
| \- Uses a domain controller object
|
dir_browser.WsgiDavDirBrowser (middleware, optional)
|
requestresolver.RequestResolver (middleware)
request_resolver.RequestResolver (middleware)
|
*-> requestserver.RequestServer (application)
*-> request_server.RequestServer (application)
\- Uses a DAVProvider object
\- Uses a lock manager object
and a property manager object
See the following sections for details.
@ -59,6 +61,7 @@ Main module, that handles the HTTP requests. This object is passed to the WSGI
server and represents our WsgiDAV application to the outside.
On init:
Use the configuration dictionary to initialize lock manager, property manager,
domain controller.
@ -69,11 +72,9 @@ On init:
For every request:
Note: The OPTIONS method for the '*' path is handled directly.
Find the registered DAV provider for the current request.
Add or modify info in the WSGI ``eniron``:
Add or modify info in the WSGI ``environ``:
environ["SCRIPT_NAME"]
Mount-point of the current share.
@ -89,6 +90,7 @@ For every request:
Log the HTTP request, then pass the request to the first middleware.
Note: The OPTIONS method for the '*' path is handled directly.
Middleware ``debug_filter.WsgiDavDebugFilter``
@ -96,11 +98,13 @@ Middleware ``debug_filter.WsgiDavDebugFilter``
Optional middleware for debugging.
On init:
Define HTTP methods and litmus tests, that should turn on the verbose mode
(currently hard coded).
For every request:
Increase value of ``eniron['verbose']``, if the request should be debugged.
Increase value of ``environ['verbose']``, if the request should be debugged.
Also dump request and response headers and body.
Then pass the request to the next middleware.
@ -119,7 +123,7 @@ For every request:
Internal exceptions are converted to HTTP_INTERNAL_ERRORs.
Middleware ``httpauthentication.HTTPAuthenticator``
Middleware ``http_authenticator.HTTPAuthenticator``
---------------------------------------------------
Uses a domain controller to establish HTTP authentication.
@ -141,22 +145,26 @@ Middleware ``dir_browser.WsgiDavDirBrowser``
Handles GET requests on collections to display a HTML directory listing.
On init:
-
For every request:
If path maps to a collection:
Render collection members as directory (HTML table).
Middleware ``requestresolver.RequestResolver``
----------------------------------------------
Middleware ``request_resolver.RequestResolver``
-----------------------------------------------
Find the mapped DAV-Provider, create a new RequestServer instance, and dispatch
the request.
On init:
Store URL-to-DAV-Provider mapping.
For every request:
Setup ``environ["SCRIPT_NAME"]`` to request realm and and
``environ["PATH_INFO"]`` to resource path.
@ -166,21 +174,22 @@ For every request:
Note: The OPTIONS method for '*' is handled directly.
Application ``requestserver.RequestServer``
-------------------------------------------
Application ``request_server.RequestServer``
--------------------------------------------
Handles one single WebDAV request.
On init:
Store a reference to the DAV-Provider object.
For every request:
Handle one single WebDAV method (PROPFIND, PROPPATCH, LOCK, ...) using a
DAV-Provider instance. Then return the response body or raise an DAVError.
Note: this object only handles one single request.
API documentation
=================
Follow this link to browse the `API documentation`_.
@ -197,23 +206,23 @@ All DAV providers must implement a common interface. This is usually done by
deriving from the abstract base class ``dav_provider.DAVProvider``.
WsgiDAV comes with a DAV provider for file systems, called
``fs_dav_provider.FilesystemProvider``. That is why WsgiDAV a WebDAV file
``fs_dav_provider.FilesystemProvider``. That is why WsgiDAV is a WebDAV file
server out-of-the-box.
There are also a few other modules that may serve as examples on how to plug-in
your own custom DAV providers:
ReadOnlyFilesystemProvider
fs_dav_provider.ReadOnlyFilesystemProvider
Similar to ``FilesystemProvider``, but raise HTTP_FORBIDDEN on write access
attempts.
DummyDAVProvider
addons.dummy_dav_provider.DummyDAVProvider
TODO
VirtualResourceProvider
addons.virtual_dav_provider.VirtualResourceProvider
TODO
SimpleMySQLResourceAbstractionLayer
addons.mysql_dav_provider.MySQLBrowserProvider
TODO
@ -222,10 +231,16 @@ Property Managers
DAV providers may use a property manager to support persistence for *dead
properties*.
WsgiDAV comes with a default implementation based in shelve, called
``property_manager.PropertyManager``.
WsgiDAV comes with two default implementations, one based on a in-memory
dictionary, and a persistent one based in shelve::
However, this may be replaced by a custom version, as long as the required
property_manager.PropertyManager
property_manager.ShelvePropertyManager
``PropertyManager`` is used by default, but ``ShelvePropertyManager`` can be
enabled by uncommenting two lines in the configuration file.
In addition, this may be replaced by a custom version, as long as the required
interface is implemented.
@ -234,10 +249,16 @@ Lock Managers
DAV providers may use a lock manager to support exclusive and shared write
locking.
WsgiDAV comes with a default implementation based in shelve, called
``lock_manager.LockManager``.
WsgiDAV comes with two default implementations, one based on a in-memory
dictionary, and a persistent one based in shelve::
However, this may be replaced by a custom version, as long as the required
lock_manager.LockManager
lock_manager.ShelveLockManager
``LockManager`` is used by default, but ``ShelveLockManager`` can be
enabled by uncommenting two lines in the configuration file.
In addition, this may be replaced by a custom version, as long as the required
interface is implemented.
@ -252,7 +273,7 @@ the config file.
However, this may be replaced by a custom version, as long as the required
interface is implemented.
``wsgidav.addons.windowsdomaincontroller`` is an example for such an extension.
``wsgidav.addons.nt_domain_controller`` is an example for such an extension.
Other objects
@ -341,7 +362,7 @@ You will find this terms / naming conventions in the source:
URL (after the mount path was popped).
The share path is the common URL prefix of this URL.
TODO: do we need to distinguish between server mount points ('mount path') and
TODO: do we need to ditinguish between server mount points ('mount path') and
WsgiDAV mount points ('share path')?
Constructed like
@ -401,7 +422,7 @@ You will find this terms / naming conventions in the source:
*reference URL*:
Quoted, ISO-8859-1 encoded byte string.
Quoted, UTF-8 encoded byte string.
This is basically the same as an URL, that was build from the *preferred path*.
But this deals with 'virtual locations' as well.
@ -430,7 +451,7 @@ You will find this terms / naming conventions in the source:
*href*:
**Quoted**, ISO-8859-1 encoded byte string.
**Quoted**, UTF-8 encoded byte string.
Used in XML responses. We are using the path-absolute option. i.e. starting
with '/'. (See http://www.webdav.org/specs/rfc4918.html#rfc.section.8.3)
@ -439,4 +460,13 @@ You will find this terms / naming conventions in the source:
href = quote(mountPath + preferredPath)
Example:
"/dav/public/my%20nice%20doc.txt"
*filePath*:
Unicode
Used by fs_dav_provider when serving files from the file system.
(At least on Vista) os.path.exists(filePath) returns False, if a file name contains
special characters, even if it is correctly UTF-8 encoded.
So we convert to unicode.

View file

@ -2,24 +2,29 @@
README
======
:Module: pyfileserver
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com
:Project: PyFileServer, http://pyfilesync.berlios.de/
:Authors: - Ho Chun Wei, fuzzybr80(at)gmail.com (original PyFileServer)
- Martin Wendt
:Project: WsgiDAV, http://wsgidav.googlecode.com/
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
:Abstract: This document gives a brief introduction to the WsgiDAV application package.
What is PyFileServer?
=====================
PyFileServer is a WSGI web application for sharing filesystem directories
.. contents:: Table Of Contents
What is WsgiDAV?
================
WsgiDAV is a WSGI web application for sharing filesystem directories
over WebDAV.
For a more detailed discussion of the package, go to the project page
<http://pyfilesync.berlios.de/pyfileserver.html>
<http://wsgidav.googlecode.com/>
Installing PyFileServer
=======================
Installing WsgiDAV
==================
1. Get and install the latest release of Python, available from
@ -28,9 +33,9 @@ Installing PyFileServer
Python 2.3 or later is required; Python 2.4.1 or later is
recommended.
2. Use the latest PyFileServer release. Get the code from:
2. Use the latest WsgiDAV release. Get the code from:
http://pyfilesync.berlios.de/pyfileserver.html
http://wsgidav.googlecode.com/
3. Unpack the archive in a temporary directory (**not** directly in
@ -38,17 +43,17 @@ Installing PyFileServer
python setup.py install
PyFileServer requires the PyXML library <http://pyxml.sourceforge.net/>
WsgiDAV requires the PyXML library <http://pyxml.sourceforge.net/>
to run, and the installation process will install it if it is
not present on the system.
Configuring PyFileServer
========================
Configuring WsgiDAV
===================
PyFileServer reads its configuration from a user-specified configuration file.
An example of this file is given in the package as 'PyFileServer-example.conf'.
WsgiDAV reads its configuration from a user-specified configuration file.
An example of this file is given in the package as 'WsgiDAV-example.conf'.
You should make a copy of this file to use as your configuration file. The file
is self-documented and you can modify any settings as required.
@ -56,10 +61,10 @@ is self-documented and you can modify any settings as required.
Refer to the TUTORIAL documentation for an example.
Running PyFileServer
====================
Running WsgiDAV
===============
PyFileServer comes bundled with a simple wsgi webserver.
WsgiDAV comes bundled with a simple wsgi webserver.
Running as standalone server
----------------------------
@ -69,8 +74,8 @@ To run as a standalone server using the bundled ext_wsgiutils_server.py::
usage: python ext_wsgiutils_server.py [options] [config-file]
config-file:
The configuration file for PyFileServer. if omitted, the application
will look for a file named 'PyFileServer.conf' in the current directory
The configuration file for WsgiDAV. if omitted, the application
will look for a file named 'WsgiDAV.conf' in the current directory
options:
--port=PORT Port to serve on (default: 8080)
@ -86,10 +91,10 @@ Running using other web servers
To run it with other WSGI web servers, you can::
from pyfileserver.mainappwrapper import PyFileApp
publish_app = PyFileApp('PyFileServer.conf')
publish_app = PyFileApp('WsgiDAV.conf')
# construct the application with configuration file
# if configuration file is omitted, the application
# will look for a file named 'PyFileServer.conf'
# will look for a file named 'WsgiDAV.conf'
# in the current directory
where ``publish_app`` is the WSGI application to be run, it will be called with
@ -109,7 +114,7 @@ Help and Documentation
For further help or documentation, please refer to the project web page or
send a query to the mailing list.
Project Page: PyFileServer <http://pyfilesync.berlios.de/pyfileserver.html>
Project Page: WsgiDAV <http://wsgidav.googlecode.com/>
Mailing List: pyfilesync-users@lists.berlios.de (subscribe
<http://lists.berlios.de/mailman/listinfo/pyfilesync-users>)

View file

@ -1,6 +0,0 @@
class IDAVProvider(object):
"""
TODO: not sure, if we really need interfaces if we have an abstract base class.
For now, see wsgidav.DAVProvider.
"""

View file

@ -1,64 +0,0 @@
class IDomainController(object):
"""
+-------------------------------------------------------------------------------+
| The following documentation was taken over from PyFileServer and is outdated! |
+-------------------------------------------------------------------------------+
This class is an interface for a PropertyManager. Implementations for the
property manager in WsgiDAV include::
wsgidav.domain_controller.WsgiDAVDomainController
wsgidav.addons.windowsdomaincontroller.SimpleWindowsDomainController
All methods must be implemented.
The environ variable here is the WSGI 'environ' dictionary. It is passed to
all methods of the domain controller as a means for developers to pass information
from previous middleware or server config (if required).
"""
"""
Domain Controllers
------------------
The HTTP basic and digest authentication schemes are based on the following
concept:
Each requested relative URI can be resolved to a realm for authentication,
for example:
/fac_eng/courses/ee5903/timetable.pdf -> might resolve to realm 'Engineering General'
/fac_eng/examsolns/ee5903/thisyearssolns.pdf -> might resolve to realm 'Engineering Lecturers'
/med_sci/courses/m500/surgery.htm -> might resolve to realm 'Medical Sciences General'
and each realm would have a set of username and password pairs that would
allow access to the resource.
A domain controller provides this information to the HTTPAuthenticator.
"""
def getDomainRealm(self, inputURL, environ):
"""
resolves a relative url to the appropriate realm name
"""
def requireAuthentication(self, realmname, environ):
"""
returns True if this realm requires authentication
or False if it is available for general access
"""
def isRealmUser(self, realmname, username, environ):
"""
returns True if this username is valid for the realm, False otherwise
"""
def getRealmUserPassword(self, realmname, username, environ):
"""
returns the password for the given username for the realm.
Used for digest authentication.
"""
def authDomainUser(self, realmname, username, password, environ):
"""
returns True if this username/password pair is valid for the realm,
False otherwise. Used for basic authentication.
"""

View file

@ -1,97 +0,0 @@
class LockManagerInterface(object):
"""
+-------------------------------------------------------------------------------+
| The following documentation was taken over from PyFileServer and is outdated! |
| See wsgidav.lock_manager instead |
+-------------------------------------------------------------------------------+
This class is an interface for a LockManager. Implementations for the lock manager
in WsgiDAV include::
wsgidav.lock_manager.LockManager
All methods must be implemented.
The url variable in methods refers to the relative URL of a resource. e.g. the
resource http://server/share1/dir1/dir2/file3.txt would have a url of
'/share1/dir1/dir2/file3.txt'
"""
def generateLock(self, username, locktype, lockscope, lockdepth, lockowner, lockheadurl, timeout):
"""
returns a new locktoken for the following lock:
username - username of user performing the lock
locktype - only the locktype "write" is defined in the webdav specification
lockscope - "shared" or "exclusive"
lockdepth - depth of lock. "0" or "infinity"
lockowner - a arbitrary field provided by the client at lock time
lockheadurl - the url the lock is being performed on
timeout - -1 for infinite, positive value for number of seconds.
Could be None, fall back to a default.
"""
def deleteLock(self, locktoken):
"""
deletes a lock specified by locktoken
"""
def isTokenLockedByUser(self, locktoken, username):
"""
returns True if locktoken corresponds to a lock locked by username
"""
def isUrlLocked(self, url):
"""
returns True if the resource at url is locked
"""
def getUrlLockScope(self, url):
"""
returns the lockscope of all locks on url. 'shared' or 'exclusive'
"""
def getLockProperty(self, locktoken, lockproperty):
"""
returns the value for the following properties for the lock specified by
locktoken:
'LOCKUSER', 'LOCKTYPE', 'LOCKSCOPE', 'LOCKDEPTH', 'LOCKOWNER', 'LOCKHEADURL'
and
'LOCKTIME' - number of seconds left on the lock.
"""
def isUrlLockedByToken(self, url, locktoken):
"""
returns True if the resource at url is locked by lock specified by locktoken
"""
def getTokenListForUrl(self, url):
"""
returns a list of locktokens corresponding to locks on url.
"""
def getTokenListForUrlByUser(self, url, username):
"""
returns a list of locktokens corresponding to locks on url by user username.
"""
def addUrlToLock(self, url, locktoken):
"""
adds url to be locked by lock specified by locktoken.
more than one url can be locked by a lock - depth infinity locks.
"""
def removeAllLocksFromUrl(self, url):
"""
removes all locks from a url.
This usually happens when the resource specified by url is being deleted.
"""
def refreshLock(self, locktoken, timeout):
"""
refreshes the lock specified by locktoken.
timeout : -1 for infinite, positive value for number of seconds.
Could be None, fall back to a default.
"""

View file

@ -1,102 +0,0 @@
class PropertyManagerInterface(object):
"""
+-------------------------------------------------------------------------------+
| The following documentation was taken over from PyFileServer and is outdated! |
| See wsgidav.property_manager instead |
+-------------------------------------------------------------------------------+
This class is an interface for a PropertyManager. Implementations for the
property manager in WsgiDAV include::
wsgidav.property_manager.PropertyManager
All methods must be implemented.
The url variables in methods refers to the relative URL of a resource. e.g. the
resource http://server/share1/dir1/dir2/file3.txt would have a url of
'/share1/dir1/dir2/file3.txt'
"""
"""
Properties and WsgiDAV
---------------------------
Properties of a resource refers to the attributes of the resource. A property
is referenced by the property name and the property namespace. We usually
refer to the property as ``{property namespace}property name``
Properties of resources as defined in webdav falls under three categories:
Live properties
These properties are attributes actively maintained by the server, such as
file size, or read permissions. if you are sharing a database record as a
resource, for example, the attributes of the record could become the live
properties of the resource.
The webdav specification defines the following properties that could be
live properties (refer to webdav specification for details):
{DAV:}creationdate
{DAV:}displayname
{DAV:}getcontentlanguage
{DAV:}getcontentlength
{DAV:}getcontenttype
{DAV:}getetag
{DAV:}getlastmodified
{DAV:}resourcetype
{DAV:}source
These properties are implemented by the abstraction layer.
Locking properties
They refer to the two webdav-defined properties
{DAV:}supportedlock and {DAV:}lockdiscovery
These properties are implemented by the locking library in
``wsgidav.lock_manager`` and dead properties library in
``wsgidav.property_manager``
Dead properties
They refer to arbitrarily assigned properties not actively maintained.
These properties are implemented by the dead properties library in
``wsgidav.property_manager``
"""
def getProperties(self, normurl):
"""
return a list of properties for url specified by normurl
return list is a list of tuples (a, b) where a is the property namespace
and b the property name
"""
def getProperty(self, normurl, propname, propns):
"""
return the value of the property for url specified by normurl where
propertyname is propname and property namespace is propns
"""
def writeProperty(self, normurl, propname, propns, propertyvalue):
"""
write propertyvalue as value of the property for url specified by
normurl where propertyname is propname and property namespace is propns
"""
def removeProperty(self, normurl, propname, propns):
"""
delete the property for url specified by normurl where
propertyname is propname and property namespace is propns
"""
def removeProperties(self, normurl):
"""
delete all properties from url specified by normurl
"""
def copyProperties(self, origurl, desturl):
"""
copy all properties from url specified by origurl to url specified by desturl
"""

14
setup.cfg Normal file
View file

@ -0,0 +1,14 @@
# Set default to 'daily build'
[egg_info]
tag_build = .dev
tag_date = 1
# Set sdist format to tar.gz
# NOTE: tar doesn't seem to run on windows
[sdist]
#formats = gztar
# Define 'release' alias to strip '.dev-DATE'
[aliases]
release = egg_info -RDb ''

View file

@ -1,5 +1,6 @@
# If true, then the svn revision won't be used to calculate the
# revision (set to True for real releases)
import os
RELEASE = False
from wsgidav.version import __version__
@ -8,22 +9,16 @@ from ez_setup import use_setuptools
use_setuptools()
#manual check for dependencies: PyXML - somehow installer unable to find on PyPI.
#import sys
#try:
# from lxml import etree #@UnusedImport
#except ImportError:
# print "Failed to detect lxml. lxml is required for WsgiDAV. Please install"
# print "lxml <http://codespeak.net/lxml/> before installing WsgiDAV"
# sys.exit(-1)
from setuptools import setup, find_packages
# 'setup.py upload' fails on Vista, because .pypirc is searched on 'HOME' path
if not "HOME" in os.environ and "HOMEPATH" in os.environ:
os.environ.setdefault("HOME", os.environ.get("HOMEPATH", ""))
print "Initializing HOME environment variable to '%s'" % os.environ["HOME"]
setup(name="WsgiDAV",
version = __version__,
author = "Ho Chun Wei, Martin Wendt",
author = "Martin Wendt, Ho Chun Wei",
author_email = "wsgidav@wwwendt.de",
maintainer = "Martin Wendt",
maintainer_email = "wsgidav@wwwendt.de",
@ -33,7 +28,7 @@ setup(name="WsgiDAV",
WsgiDAV is a WebDAV server for sharing files and other resources over the web.
It is based on the WSGI interface <http://www.python.org/peps/pep-0333.html>.
It comes bundled with a simple WSIG web server.
It comes bundled with a simple WSGI web server.
*This package is based on PyFileServer by Ho Chun Wei.*
@ -45,7 +40,7 @@ Project home: http://wsgidav.googlecode.com/
#Development Status :: 4 - Beta
#Development Status :: 5 - Production/Stable
classifiers = ["Development Status :: 3 - Alpha",
classifiers = ["Development Status :: 4 - Beta",
"Intended Audience :: Information Technology",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
@ -60,15 +55,18 @@ Project home: http://wsgidav.googlecode.com/
"Topic :: Internet :: WWW/HTTP :: WSGI :: Server",
"Topic :: Software Development :: Libraries :: Python Modules",
],
keywords = 'web wsgi webdav application server',
# platforms=['Unix', 'Windows'],
keywords = "web wsgi webdav application server",
# platforms=["Unix", "Windows"],
license = "LGPL",
install_requires = ["lxml"],
# install_requires = ["lxml"],
packages = find_packages(exclude=[]),
# package_data={'': ['*.txt', '*.html', '*.conf']},
py_modules = ["ez_setup", ],
# package_data={"": ["*.txt", "*.html", "*.conf"]},
# include_package_data = True, # TODO: PP
zip_safe = False,
extras_require = {},
test_suite = "tests.test_all.run",
entry_points = {
"console_scripts" : ["wsgidav = wsgidav.server.run_server:run"],
},

14
tests/__init__.py Normal file
View file

@ -0,0 +1,14 @@
#__all__ = ['mainappwrapper',
# 'request_server',
# 'processrequesterrorhandler',
## 'httpdatehelper',
# 'util',
# 'request_resolver',
# 'http_authenticator',
# 'domain_controller',
# 'loadconfig_primitive',
# 'property_manager',
# 'lock_manager',
# 'fileabstractionlayer',
# 'websupportfuncs',
# 'wsgiapp']

View file

@ -0,0 +1,45 @@
open http://localhost/test
tester
tester
ls
rmcol cadaverTEST
mkcol cadaverTEST
cd cadaverTEST
mput *.txt
mkcol container
move *.txt container
mkcol MOVETEST
mkcol COPYTEST
lock MOVETEST
discover MOVETEST
lock COPYTEST
discover COPYTEST
move container MOVETEST
ls MOVETEST
discover MOVETEST/container/LICENSE.txt
copy MOVETEST/container COPYTEST
ls COPYTEST
discover COPYTEST/container/LICENSE.txt
rmcol COPYTEST
ls
unlock MOVETEST
discover MOVETEST
discover MOVETEST/container/LICENSE.txt
propnames MOVETEST/container/LICENSE.txt
propget MOVETEST/container/LICENSE.txt
propset MOVETEST/container/LICENSE.txt deadproperty testvalue
propget MOVETEST/container/LICENSE.txt
rmcol MOVETEST
quit

4
tests/conftest.py Normal file
View file

@ -0,0 +1,4 @@
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
import pkg_resources
pkg_resources.require('wsgidav')

24
tests/test_all.py Normal file
View file

@ -0,0 +1,24 @@
# -*- coding: iso-8859-1 -*-
"""
Unit tests for the WsgiDAV package.
"""
from tests import test_lock_manager, test_property_manager, test_wsgidav_app,\
test_util
from unittest import TestSuite, TextTestRunner
import sys
def run():
suite = TestSuite([test_util.suite(),
test_lock_manager.suite(),
test_property_manager.suite(),
test_wsgidav_app.suite(),
])
failures = TextTestRunner(descriptions=0, verbosity=2).run(suite)
sys.exit(failures)
if __name__ == "__main__":
run()

251
tests/test_lock_manager.py Normal file
View file

@ -0,0 +1,251 @@
# -*- coding: iso-8859-1 -*-
"""Unit test for lock_manager.py"""
from tempfile import gettempdir
from wsgidav.dav_error import DAVError
import os
from time import sleep
from unittest import TestCase, TestSuite, TextTestRunner
from wsgidav import lock_manager
#===============================================================================
# BasicTest
#===============================================================================
class BasicTest(TestCase):
"""Test lock_manager.LockManager()."""
principal = "Joe Tester"
owner = "joe.tester@example.com"
root = "/dav/res"
timeout = 10 * 60 # Default lock timeout 10 minutes
@classmethod
def suite(cls):
"""Return test case suite (so we can control the order)."""
suite = TestSuite()
suite.addTest(cls("testPreconditions"))
suite.addTest(cls("testOpen"))
suite.addTest(cls("testValidation"))
suite.addTest(cls("testLock"))
suite.addTest(cls("testTimeout"))
suite.addTest(cls("testConflict"))
return suite
def setUp(self):
self.lm = lock_manager.LockManager()
self.lm._verbose = 1
def tearDown(self):
self.lm._close()
self.lm = None
def _isLockDict(self, o):
try:
_ = o["root"]
except:
return False
return True
def _isLockResultOK(self, resultTupleList):
"""Return True, if result is [ (lockDict, None) ]."""
try:
return (len(resultTupleList) == 1
and len(resultTupleList) == 2
and self._isLockDict(resultTupleList[0][0])
and resultTupleList[0][1] is None)
except:
return False
def _isLockResultFault(self, resultTupleList, status=None):
"""Return True, if it is a valid result tuple containing a DAVError."""
try:
if len(resultTupleList) < 1:
return False
resultTuple = resultTupleList[0]
if len(resultTuple) != 2 or not self._isLockDict(resultTuple[0]) or not isinstance(resultTuple[1], DAVError):
return False
elif status and status!=DAVError.value:
return False
return True
except:
return False
def testPreconditions(self):
"""Environment must be set."""
self.assertTrue(__debug__, "__debug__ must be True, otherwise asserts are ignored")
def testOpen(self):
"""Lock manager should be lazy opening on first access."""
lm = self.lm
assert not lm._loaded, "LM must only be opened after first access"
lm._generateLock(self.principal, "write", "exclusive", "infinity",
self.owner,
"/dav",
10)
assert lm._loaded, "LM must be opened after first access"
def testValidation(self):
"""Lock manager should raise errors on bad args."""
lm = self.lm
self.assertRaises(AssertionError,
lm._generateLock, lm, "writeX", "exclusive", "infinity",
self.owner, self.root, self.timeout)
self.assertRaises(AssertionError,
lm._generateLock, lm, "write", "exclusiveX", "infinity",
self.owner, self.root, self.timeout)
self.assertRaises(AssertionError,
lm._generateLock, lm, "write", "exclusive", "infinityX",
self.owner, self.root, self.timeout)
self.assertRaises(AssertionError,
lm._generateLock, lm, "write", "exclusive", "infinity",
None, self.root, self.timeout)
self.assertRaises(AssertionError,
lm._generateLock, lm, "write", "exclusive", "infinity",
self.owner, None, self.timeout)
assert lm._dict is None, "No locks should have been created by this test"
def testLock(self):
"""Lock manager should create and find locks."""
lm = self.lm
url = "/dav/res"
# Create a new lock
lockDict = lm._generateLock(self.principal, "write", "exclusive", "infinity",
self.owner, url, self.timeout)
# Check returned dictionary
assert lockDict is not None
assert lockDict["root"] == url
assert lockDict["type"] == "write"
assert lockDict["scope"] == "exclusive"
assert lockDict["depth"] == "infinity"
assert lockDict["owner"] == self.owner
assert lockDict["principal"] == self.principal
# Test lookup
tok = lockDict.get("token")
assert lm.getLock(tok, "root") == url
lockDict = lm.getLock(tok)
assert lockDict is not None
assert lockDict["root"] == url
assert lockDict["type"] == "write"
assert lockDict["scope"] == "exclusive"
assert lockDict["depth"] == "infinity"
assert lockDict["owner"] == self.owner
assert lockDict["principal"] == self.principal
# We locked "/dav/res", did we?
assert lm.isTokenLockedByUser(tok, self.principal)
res = lm.getUrlLockList(url, self.principal)
assert len(res) == 1
res = lm.getUrlLockList(url, "another user")
assert len(res) == 0
assert lm.isUrlLockedByToken("/dav/res", tok), "url not directly locked by locktoken."
assert lm.isUrlLockedByToken("/dav/res/", tok), "url not directly locked by locktoken."
assert lm.isUrlLockedByToken("/dav/res/sub", tok), "child url not indirectly locked"
assert not lm.isUrlLockedByToken("/dav/ressub", tok), "non-child url reported as locked"
assert not lm.isUrlLockedByToken("/dav", tok), "parent url reported as locked"
assert not lm.isUrlLockedByToken("/dav/", tok), "parent url reported as locked"
def testTimeout(self):
"""Locks should be purged after expiration date."""
lm = self.lm
timeout = 1
lockDict = lm._generateLock(self.principal, "write", "exclusive", "infinity",
self.owner, self.root, timeout)
assert lockDict is not None
tok = lockDict.get("token")
assert lm.getLock(tok, "root") == self.root
sleep(timeout - 0.5)
lockDict = lm.getLock(tok)
assert lockDict is not None, "Lock expired too early"
sleep(1)
lockDict = lm.getLock(tok)
assert lockDict is None, "Lock has not expired"
def testConflict(self):
"""Locks should prevent conflicts."""
lm = self.lm
tokenList = []
# Create a lock for '/dav/res/'
res = lm.acquire("/dav/res/", "write", "exclusive", "infinity",
self.owner, self.timeout, self.principal, tokenList)
assert self._isLockDict(res[0][0]) and res[0][1] is None, "Could not acquire lock"
# Try to lock with a slightly different URL (without trailing '/')
res = lm.acquire("/dav/res", "write", "exclusive", "infinity",
self.owner, self.timeout, "another principal", tokenList)
assert self._isLockResultFault(res), "Could acquire a conflicting lock"
# Try to lock with another principal
res = lm.acquire("/dav/res/", "write", "exclusive", "infinity",
self.owner, self.timeout, "another principal", tokenList)
assert self._isLockResultFault(res), "Could acquire a conflicting lock"
# Try to lock child with another principal
res = lm.acquire("/dav/res/sub", "write", "exclusive", "infinity",
self.owner, self.timeout, "another principal", tokenList)
assert self._isLockResultFault(res), "Could acquire a conflicting child lock"
# Try to lock parent with same principal
res = lm.acquire("/dav/", "write", "exclusive", "infinity",
self.owner, self.timeout, self.principal, tokenList)
assert self._isLockResultFault(res), "Could acquire a conflicting parent lock"
# Try to lock child with same principal
res = lm.acquire("/dav/res/sub", "write", "exclusive", "infinity",
self.owner, self.timeout, self.principal, tokenList)
assert self._isLockResultFault(res), "Could acquire a conflicting child lock (same principal)"
#===============================================================================
# ShelveTest
#===============================================================================
class ShelveTest(BasicTest):
"""Test lock_manager.ShelveLockManager()."""
def setUp(self):
self.path = os.path.join(gettempdir(), "wsgidav-locks.shelve")
if os.path.exists(self.path):
os.remove(self.path)
self.lm = lock_manager.ShelveLockManager(self.path)
self.lm._verbose = 1
def tearDown(self):
self.lm._close()
self.lm = None
# os.remove(self.path)
#===============================================================================
# suite
#===============================================================================
def suite():
"""Return suites of all test cases."""
return TestSuite([BasicTest.suite(),
ShelveTest.suite(),
])
if __name__ == "__main__":
# unittest.main()
suite = suite()
TextTestRunner(descriptions=0, verbosity=2).run(suite)

View file

@ -0,0 +1,106 @@
# -*- coding: iso-8859-1 -*-
"""Unit test for lock_manager.py"""
from tempfile import gettempdir
from unittest import TestCase, TestSuite, TextTestRunner
import os
from wsgidav import property_manager
#===============================================================================
# BasicTest
#===============================================================================
class BasicTest(TestCase):
"""Test property_manager.PropertyManager()."""
respath = "/dav/res"
@classmethod
def suite(cls):
"""Return test case suite (so we can control the order)."""
suite = TestSuite()
suite.addTest(cls("testPreconditions"))
suite.addTest(cls("testOpen"))
suite.addTest(cls("testValidation"))
suite.addTest(cls("testReadWrite"))
return suite
def setUp(self):
self.pm = property_manager.PropertyManager()
self.pm._verbose = 2
def tearDown(self):
self.pm._close()
self.pm = None
def testPreconditions(self):
"""Environment must be set."""
self.assertTrue(__debug__, "__debug__ must be True, otherwise asserts are ignored")
def testOpen(self):
"""Property manager should be lazy opening on first access."""
pm = self.pm
assert not pm._loaded, "PM must be closed until first access"
pm.getProperties(self.respath)
assert pm._loaded, "PM must be opened after first access"
def testValidation(self):
"""Property manager should raise errors on bad args."""
pm = self.pm
self.assertRaises(AssertionError,
pm.writeProperty, None, "{ns1:}foo", "hurz", False)
# name must have a namespace
# self.assertRaises(AssertionError,
# pm.writeProperty, "/dav/res", "foo", "hurz", False)
self.assertRaises(AssertionError,
pm.writeProperty, "/dav/res", None, "hurz", False)
self.assertRaises(AssertionError,
pm.writeProperty, "/dav/res", "{ns1:}foo", None, False)
assert pm._dict is None, "No properties should have been created by this test"
def testReadWrite(self):
"""Property manager should raise errors on bad args."""
pm = self.pm
url = "/dav/res"
pm.writeProperty(url, "foo", "my name is joe")
assert pm.getProperty(url, "foo") == "my name is joe"
#===============================================================================
# ShelveTest
#===============================================================================
class ShelveTest(BasicTest):
"""Test property_manager.ShelvePropertyManager()."""
def setUp(self):
self.path = os.path.join(gettempdir(), "wsgidav-props.shelve")
if os.path.exists(self.path):
os.remove(self.path)
self.pm = property_manager.ShelvePropertyManager(self.path)
self.pm._verbose = 1
def tearDown(self):
self.pm._close()
self.pm = None
# os.remove(self.path)
#===============================================================================
# suite
#===============================================================================
def suite():
"""Return suites of all test cases."""
return TestSuite([BasicTest.suite(),
ShelveTest.suite(),
])
if __name__ == "__main__":
# unittest.main()
suite = suite()
TextTestRunner(descriptions=1, verbosity=2).run(suite)

69
tests/test_util.py Normal file
View file

@ -0,0 +1,69 @@
# -*- coding: iso-8859-1 -*-
"""Unit tests for wsgidav.util"""
from unittest import TestCase, TestSuite, TextTestRunner
from wsgidav.util import *
class BasicTest(TestCase):
"""Test ."""
@classmethod
def suite(cls):
"""Return test case suite (so we can control the order)."""
suite = TestSuite()
suite.addTest(cls("testPreconditions"))
suite.addTest(cls("testBasics"))
return suite
def setUp(self):
pass
def tearDown(self):
pass
def testPreconditions(self):
"""Environment must be set."""
self.assertTrue(__debug__, "__debug__ must be True, otherwise asserts are ignored")
def testBasics(self):
"""Test basic tool functions."""
assert not isChildUri("/a/b", "/a/")
assert not isChildUri("/a/b", "/a/b")
assert not isChildUri("/a/b", "/a/b/")
assert not isChildUri("/a/b", "/a/bc")
assert not isChildUri("/a/b", "/a/bc/")
assert isChildUri("/a/b", "/a/b/c")
assert isChildUri("/a/b", "/a/b/c")
assert not isEqualOrChildUri("/a/b", "/a/")
assert isEqualOrChildUri("/a/b", "/a/b")
assert isEqualOrChildUri("/a/b", "/a/b/")
assert not isEqualOrChildUri("/a/b", "/a/bc")
assert not isEqualOrChildUri("/a/b", "/a/bc/")
assert isEqualOrChildUri("/a/b", "/a/b/c")
assert isEqualOrChildUri("/a/b", "/a/b/c")
assert lstripstr("/dav/a/b", "/dav") == "/a/b"
assert lstripstr("/dav/a/b", "/DAV") == "/dav/a/b"
assert lstripstr("/dav/a/b", "/DAV", True) == "/a/b"
#===============================================================================
# suite
#===============================================================================
def suite():
"""Return suites of all test cases."""
return TestSuite([BasicTest.suite(),
])
if __name__ == "__main__":
# unittest.main()
suite = suite()
TextTestRunner(descriptions=0, verbosity=2).run(suite)

269
tests/test_wsgidav_app.py Normal file
View file

@ -0,0 +1,269 @@
# -*- coding: iso-8859-1 -*-
"""
Unit test for wsgidav.lock_manager.py
This test suite uses paste.fixture to send fake requests to the WSGI
stack.
See http://pythonpaste.org/testing-applications.html
and http://pythonpaste.org/modules/fixture.html
"""
from paste.fixture import TestApp #@UnresolvedImport
from tempfile import gettempdir
from wsgidav.wsgidav_app import DEFAULT_CONFIG, WsgiDAVApp
from wsgidav.fs_dav_provider import FilesystemProvider
#from wsgidav import util
import os
import unittest
#===============================================================================
# ServerTest
#===============================================================================
class ServerTest(unittest.TestCase):
"""Test wsgidav_app using paste.fixture."""
@classmethod
def suite(cls):
"""Return test case suite (so we can control the order)."""
suite = unittest.TestSuite()
suite.addTest(cls("testPreconditions"))
suite.addTest(cls("testDirBrowser"))
suite.addTest(cls("testGetPut"))
suite.addTest(cls("testEncoding"))
suite.addTest(cls("testAuthentication"))
return suite
def _makeWsgiDAVApp(self, withAuthentication):
self.rootpath = os.path.join(gettempdir(), "wsgidav-test")
if not os.path.exists(self.rootpath):
os.mkdir(self.rootpath)
provider = FilesystemProvider(self.rootpath)
config = DEFAULT_CONFIG.copy()
config.update({
"provider_mapping": {"/": provider},
"user_mapping": {},
"verbose": 1,
"enable_loggers": [],
"propsmanager": None, # None: use property_manager.PropertyManager
"locksmanager": None, # None: use lock_manager.LockManager
"domaincontroller": None, # None: domain_controller.WsgiDAVDomainController(user_mapping)
})
if withAuthentication:
config["user_mapping"] = {"/": {"tester": {"password": "tester",
"description": "",
"roles": [],
},
},
}
config["acceptbasic"] = True
config["acceptdigest"] = False
config["defaultdigest"] = False
return WsgiDAVApp(config)
def setUp(self):
wsgi_app = self._makeWsgiDAVApp(False)
self.app = TestApp(wsgi_app)
def tearDown(self):
# os.rmdir(self.rootpath)
del self.app
self.app = None
def testPreconditions(self):
"""Environment must be set."""
self.assertTrue(__debug__, "__debug__ must be True, otherwise asserts are ignored")
def testDirBrowser(self):
"""Server must respond to GET on a collection."""
app = self.app
# Access collection (expect '200 Ok' with HTML response)
res = app.get("/", status=200)
assert "WsgiDAV - Index of /" in res, "Could not list root share"
# Access unmapped resource (expect '404 Not Found')
res = app.get("/not-existing-124/", status=404)
def testGetPut(self):
"""Read and write file contents."""
app = self.app
# Prepare file content
data1 = "this is a file\nwith two lines"
data2 = "this is another file\nwith three lines\nsee!"
# Big file with 10 MB
lines = []
line = "." * (1000-6)
for i in xrange(10*1000):
lines.append("%04i: %s" % (i, line))
data3 = "\n".join(lines)
# Remove old test files
app.delete("/file1.txt", expect_errors=True)
app.delete("/file2.txt", expect_errors=True)
app.delete("/file3.txt", expect_errors=True)
# Access unmapped resource (expect '404 Not Found')
app.delete("/file1.txt", status=404)
app.get("/file1.txt", status=404)
# PUT a small file (expect '201 Created')
app.put("/file1.txt", params=data1, status=201)
res = app.get("/file1.txt", status=200)
assert res.body == data1, "GET file content different from PUT"
# PUT overwrites a small file (expect '204 No Content')
app.put("/file1.txt", params=data2, status=204)
res = app.get("/file1.txt", status=200)
assert res.body == data2, "GET file content different from PUT"
# PUT writes a big file (expect '201 Created')
app.put("/file2.txt", params=data3, status=201)
# Request must not contain a body (expect '415 Media Type Not Supported')
app.get("/file1.txt",
headers={"Content-Length": str(len(data1))},
params=data1,
status=415)
def testEncoding(self):
"""Handle special characters."""
app = self.app
uniData = u"This is a file with special characters:\n" \
+ u"Umlaute(äöüß)\n" \
+ u"Euro(\u20AC)\n" \
+ u"Male(\u2642)"
data = uniData.encode("utf8")
def __testrw(filename):
# Write/read UTF-8 encoded file name
# print util.stringRepr(filename)
app.delete(filename, expect_errors=True)
app.put(filename, params=data, status=201)
res = app.get(filename, status=200)
assert res.body == data, "GET file content different from PUT"
# filenames with umlauts
__testrw("/file uml(äöüß).txt")
# UTF-8 encoded filenames
__testrw("/file euro(\xE2\x82\xAC).txt")
__testrw("/file male(\xE2\x99\x82).txt")
def testAuthentication(self):
"""Require login."""
# Re-create test app with authentication
self.tearDown()
wsgi_app = self._makeWsgiDAVApp(True)
app = self.app = TestApp(wsgi_app)
# Anonymous access must fail (expect 401 Not Authorized)
# Existing resource
app.get("/file1.txt", status=401)
# Non-existing resource
app.get("/not_existing_file.txt", status=401)
# Root container
app.get("/", status=401)
# Try basic access authentication
user = "tester"
password = "tester"
creds = (user + ":" + password).encode("base64").strip()
headers = {"Authorization": "Basic %s" % creds,
}
# Existing resource
app.get("/file1.txt", headers=headers, status=200)
# Non-existing resource (expect 404 NotFound)
app.get("/not_existing_file.txt", headers=headers, status=404)
#===============================================================================
# WsgiDAVServerTest
#===============================================================================
#class WsgiDAVServerTest(unittest.TestCase):
# """Test the built-in WsgiDAV server with cadaver."""
#
# @classmethod
# def suite(cls):
# """Return test case suite (so we can control the order)."""
# suite = unittest.TestSuite()
# suite.addTest(cls("testPreconditions"))
# suite.addTest(cls("testOpen"))
# return suite
#
#
# def setUp(self):
# config = DEFAULT_CONFIG.copy()
# config.update({
# "provider_mapping": {},
# "user_mapping": {},
# "host": "localhost",
# "port": 8080,
# "ext_servers": [
# # "paste",
# # "cherrypy",
# # "wsgiref",
# "wsgidav",
# ],
# "enable_loggers": [
# ],
#
# "propsmanager": None, # None: use properry_manager.PropertyManager
# "locksmanager": None, # None: use lock_manager.LockManager
# "domaincontroller": None,
# "verbose": 2,
# })
# self.app = WsgiDAVApp(config)
#
## from wsgidav.server.run_server import _runBuiltIn
## _runBuiltIn(app, config)
#
#
# def tearDown(self):
# del self.app
# self.app = None
#
#
# def testPreconditions(self):
# """Environment must be set."""
# self.assertTrue(__debug__, "__debug__ must be True, otherwise asserts are ignored")
#
#
# def testOpen(self):
# """Property manager should be lazy opening on first access."""
# app = self.app
# conf = self.app.config
#
#
# def testValidation(self):
# """Property manager should raise errors on bad args."""
# conf = self.app.config
#===============================================================================
# suite
#===============================================================================
def suite():
"""Return suites of all test cases."""
return unittest.TestSuite([ServerTest.suite(),
])
if __name__ == "__main__":
# unittest.main()
suite = suite()
unittest.TextTestRunner(descriptions=1, verbosity=2).run(suite)

5
tools/_map_w.bat Normal file
View file

@ -0,0 +1,5 @@
net use W: http://127.0.0.1/temp/ /persistent:no /user:tester tester
rem net use W: http://127.0.0.1/ /persistent:no /user:tester tester
net use
dir w:
pause

3
tools/_unmap_w.bat Normal file
View file

@ -0,0 +1,3 @@
net use W: /delete
net use
pause

View file

@ -7,11 +7,13 @@ url: http://wsgidav.googlecode.com/
# The list of modules to document. Modules can be named using
# dotted names, module filenames, or package directory names.
# This option may be repeated.
modules: wsgidav_server
;modules: wsgidav_server
modules: wsgidav.interfaces
modules: wsgidav.interfaces.propertymanagerinterface
# pyfileserver is specified in the command line
# wsgidav is specified in the command line
;exclude=PATTERN
exclude=tests, wsgidav.path, wsgidav.pickleshare
# Whether or not to include syntax highlighted source code in
# the output (HTML only).
@ -24,7 +26,7 @@ output: html
include-log: yes
inheritance: grouped
top: class-tree.html
;top: class-tree.html
# Include all automatically generated graphs. These graphs are
# generated using Graphviz dot.

View file

@ -1,3 +1,3 @@
#__all__ = ['windowsdomaincontroller',
#__all__ = ['nt_domain_controller',
# 'simplemysqlabstractionlayer',
# ]

View file

@ -20,7 +20,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
__docformat__ = 'reStructuredText'
__docformat__ = "reStructuredText"
from wsgidav.dav_provider import DAVProvider
@ -121,16 +121,16 @@ class DummyDAVProvider(DAVProvider):
return super(DummyDAVProvider, self).getResourceInfo(path)
def getPropertyNames(self, path, mode="allprop"):
def getPropertyNames(self, davres, mode="allprop"):
"""Return list of supported property names in Clark Notation.
@param mode: 'allprop': common properties, that should be send on 'allprop' requests.
'propname': all available properties.
"""
return super(DummyDAVProvider, self).getPropertyNames(path, mode)
return super(DummyDAVProvider, self).getPropertyNames(davres, mode)
def getProperties(self, path, mode, nameList=None, namesOnly=False):
def getProperties(self, davres, mode, nameList=None, namesOnly=False):
"""Return properties as list of 2-tuples (name, value).
<name> is the property name in Clark notation.
@ -146,11 +146,11 @@ class DummyDAVProvider(DAVProvider):
@param nameList: list of property names in Clark Notation (only for mode 'named')
@param namesOnly: return None for <value>
"""
return super(DummyDAVProvider, self).getProperties(path, mode, nameList, namesOnly)
return super(DummyDAVProvider, self).getProperties(davres, mode, nameList, namesOnly)
def getPropertyValue(self, path, name):
return super(DummyDAVProvider, self).getPropertyValue(path, name)
def getPropertyValue(self, path, propname, davres=None):
return super(DummyDAVProvider, self).getPropertyValue(path, propname, davres)
def setPropertyValue(self, path, name, value, dryRun=False):
@ -200,7 +200,7 @@ class DummyDAVProvider(DAVProvider):
#---------------------------------------------------------------------------
def getSupportedLivePropertyNames(self, path):
def getSupportedLivePropertyNames(self, davres):
"""Return list of supported live properties in Clark Notation.
Do NOT add {DAV:}lockdiscovery and {DAV:}supportedlock.
@ -208,7 +208,7 @@ class DummyDAVProvider(DAVProvider):
return NotImplementedError() # Provider must override this
def getLivePropertyValue(self, path, name):
def getLivePropertyValue(self, davres, propname):
"""Set list of supported live properties in Clark Notation.
Raise HTTP_NOT_FOUND if property is not supported.

View file

@ -0,0 +1,542 @@
"""
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
:Author: Martin Wendt, moogle(at)wwwendt.de
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
Implementation of a DAV provider that provides a very basic, read-only
resource layer emulation of a MySQL database.
This module is specific to the WsgiDAV application. It provides a
classes ``MySQLBrowserProvider``.
Usage::
(see sample_wsgidav.conf)
MySQLBrowserProvider(host, user, passwd, db)
host - host of database server
user - username to access database
passwd - passwd to access database
db - name of database on database server
The ``MySQLBrowserProvider`` provides a very basic, read-only
resource layer emulation of a MySQL database.
It provides the following interface:
- the root collection shared consists of collections that correspond to
table names
- in each table collection, there is a resource called "_ENTIRE_CONTENTS".
This is a non-collection resource that returns a csv representation of the
entire table
- if the table has a single primary key, each table record will also appear
as a non-collection resource in the table collection using the primary key
value as its name. This resource returns a csv representation of the record
and will also include the record attributes as live properties with
attribute name as property name and table name suffixed with colon as the
property namespace
This is a very basic interface and below is a by no means thorough summary of
its limitations:
- Really only supports having numbers or strings as primary keys. The code uses
a numeric or string comparison that may not hold up if the primary key is
a date or some other datatype.
- There is no handling for cases like BLOBs as primary keys or such. Well, there is
no handling for BLOBs in general.
- When returning contents, it buffers the entire contents! A bad way to return
large tables. Ideally you would have a FileMixin that reads the database even
as the application reads the file object....
- It takes too many database queries to return information.
Ideally there should be some sort of caching for metadata at least, to avoid
unnecessary queries to the database.
Abstraction Layers must provide the methods as described in
abstractionlayerinterface_
See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
.. _abstractionlayerinterface : interfaces/abstractionlayerinterface.py
"""
from wsgidav.dav_provider import DAVProvider
from wsgidav.dav_error import DAVError, HTTP_FORBIDDEN
from wsgidav import util
import MySQLdb
import md5
import time
import csv
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
__docformat__ = "reStructuredText"
_logger = util.getModuleLogger(__name__)
class MySQLBrowserProvider(DAVProvider):
def __init__(self, host, user, passwd, db):
super(MySQLBrowserProvider, self).__init__()
self._host = host
self._user = user
self._passwd = passwd
self._db = db
self.connectCount = 0
def __repr__(self):
return "%s for db '%s' on '%s' (user: '%s')'" % (self.__class__.__name__, self._db, self._host, self._user)
def _splitPath(self, path):
"""Return (tableName, primaryKey) tuple for a request path."""
if path.strip() in (None, "", "/"):
return (None, None)
tableName, primKey = util.saveSplit(path.strip("/"), "/", 1)
# _logger.debug("'%s' -> ('%s', '%s')" % (path, tableName, primKey))
return (tableName, primKey)
def _initConnection(self):
self.connectCount += 1
return MySQLdb.connect(host=self._host,
user=self._user,
passwd=self._passwd,
db=self._db)
def _getFieldList(self, conn, table_name):
retlist = []
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("DESCRIBE " + table_name)
result_set = cursor.fetchall ()
for row in result_set:
retlist.append(row["Field"])
cursor.close ()
return retlist
def _isDataTypeNumeric(self, datatype):
if datatype is None:
return False
#how many MySQL datatypes does it take to change a lig... I mean, store numbers
numerictypes = ["BIGINT",
"INTT",
"MEDIUMINT",
"SMALLINT",
"TINYINT",
"BIT",
"DEC",
"DECIMAL",
"DOUBLE",
"FLOAT",
"REAL",
"DOUBLE PRECISION",
"INTEGER",
"NUMERIC"]
datatype = datatype.upper()
for numtype in numerictypes:
if datatype.startswith(numtype):
return True
return False
def _existsRecordByPrimaryKey(self, conn, table_name, pri_key_value):
pri_key = None
pri_field_type = None
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("DESCRIBE " + table_name)
result_set = cursor.fetchall ()
for row in result_set:
if row["Key"] == "PRI":
if pri_key is None:
pri_key = row["Field"]
pri_field_type = row["Type"]
else:
return False #more than one primary key - multipart key?
cursor.close ()
isNumType = self._isDataTypeNumeric(pri_field_type)
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
if isNumType:
cursor.execute("SELECT " + pri_key + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
else:
cursor.execute("SELECT " + pri_key + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
row = cursor.fetchone ()
if row is None:
cursor.close()
return False
cursor.close()
return True
def _getFieldByPrimaryKey(self, conn, table_name, pri_key_value, field_name):
pri_key = None
pri_field_type = None
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("DESCRIBE " + table_name)
result_set = cursor.fetchall ()
for row in result_set:
if row["Key"] == "PRI":
if pri_key is None:
pri_key = row["Field"]
pri_field_type = row["Type"]
else:
return None #more than one primary key - multipart key?
cursor.close ()
isNumType = self._isDataTypeNumeric(pri_field_type)
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
if isNumType:
cursor.execute("SELECT " + field_name + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
else:
cursor.execute("SELECT " + field_name + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
row = cursor.fetchone ()
if row is None:
cursor.close()
return None
val = str(row[field_name])
cursor.close()
return val
def _getRecordByPrimaryKey(self, conn, table_name, pri_key_value):
dictRet = {}
pri_key = None
pri_field_type = None
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("DESCRIBE " + table_name)
result_set = cursor.fetchall ()
for row in result_set:
if row["Key"] == "PRI":
if pri_key is None:
pri_key = row["Field"]
pri_field_type = row["Type"]
else:
return None #more than one primary key - multipart key?
cursor.close ()
isNumType = self._isDataTypeNumeric(pri_field_type)
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
if isNumType:
cursor.execute("SELECT * FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
else:
cursor.execute("SELECT * FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
row = cursor.fetchone ()
if row is None:
cursor.close()
return None
for fname in row.keys():
dictRet[fname] = str(row[fname])
cursor.close()
return dictRet
def _findPrimaryKey(self, conn, table_name):
pri_key = None
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("DESCRIBE " + table_name)
result_set = cursor.fetchall ()
for row in result_set:
fieldname = row["Field"]
keyvalue = row["Key"]
if keyvalue == "PRI":
if pri_key is None:
pri_key = fieldname
else:
return None #more than one primary key - multipart key?
cursor.close ()
return pri_key
def _listFields(self, conn, table_name, field_name):
retlist = []
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute("SELECT " + field_name + " FROM " + self._db + "." + table_name)
result_set = cursor.fetchall ()
for row in result_set:
retlist.append(str(row[field_name]))
cursor.close()
return retlist
def _listTables(self, conn):
retlist = []
cursor = conn.cursor ()
cursor.execute ("SHOW TABLES")
result_set = cursor.fetchall ()
for row in result_set:
retlist.append("%s" % (row[0]))
cursor.close ()
return retlist
def getMemberNames(self, path):
"""
path - path identifier for the resource
returns a list of names of resources contained in the collection resource
specified
"""
conn = self._initConnection()
# resdata = path.strip(":").split(":")
tableName, primKey = self._splitPath(path)
# if len(resdata) == 1:
if tableName is None:
retlist = self._listTables(conn)
# elif len(resdata) == 2:
elif primKey is None:
pri_key = self._findPrimaryKey(conn, tableName)
if pri_key is not None:
retlist = self._listFields(conn, tableName, pri_key)
else:
retlist = []
retlist[0:0] = ["_ENTIRE_CONTENTS"]
else:
retlist = []
conn.close()
return retlist
# def getSupportedInfoTypes(self, path):
# """Return a list of supported information types.
#
# See DAVProvider.getSupportedInfoTypes()
# """
# infoTypes = ["created",
# "contentType",
# "etag",
# "isCollection",
# "displayName",
# ]
## if not self.isCollection(path):
## pass
#
# return infoTypes
def getInfoDict(self, path, typeList=None):
"""Return info dictionary for path.
See DAVProvider.getInfoDict()
"""
# TODO: calling exists() makes directory browsing VERY slow.
# At least compared to PyFileServer, which simply used string
# functions to get displayType and displayRemarks
if not self.exists(path):
return None
tableName, primKey = self._splitPath(path)
displayType = "Unknown"
displayRemarks = ""
contentType = "text/html"
# _logger.debug("getInfoDict(%s), nc=%s" % (path, self.connectCount))
if tableName is None:
displayType = "Database"
elif primKey is None: # "database" and table name
displayType = "Database Table"
else:
contentType = "text/csv"
if primKey == "_ENTIRE_CONTENTS":
displayType = "Database Table Contents"
displayRemarks = "CSV Representation of Table Contents"
else:
displayType = "Database Record"
displayRemarks = "Attributes available as properties"
# Avoid calling isCollection, since it would call isExisting -> _initConnection
# isCollection = self.isCollection(path)
isCollection = primKey is None
# Avoid calling getPreferredPath, since it would call isCollection -> _initConnection
# name = util.getUriName(self.getPreferredPath(path))
name = util.getUriName(path)
# supportedInfoTypes = ["created",
# "contentType",
# "etag",
# "isCollection",
# "displayName",
# ]
dict = {"contentLength": None,
"contentType": contentType,
"name": name,
"displayName": name,
"displayType": displayType,
"modified": None,
"created": time.time(),
"etag": md5.new(path).hexdigest(),
"supportRanges": False,
"isCollection": isCollection,
# "supportedInfoTypes": supportedInfoTypes,
}
# Some resource-only infos:
if not isCollection:
dict["modified"] = time.time()
# _logger.debug("---> getInfoDict, nc=%s" % self.connectCount)
return dict
def exists(self, path):
tableName, primKey = self._splitPath(path)
if path == "/":
return True
try:
conn = self._initConnection()
# Check table existence:
tbllist = self._listTables(conn)
if tableName not in tbllist:
return False
# Check table key existence:
if primKey and primKey != "_ENTIRE_CONTENTS":
return self._existsRecordByPrimaryKey(conn, tableName, primKey)
return True
finally:
conn.close()
def isCollection(self, path):
_tableName, primKey = self._splitPath(path)
return self.exists(path) and primKey is None
def isResource(self, path):
_tableName, primKey = self._splitPath(path)
return self.exists(path) and primKey is not None
def createEmptyResource(self, path):
raise DAVError(HTTP_FORBIDDEN)
def createCollection(self, path):
raise DAVError(HTTP_FORBIDDEN)
def deleteCollection(self, path):
raise DAVError(HTTP_FORBIDDEN)
def openResourceForRead(self, path, davres=None):
"""
path - path identifier for the resource
returns a file-like object / stream containing the contents of the
resource specified.
The application will close() the stream.
"""
filestream = StringIO()
# resdata = path.strip(":").split(":")
tableName, primKey = self._splitPath(path)
# if len(resdata) == 3:
if primKey is not None:
# table_name = resdata[1]
conn = self._initConnection()
listFields = self._getFieldList(conn, tableName)
csvwriter = csv.DictWriter(filestream, listFields, extrasaction="ignore")
dictFields = {}
for field_name in listFields:
dictFields[field_name] = field_name
csvwriter.writerow(dictFields)
if primKey == "_ENTIRE_CONTENTS":
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("SELECT * from " + self._db + "." + tableName)
result_set = cursor.fetchall ()
for row in result_set:
csvwriter.writerow(row)
cursor.close ()
else:
row = self._getRecordByPrimaryKey(conn, tableName, primKey)
if row is not None:
csvwriter.writerow(row)
conn.close()
#this suffices for small dbs, but
#for a production big database, I imagine you would have a FileMixin that
#does the retrieving and population even as the file object is being read
filestream.seek(0)
return filestream
#filevalue = filestream.getvalue()
#filestream.close()
#return StringIO.StringIO(filevalue)
def openResourceForWrite(self, path, contenttype=None):
raise DAVError(HTTP_FORBIDDEN)
def deleteResource(self, path):
raise DAVError(HTTP_FORBIDDEN)
def copyResource(self, path, destrespath):
raise DAVError(HTTP_FORBIDDEN)
def getPropertyValue(self, path, propname, davres=None):
"""Return the value of a property.
The base implementation handles:
- ``{DAV:}lockdiscovery`` and ``{DAV:}supportedlock`` using the
associated lock manager.
- All other *live* properties (i.e. name starts with ``{DAV:}``) are
delegated to self.getLivePropertyValue()
- Finally, other properties are considered *dead*, and are handled using
the associated property manager, if one is present.
"""
# Return table field as property
# resdata = path.strip(":").split(":")
tableName, primKey = self._splitPath(path)
if primKey is not None:
ns, localName = util.splitNamespace(propname)
if ns == tableName:
conn = self._initConnection()
fieldlist = self._getFieldList(conn, tableName)
if localName in fieldlist:
val = self._getFieldByPrimaryKey(conn, tableName, primKey, localName)
conn.close()
return val
conn.close()
# else, let default implementation return supported live and dead properties
return super(MySQLBrowserProvider, self).getPropertyValue(path, propname, davres)
def getPropertyNames(self, davres, mode="allprop"):
"""Return list of supported property names in Clark Notation.
Return supported live and dead properties. (See also DAVProvider.getPropertyNames().)
In addition, all table field names are returned as properties.
"""
# Let default implementation return supported live and dead properties
propNames = super(MySQLBrowserProvider, self).getPropertyNames(davres, mode)
# Add fieldnames as properties
# resdata = path.strip(":").split(":")
tableName, primKey = self._splitPath(davres.path)
if primKey is not None:
conn = self._initConnection()
fieldlist = self._getFieldList(conn, tableName)
for fieldname in fieldlist:
propNames.append("{%s:}%s" % (tableName, fieldname))
conn.close()
return propNames

View file

@ -5,21 +5,17 @@
Implementation of a domain controller that allows users to authenticate against
a Windows NT domain or a local computer (used by HTTPAuthenticator).
+-------------------------------------------------------------------------------+
| The following documentation was taken over from PyFileServer and is outdated! |
+-------------------------------------------------------------------------------+
Purpose
-------
Usage::
from wsgidav.addons.windowsdomaincontroller import SimpleWindowsDomainController
domaincontroller = SimpleWindowsDomainController(presetdomain = None, presetserver = None)
from wsgidav.addons.nt_domain_controller import NTDomainController
domaincontroller = NTDomainController(presetdomain=None, presetserver=None)
where:
+ domaincontroller object corresponds to that in ``PyFileServer.conf`` or
+ domaincontroller object corresponds to that in ``wsgidav.conf`` or
as input into ``wsgidav.http_authenticator.HTTPAuthenticator``.
+ presetdomain allows the admin to specify a domain to be used (instead of any domain that
@ -51,7 +47,7 @@ Testability and caveats
**Digest Authentication**
Digest authentication requires the password to be retrieve from the system to compute
the correct digest for comparison. This is sofar impossible (and indeed would be a
the correct digest for comparison. This is so far impossible (and indeed would be a
big security loophole if it was allowed), so digest authentication WILL not work
with this class.
@ -77,22 +73,28 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
__docformat__ = 'reStructuredText'
from wsgidav import util
import win32net
import win32security
import win32api
#import win32api
import win32netcon
class SimpleWindowsDomainController(object):
__docformat__ = "reStructuredText"
_logger = util.getModuleLogger(__name__)
class NTDomainController(object):
def __init__(self, presetdomain = None, presetserver = None):
self._presetdomain = presetdomain
self._presetserver = presetserver
def __repr__(self):
return self.__class__.__name__
def getDomainRealm(self, inputURL, environ):
return "Windows Domain Authentication"
@ -112,13 +114,15 @@ class SimpleWindowsDomainController(object):
dcname = self._getDomainControllerName(domain)
try:
userdata = win32net.NetUserGetInfo(dcname,user,1)
userdata = win32net.NetUserGetInfo(dcname, user, 1)
except:
userdata = dict()
if 'password' in userdata:
if userdata['password'] != None:
return userdata['password']
return None
_logger.exception("NetUserGetInfo")
userdata = {}
# if "password" in userdata:
# if userdata["password"] != None:
# return userdata["password"]
# return None
return userdata.get("password")
def authDomainUser(self, realmname, username, password, environ):
@ -141,6 +145,7 @@ class SimpleWindowsDomainController(object):
return (domain, username)
def _getDomainControllerName(self, domain):
if self._presetserver != None:
return self._presetserver
@ -153,22 +158,29 @@ class SimpleWindowsDomainController(object):
return pdc
def _isUser(self, username, domain, server):
resume = 'init'
userslist = []
resume = "init"
while resume:
if resume == 'init': resume = 0
if resume == "init":
resume = 0
try:
users, total, resume = win32net.NetUserEnum(server, 0, win32netcon.FILTER_NORMAL_ACCOUNT, 0)
userslist += users
users, _total, resume = win32net.NetUserEnum(server, 0, win32netcon.FILTER_NORMAL_ACCOUNT, 0)
# Make sure, we compare unicode
un = username.decode("utf8").lower()
for userinfo in users:
if username.lower() == str(userinfo['name']).lower():
uiname = userinfo.get("name")
assert uiname
assert isinstance(uiname, unicode)
if un == userinfo["name"].lower():
return True
except win32net.error, err:
#print err
except win32net.error, e:
_logger.exception("NetUserEnum: %s" % e)
return False
_logger.info("User '%s' not found on server '%s'" % (username, server))
return False
def _authUser(self, username, password, domain, server):
if not self._isUser(username, domain, server):
return False
@ -176,10 +188,12 @@ class SimpleWindowsDomainController(object):
try:
htoken = win32security.LogonUser(username, domain, password, win32security.LOGON32_LOGON_NETWORK, win32security.LOGON32_PROVIDER_DEFAULT)
except win32security.error, err:
#print err
_logger.warning("LogonUser failed for user '%s': %s" % (username, err))
return False
else:
if htoken:
htoken.Close() #guarantee's cleanup
_logger.debug("User '%s' logged on." % username)
return True
_logger.warning("Logon failed for user '%s'." % username)
return False

View file

@ -1,563 +0,0 @@
"""
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
+----------------------------------------------------------------------------+
| TODO: this module has not yet been ported from PyFileServer to WsgiDAV API |
+----------------------------------------------------------------------------+
Implementation of a DAV provider that provides a very basic, read-only
resource layer emulation of a MySQL database.
This module is specific to the WsgiDAV application. It provides a
classes ``SimpleMySQLResourceAbstractionLayer``.
Usage::
(see WsgiDAV-example.conf)
SimpleMySQLResourceAbstractionLayer(host, user, passwd, db)
host - host of database server
user - username to access database
passwd - passwd to access database
db - name of database on database server
The ``SimpleMySQLResourceAbstractionLayer`` provides a very basic, read-only
resource layer emulation of a MySQL database.
It provides the following interface:
- the root collection shared consists of collections that correspond to
table names
- in each table collection, there is a resource called "_ENTIRE_CONTENTS".
This is a non-collection resource that returns a csv representation of the
entire table
- if the table has a single primary key, each table record will also appear
as a non-collection resource in the table collection using the primary key
value as its name. This resource returns a csv representation of the record
and will also include the record attributes as live properties with
attribute name as property name and table name suffixed with colon as the
property namespace
This is a very basic interface and below is a by no means thorough summary of
its limitations:
- Really only supports having numbers or strings as primary keys. The code uses
a numeric or string comparison that may not hold up if the primary key is
a date or some other datatype.
- There is no handling for cases like BLOBs as primary keys or such. Well, there is
no handling for BLOBs in general.
- When returning contents, it buffers the entire contents! A bad way to return
large tables. Ideally you would have a FileMixin that reads the database even
as the application reads the file object....
- It takes too many database queries to return information.
Ideally there should be some sort of caching for metadata at least, to avoid
unnecessary queries to the database.
Abstraction Layers must provide the methods as described in
abstractionlayerinterface_
See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
.. _abstractionlayerinterface : interfaces/abstractionlayerinterface.py
"""
import MySQLdb
import MySQLdb.cursors
import md5
import time
import csv
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
__docformat__ = 'reStructuredText'
class SimpleMySQLResourceAbstractionLayer(object):
def __init__(self, host, user, passwd, db):
self._host = host
self._user = user
self._passwd = passwd
self._db = db
def _initConnection(self):
return MySQLdb.connect (host = self._host,
user = self._user,
passwd = self._passwd,
db = self._db)
def _getFieldList(self, conn, table_name):
retlist = []
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("DESCRIBE " + table_name)
result_set = cursor.fetchall ()
for row in result_set:
retlist.append(row["Field"])
cursor.close ()
return retlist
def _isDataTypeNumeric(self, datatype):
if datatype is None:
return False
#how many MySQL datatypes does it take to change a lig... I mean, store numbers
numerictypes = ['BIGINT',
'INTT',
'MEDIUMINT',
'SMALLINT',
'TINYINT',
'BIT',
'DEC',
'DECIMAL',
'DOUBLE',
'FLOAT',
'REAL',
'DOUBLE PRECISION',
'INTEGER',
'NUMERIC']
datatype = datatype.upper()
for numtype in numerictypes:
if datatype.startswith(numtype):
return True
return False
def _existsRecordByPrimaryKey(self, conn, table_name, pri_key_value):
pri_key = None
pri_field_type = None
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("DESCRIBE " + table_name)
result_set = cursor.fetchall ()
for row in result_set:
if row["Key"] == "PRI":
if pri_key is None:
pri_key = row["Field"]
pri_field_type = row["Type"]
else:
return False #more than one primary key - multipart key?
cursor.close ()
isNumType = self._isDataTypeNumeric(pri_field_type)
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
if isNumType:
cursor.execute("SELECT " + pri_key + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
else:
cursor.execute("SELECT " + pri_key + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
row = cursor.fetchone ()
if row is None:
cursor.close()
return False
cursor.close()
return True
def _getFieldByPrimaryKey(self, conn, table_name, pri_key_value, field_name):
pri_key = None
pri_field_type = None
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("DESCRIBE " + table_name)
result_set = cursor.fetchall ()
for row in result_set:
if row["Key"] == "PRI":
if pri_key is None:
pri_key = row["Field"]
pri_field_type = row["Type"]
else:
return None #more than one primary key - multipart key?
cursor.close ()
isNumType = self._isDataTypeNumeric(pri_field_type)
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
if isNumType:
cursor.execute("SELECT " + field_name + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
else:
cursor.execute("SELECT " + field_name + " FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
row = cursor.fetchone ()
if row is None:
cursor.close()
return None
val = str(row[field_name])
cursor.close()
return val
def _getRecordByPrimaryKey(self, conn, table_name, pri_key_value):
dictRet = {}
pri_key = None
pri_field_type = None
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("DESCRIBE " + table_name)
result_set = cursor.fetchall ()
for row in result_set:
if row["Key"] == "PRI":
if pri_key is None:
pri_key = row["Field"]
pri_field_type = row["Type"]
else:
return None #more than one primary key - multipart key?
cursor.close ()
isNumType = self._isDataTypeNumeric(pri_field_type)
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
if isNumType:
cursor.execute("SELECT * FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = " + pri_key_value)
else:
cursor.execute("SELECT * FROM " + self._db + "." + table_name + " WHERE " + pri_key + " = '" + pri_key_value + "'")
row = cursor.fetchone ()
if row is None:
cursor.close()
return None
for fname in row.keys():
dictRet[fname] = str(row[fname])
cursor.close()
return dictRet
def _findPrimaryKey(self, conn, table_name):
pri_key = None
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("DESCRIBE " + table_name)
result_set = cursor.fetchall ()
for row in result_set:
fieldname = row["Field"]
keyvalue = row["Key"]
if keyvalue == "PRI":
if pri_key is None:
pri_key = fieldname
else:
return None #more than one primary key - multipart key?
cursor.close ()
return pri_key
def _listFields(self, conn, table_name, field_name):
retlist = []
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute('SELECT ' + field_name + " FROM " + self._db + "." + table_name)
result_set = cursor.fetchall ()
for row in result_set:
retlist.append(str(row[field_name]))
cursor.close()
return retlist
def _listTables(self, conn):
retlist = []
cursor = conn.cursor ()
cursor.execute ("SHOW TABLES")
result_set = cursor.fetchall ()
for row in result_set:
retlist.append("%s" % (row[0]))
cursor.close ()
return retlist
def resolvePath(self, resheadpath, urlelementlist):
"""
resheadpath should always be "database"
"""
if len(urlelementlist) == 0:
return resheadpath
return resheadpath + ":" + ":".join(urlelementlist)
def breakPath(self, resheadpath, respath):
residue = respath[len(resheadpath):].strip(":")
return residue.split(":")
def getResourceDescriptor(self, respath):
resdata = respath.strip(":").split(":")
if len(resdata) == 1:
return ["Database", ""]
elif len(resdata) == 2: # "database" and table name
return ["Database Table", ""]
elif len(resdata) == 3:
if resdata[2] == "_ENTIRE_CONTENTS":
return ["Database Table Contents", "CSV Representation of Table Contents"]
else:
return ["Database Record", "Attributes available as properties"]
else:
return ["Unknown", "Unknown"]
def getResourceDescription(self, respath):
resdata = respath.strip(":").split(":")
if len(resdata) == 1:
return "Database"
elif len(resdata) == 2: # "database" and table name
return "Database Table"
elif len(resdata) == 3:
if resdata[2] == "_ENTIRE_CONTENTS":
return "Database Table Contents"
else:
return "Database Record"
else:
return "Unknown"
def getContentType(self, respath):
resdata = respath.strip(":").split(":")
if len(resdata) == 3:
return "text/csv"
else:
return "text/html"
def getLastModified(self, respath):
return time.time()
def supportContentLength(self, respath):
return False
def getContentLength(self, respath):
return 0
def getEntityTag(self, respath):
return md5.new(respath).hexdigest()
def isCollection(self, respath):
if self.exists(respath):
resdata = respath.strip(":").split(":")
return len(resdata) <= 2
else:
return False
def isResource(self, respath):
if self.exists(respath):
resdata = respath.strip(":").split(":")
return len(resdata) == 3
else:
return False
def exists(self, respath):
resdata = respath.strip(":").split(":")
if len(resdata) >= 2: #database:table_name check
conn = self._initConnection()
tbllist = self._listTables(conn)
conn.close()
if resdata[1] not in tbllist:
return False
if len(resdata) == 3: #database:table_name:value check
if resdata[2] == "_ENTIRE_CONTENTS":
return True
else:
conn = self._initConnection()
val = self._existsRecordByPrimaryKey(conn, resdata[1], resdata[2])
conn.close()
return val
return True
def createCollection(self, respath):
raise HTTPRequestException(processrequesterrorhandler.HTTP_FORBIDDEN)
def deleteCollection(self, respath):
raise HTTPRequestException(processrequesterrorhandler.HTTP_FORBIDDEN)
def supportEntityTag(self, respath):
return False
def supportLastModified(self, respath):
return False
def supportRanges(self, respath):
return False
def openResourceForRead(self, respath):
"""
respath - path identifier for the resource
returns a file-like object / stream containing the contents of the
resource specified.
The application will close() the stream.
"""
filestream = StringIO()
resdata = respath.strip(":").split(":")
if len(resdata) == 3:
table_name = resdata[1]
conn = self._initConnection()
listFields = self._getFieldList(conn, table_name)
csvwriter = csv.DictWriter(filestream, listFields, extrasaction='ignore')
dictFields = {}
for field_name in listFields:
dictFields[field_name] = field_name
csvwriter.writerow(dictFields)
if resdata[2] == "_ENTIRE_CONTENTS":
cursor = conn.cursor (MySQLdb.cursors.DictCursor)
cursor.execute ("SELECT * from " + self._db + "." + table_name)
result_set = cursor.fetchall ()
for row in result_set:
csvwriter.writerow(row)
cursor.close ()
else:
row = self._getRecordByPrimaryKey(conn, table_name, resdata[2])
if row is not None:
csvwriter.writerow(row)
conn.close()
#this suffices for small dbs, but
#for a production big database, I imagine you would have a FileMixin that
#does the retrieving and population even as the file object is being read
filestream.seek(0)
return filestream
#filevalue = filestream.getvalue()
#filestream.close()
#return StringIO.StringIO(filevalue)
def openResourceForWrite(self, respath, contenttype=None):
raise HTTPRequestException(processrequesterrorhandler.HTTP_FORBIDDEN)
def deleteResource(self, respath):
raise HTTPRequestException(processrequesterrorhandler.HTTP_FORBIDDEN)
def copyResource(self, respath, destrespath):
raise HTTPRequestException(processrequesterrorhandler.HTTP_FORBIDDEN)
def getParent(self, respath):
dsplit = respath.rsplit(":",1)
return dsplit[0]
def getMemberNames(self, respath):
"""
respath - path identifier for the resource
returns a list of names of resources contained in the collection resource
specified
"""
conn = self._initConnection()
resdata = respath.strip(":").split(":")
if len(resdata) == 1:
retlist = self._listTables(conn)
elif len(resdata) == 2:
pri_key = self._findPrimaryKey(conn, resdata[1])
if pri_key is not None:
retlist = self._listFields(conn, resdata[1], pri_key)
else:
retlist = []
retlist[0:0] = ["_ENTIRE_CONTENTS"]
else:
retlist = []
conn.close()
return retlist
def joinPath(self, rescollectionpath, resname):
return rescollectionpath + ":" + resname
def splitPath(self, respath):
dsplit = respath.rsplit(":",1)
return (dsplit[0],dsplit[1])
"""
Properties and WsgiDAV
---------------------------
Properties of a resource refers to the attributes of the resource. A property
is referenced by the property name and the property namespace. We usually
refer to the property as ``{property namespace}property name``
Properties of resources as defined in webdav falls under three categories:
Live properties
These properties are attributes actively maintained by the server, such as
file size, or read permissions. if you are sharing a database record as a
resource, for example, the attributes of the record could become the live
properties of the resource.
The webdav specification defines the following properties that could be
live properties (refer to webdav specification for details):
{DAV:}creationdate
{DAV:}displayname
{DAV:}getcontentlanguage
{DAV:}getcontentlength
{DAV:}getcontenttype
{DAV:}getetag
{DAV:}getlastmodified
{DAV:}resourcetype
{DAV:}source
These properties are implemented by the abstraction layer.
Locking properties
They refer to the two webdav-defined properties
{DAV:}supportedlock and {DAV:}lockdiscovery
These properties are implemented by the locking library in
``wsgidav.lock_manager`` and dead properties library in
``wsgidav.property_manager``
Dead properties
They refer to arbitrarily assigned properties not actively maintained.
These properties are implemented by the dead properties library in
``wsgidav.property_manager``
"""
def writeProperty(self, respath, propertyname, propertyns, propertyvalue):
raise HTTPRequestException(processrequesterrorhandler.HTTP_CONFLICT)
def removeProperty(self, respath, propertyname, propertyns):
raise HTTPRequestException(processrequesterrorhandler.HTTP_CONFLICT)
def getProperty(self, respath, propertyname, propertyns):
if propertyns == 'DAV:':
if propertyname == 'creationdate':
return time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(0))
elif propertyname == 'getcontenttype':
return self.getContentType(respath)
elif propertyname == 'resourcetype':
if self.isCollection(respath):
return '<D:collection />'
else:
return ''
resdata = respath.strip(":").split(":")
if len(resdata) == 3:
if propertyns == resdata[1] + ":":
conn = self._initConnection()
fieldlist = self._getFieldList(conn, resdata[1])
if propertyname in fieldlist:
val = self._getFieldByPrimaryKey(conn, resdata[1], resdata[2], propertyname)
conn.close()
return val
conn.close()
raise HTTPRequestException(processrequesterrorhandler.HTTP_NOT_FOUND)
def isPropertySupported(self, respath, propertyname, propertyns):
supportedliveprops = ['creationdate', 'getcontenttype','resourcetype']
if propertyns == "DAV:" and propertyname in supportedliveprops:
return True
resdata = respath.strip(":").split(":")
if len(resdata) == 3:
conn = self._initConnection()
fieldlist = self._getFieldList(conn, resdata[1])
conn.close()
ns = resdata[1] + ":"
if propertyns == ns and propertyname in fieldlist:
return True
return False
def getSupportedPropertyNames(self, respath):
appProps = []
#DAV properties for all resources
appProps.append( ('DAV:','creationdate') )
appProps.append( ('DAV:','getcontenttype') )
appProps.append( ('DAV:','resourcetype') )
resdata = respath.strip(":").split(":")
if len(resdata) == 3:
conn = self._initConnection()
fieldlist = self._getFieldList(conn, resdata[1])
ns = resdata[1] + ":"
for fieldname in fieldlist:
appProps.append( (ns,fieldname) )
conn.close()
return appProps

View file

@ -292,25 +292,25 @@ class VirtualResourceProvider(DAVProvider):
return res.entryList
def getSupportedInfoTypes(self, path):
"""Return a list of supported information types.
See DAVProvider.getSupportedInfoTypes()
"""
res = self._getResByPath(path)
infoTypes = ["isCollection",
"displayName",
"displayType",
]
if not res.isCollection():
infoTypes.append("contentType")
infoTypes.append("contentLength")
infoTypes.append("etag")
if isinstance(res, VirtualResFile):
infoTypes.append("created")
infoTypes.append("modified")
return infoTypes
# def getSupportedInfoTypes(self, path):
# """Return a list of supported information types.
#
# See DAVProvider.getSupportedInfoTypes()
# """
# res = self._getResByPath(path)
# infoTypes = ["isCollection",
# "displayName",
# "displayType",
# ]
# if not res.isCollection():
# infoTypes.append("contentType")
# infoTypes.append("contentLength")
# infoTypes.append("etag")
# if isinstance(res, VirtualResFile):
# infoTypes.append("created")
# infoTypes.append("modified")
#
# return infoTypes
def getInfoDict(self, path, typeList=None):
@ -332,6 +332,18 @@ class VirtualResourceProvider(DAVProvider):
else:
displayType = "%s-File" % res.data["type"]
# supportedInfoTypes = ["isCollection",
# "displayName",
# "displayType",
# ]
# if not isCollection():
# supportedInfoTypes.append("contentType")
# supportedInfoTypes.append("contentLength")
# supportedInfoTypes.append("etag")
# if isinstance(res, VirtualResFile):
# supportedInfoTypes.append("created")
# supportedInfoTypes.append("modified")
dict = {"contentLength": None,
"contentType": None,
"name": name,
@ -341,6 +353,7 @@ class VirtualResourceProvider(DAVProvider):
"modified": res.getModifiedDate(),
"created": res.getCreationDate(),
"supportRanges": False,
# "supportedInfoTypes": supportedInfoTypes,
"isCollection": isCollection,
}
# fp = res.data.get("file")
@ -378,6 +391,6 @@ class VirtualResourceProvider(DAVProvider):
raise DAVError(HTTP_FORBIDDEN)
def openResourceForRead(self, path):
def openResourceForRead(self, path, davres=None):
res = self._getResByPath(path)
return res.getContent()

View file

@ -12,7 +12,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
import sys
__docformat__ = 'reStructuredText'
__docformat__ = "reStructuredText"
#===============================================================================
# List of HTTP Response Codes.
@ -36,7 +36,6 @@ HTTP_MOVED = 301
HTTP_FOUND = 302
HTTP_SEE_OTHER = 303
HTTP_NOT_MODIFIED = 304
HTTP_USE_PROXY = 305
HTTP_TEMP_REDIRECT = 307
HTTP_BAD_REQUEST = 400
@ -76,6 +75,7 @@ HTTP_NOT_EXTENDED = 510
# sent as the error response code.
# Otherwise only the numeric code itself is sent.
#===============================================================================
# TODO: paste.httpserver may raise exceptions, if a status code is not followed by a description, so should define all of them.
ERROR_DESCRIPTIONS = {
HTTP_OK: "200 OK",
HTTP_CREATED: "201 Created",
@ -85,7 +85,7 @@ ERROR_DESCRIPTIONS = {
HTTP_FORBIDDEN: "403 Forbidden",
HTTP_METHOD_NOT_ALLOWED: "405 Method Not Allowed",
HTTP_NOT_FOUND: "404 Not Found",
HTTP_CONFLICT: '409 Conflict',
HTTP_CONFLICT: "409 Conflict",
HTTP_PRECONDITION_FAILED: "412 Precondition Failed",
HTTP_RANGE_NOT_SATISFIABLE: "416 Range Not Satisfiable",
HTTP_MEDIATYPE_NOT_SUPPORTED: "415 Media Type Not Supported",
@ -93,6 +93,7 @@ ERROR_DESCRIPTIONS = {
HTTP_FAILED_DEPENDENCY: "424 Failed Dependency",
HTTP_INTERNAL_ERROR: "500 Internal Server Error",
HTTP_NOT_IMPLEMENTED: "501 Not Implemented",
HTTP_BAD_GATEWAY: "502 Bad Gateway",
}
#===============================================================================
@ -105,7 +106,7 @@ ERROR_RESPONSES = {
HTTP_BAD_REQUEST: "An invalid request was specified",
HTTP_NOT_FOUND: "The specified resource was not found",
HTTP_FORBIDDEN: "Access denied to the specified resource",
HTTP_INTERNAL_ERROR: "An internal server error occured",
HTTP_INTERNAL_ERROR: "An internal server error occurred",
HTTP_NOT_IMPLEMENTED: "Not Implemented",
}
@ -170,6 +171,9 @@ class DAVError(Exception):
# return repr(self.value)
return "DAVError(%s)" % self.getUserInfo()
def __str__(self): # Required for 2.4
return self.__repr__()
def getUserInfo(self):
"""Return readable string."""
if self.value in ERROR_DESCRIPTIONS:

View file

@ -2,8 +2,8 @@
dav_provider
============
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
:Author: Martin Wendt, moogle(at)wwwendt.de
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
Abstract base class for DAV resource providers.
@ -45,30 +45,84 @@ lockmMnager
See lock_manager.LockManager for a sample implementation
using shelve.
"""
from wsgidav import util
__docformat__ = 'reStructuredText'
__docformat__ = "reStructuredText"
import urllib
import time
from lxml import etree
import traceback
import sys
import time
import traceback
import urllib
from wsgidav import util
# Trick PyDev to do intellisense and don't produce warnings:
from util import etree #@UnusedImport
if False: from xml.etree import ElementTree as etree #@Reimport @UnresolvedImport
from dav_error import DAVError, \
HTTP_NOT_FOUND, HTTP_FORBIDDEN,\
PRECONDITION_CODE_ProtectedProperty, asDAVError
_livePropNames = ['{DAV:}creationdate',
'{DAV:}displayname',
'{DAV:}getcontenttype',
'{DAV:}resourcetype',
'{DAV:}getlastmodified',
'{DAV:}getcontentlength',
'{DAV:}getetag',
'{DAV:}getcontentlanguage',
'{DAV:}source',
'{DAV:}lockdiscovery',
'{DAV:}supportedlock']
_logger = util.getModuleLogger(__name__)
_livePropNames = ["{DAV:}creationdate",
"{DAV:}displayname",
"{DAV:}getcontenttype",
"{DAV:}resourcetype",
"{DAV:}getlastmodified",
"{DAV:}getcontentlength",
"{DAV:}getetag",
"{DAV:}getcontentlanguage",
"{DAV:}source",
"{DAV:}lockdiscovery",
"{DAV:}supportedlock"]
#===============================================================================
# DAVResource
#===============================================================================
class DAVResource(object):
"""Helper class that represents a single DAV resource instance.
This class reads resource information on initialization, so querying
multiple live properties can be handled more efficiently.
See also DAVProvider.getInfoDict().
"""
def __init__(self, davProvider, path, typeList=None):
self.provider = davProvider
self.path = path
self._dict = davProvider.getInfoDict(path, typeList) or {}
self._exists = bool(self._dict)
def __repr__(self):
return "%s(%s): %s" % (self.__class__.__name__, self.path, self._dict)
def exists(self):
return self._exists
def isCollection(self):
return self._exists and self._dict["isCollection"]
def isResource(self):
return self._exists and not self._dict["isCollection"]
def contentLength(self):
return self._dict.get("contentLength")
def contentType(self):
return self._dict.get("contentType")
def created(self):
return self._dict.get("created")
def displayName(self):
return self._dict.get("displayName")
def displayType(self):
return self._dict.get("displayType")
def etag(self):
return self._dict.get("etag")
def modified(self):
return self._dict.get("modified")
def name(self):
return self._dict.get("name")
def supportRanges(self):
return self._dict.get("supportRanges")
def supportEtag(self):
return self._dict.get("etag") is not None
def supportModified(self):
return self._dict.get("modified") is not None
def supportContentLength(self):
return self._dict.get("contentLength") is not None
#===============================================================================
@ -76,45 +130,44 @@ _livePropNames = ['{DAV:}creationdate',
#===============================================================================
class DAVProvider(object):
"""Abstract base class for DAV resource providers."""
"""Abstract base class for DAV resource providers.
There will be only one DAVProvider instance per share (not per request).
"""
"""Available info types, that a DAV Provider MAY support.
See DAVProvider.getInfoDict() for details."""
INFO_TYPES = ["contentLength",
"contentType",
"created",
"displayName",
"displayType",
"etag",
"isCollection",
"modified",
"name",
"supportRanges",
]
def __init__(self):
self.mountPath = ""
self.sharePath = None # TODO_ define encoding / quoting
self.sharePath = None
self.lockManager = None
self.propManager = None
self.verbose = 2
self.caseSensitiveUrls = True
def _log(self, msg):
if self.verbose >= 2:
print msg
def __repr__(self):
return self.__class__.__name__
def setMountPath(self, mountPath):
"""Set application root for this resource provider.
This is the value of SCRIPT_NAME, when WsgiDAVApp is called.
"""
assert mountPath in ("", "/") or not mountPath.endswith("/")
self.mountPath = mountPath
def setSharePath(self, sharePath):
"""Set application location for this resource provider.
@param sharePath: a ISO-8859-1 encoded byte string (unquoted).
@param sharePath: a UTF-8 encoded, unquoted byte string.
"""
if isinstance(sharePath, unicode):
sharePath = sharePath.encode("iso_8859_1")
sharePath = sharePath.encode("utf8")
assert sharePath=="" or sharePath.startswith("/")
if sharePath == "/":
sharePath = "" # This allows to code 'absPath = sharePath + path'
@ -137,7 +190,10 @@ class DAVProvider(object):
Different URLs may map to the same resource, e.g.:
'/a/b' == '/A/b' == '/a/b/'
getPreferredPath() returns the same value for all these variants, e.g.:
'/a/b/'
'/a/b/' (assuming resource names considered case insensitive)
@param path: a UTF-8 encoded, unquoted byte string.
@return: a UTF-8 encoded, unquoted byte string.
"""
if path in ("", "/"):
return "/"
@ -153,45 +209,62 @@ class DAVProvider(object):
def getRefUrl(self, path):
"""Return the quoted, absolute, unique URL of a resource, relative to the server.
"""Return the quoted, absolute, unique URL of a resource, relative to appRoot.
Byte string, ISO-8859-1 encoded.
Byte string, UTF-8 encoded, quoted.
Starts with a '/'. Collections also have a trailing '/'.
This is basically the same as normPath, but deals with 'virtual locations'
as well.
Since it is always unique for one resource, <refUrl> is used as key for the
lock- and property storage.
This is basically the same as getPreferredPath, but deals with
'virtual locations' as well.
e.g. '/a/b' == '/A/b' == '/bykey/123' == '/byguid/as532'
getRefUrl() returns the same value for all these URLs, so it can be
used for locking and persistence.
used as a key for locking and persistence storage.
DAV providers that allow these virtual-mappings must override this
method.
DAV providers that allow virtual-mappings must override this method.
See also comments in DEVELOPERS.txt glossary.
"""
return urllib.quote(self.sharePath + self.getPreferredPath(path))
# def getRefKey(self, path):
# """Return an unambigous identifier string for a resource.
#
# Since it is always unique for one resource, <refKey> is used as key for
# the lock- and property storage dictionaries.
#
# This default implementation calls getRefUrl(), and strips a possible
# trailing '/'.
# """
# refKey = self.getRefUrl(path)
# if refKey == "/":
# return refKey
# return refKey.rstrip("/")
def getHref(self, path):
"""Convert path to a URL that can be passed to XML responses.
Byte string, UTF-8 encoded, quoted.
@see http://www.webdav.org/specs/rfc4918.html#rfc.section.8.3
We are using the path-absolute option. i.e. starting with '/'.
We are using the path-absolute option. i.e. starting with '/'.
URI ; See section 3.2.1 of [RFC2068]
"""
# TODO: Nautilus chokes, if href encodes '(' as '%28'
# Nautilus chokes, if href encodes '(' as '%28'
# So we don't encode 'extra' and 'safe' characters (see rfc2068 3.2.1)
safe = "/" + "!*'()," + "$-_|."
return urllib.quote(self.sharePath + self.getPreferredPath(path), safe=safe)
return urllib.quote(self.mountPath + self.sharePath + self.getPreferredPath(path), safe=safe)
def refUrlToPath(self, refUrl):
"""Convert a refUrl to a path, by stripping the mount prefix."""
return "/" + urllib.unquote(refUrl.lstrip(self.sharePath).lstrip("/"))
"""Convert a refUrl to a path, by stripping the mount prefix.
Used to calculate the <path> from a storage key by inverting getRefUrl().
"""
return "/" + urllib.unquote(util.lstripstr(refUrl, self.sharePath)).lstrip("/")
def getParent(self, path):
@ -206,7 +279,7 @@ class DAVProvider(object):
def getMemberNames(self, path):
"""Return list of (direct) collection member names (ISO-8859-1 byte strings).
"""Return list of (direct) collection member names (UTF-8 byte strings).
Every provider must override this method.
"""
@ -221,12 +294,15 @@ class DAVProvider(object):
"""Return iterator of child path's.
This default implementation calls getMemberNames() recursively.
@param deptFirst: use <False>, to list containers before content.
(e.g. when moving / copying branches.)
Use <True>, to list content before containers.
(e.g. when deleting branches.)
@param depth: '0' | '1' | 'infinity'
:Parameters:
depthFirst : bool
use <False>, to list containers before content.
(e.g. when moving / copying branches.)
Use <True>, to list content before containers.
(e.g. when deleting branches.)
depth : string
'0' | '1' | 'infinity'
"""
assert depth in ("0", "1", "infinity")
@ -265,22 +341,24 @@ class DAVProvider(object):
return list(self.iter(path, collections=True, resources=True, depthFirst=False, depth="1", addSelf=False))
def getSupportedInfoTypes(self, path):
"""Return a list of supported information types for a resource.
Return None, if <path> does not exist.
Otherwise return a list with a subset of DAVProvider.INFO_TYPES
This method must be implemented.
"""
raise NotImplementedError()
def getInfoDict(self, path, typeList=None):
"""Return info dictionary for path.
"""Return an info dictionary for path.
This function is used to ...
This function is mainly used to query live properties for a resource.
Also display information should be provided here, that can be used to
render HTML directories.
The assumption is, that it is more efficient to query all infos in one
call, rather than have single calls for every info type.
``getInfoDict()`` is called indirectly by the ``DAVResource`` constructor.
It should be called only once per request and resource::
davres = DAVResource(davprovider, path)
[..]
if davres.exists():
print davres.contentType()
Return None, if <path> does not exist.
Otherwise return a dictionary with these items:
@ -296,7 +374,8 @@ class DAVProvider(object):
(int) last modification date (in seconds, compatible with time module)
created:
(int) creation date (in seconds, compatible with time module)
and for simple resources (i.e. isCollection == False):
and additionally for simple resources (i.e. isCollection == False):
contentType:
(str) MIME type of content
contentLength:
@ -310,31 +389,17 @@ class DAVProvider(object):
not supported.
typeList MAY be passed, to specify a list of requested information types.
A caller may pass an empty array, if he only wants to check for existence.
The implementation MAY uses this list to avoid expensive calculation of
unwanted information types.
A caller may pass an empty array for typeList, if he only wants to check
for the existence of path.
This method must be implemented.
"""
raise NotImplementedError()
def isInfoTypeSupported(self, path, infoType):
"""Shortcut to query support of one single info type via getSupportedInfoTypes().
This method may be overridden with a more efficient version.
"""
assert infoType in DAVProvider.INFO_TYPES
return infoType in self.getSupportedInfoTypes(path)
def getInfo(self, path, infoType):
"""Shortcut to query one single value via getInfoDict(). """
assert infoType in DAVProvider.INFO_TYPES
return self.getInfoDict(path, [infoType]).get(infoType)
def exists(self, path):
"""Return True, if path maps to an existing resource.
@ -361,10 +426,10 @@ class DAVProvider(object):
# --- Properties -----------------------------------------------------------
def getPropertyNames(self, path, mode="allprop"):
def getPropertyNames(self, davres, mode="allprop"):
"""Return list of supported property names in Clark Notation.
@param mode: 'allprop': common properties, that should be send on 'allprop' requests.
@param mode: 'allprop': common properties, that should be sent on 'allprop' requests.
'propname': all available properties.
This default implementation returns a combination of:
@ -379,7 +444,7 @@ class DAVProvider(object):
raise ValueError("Invalid mode '%s'." % mode)
# use a copy
propNameList = self.getSupportedLivePropertyNames(path) [:]
propNameList = self.getSupportedLivePropertyNames(davres) [:]
if self.lockManager:
if not "{DAV:}lockdiscovery" in propNameList:
@ -388,46 +453,47 @@ class DAVProvider(object):
propNameList.append("{DAV:}supportedlock")
if self.propManager:
refUrl = self.getRefUrl(path)
refUrl = self.getRefUrl(davres.path)
for deadProp in self.propManager.getProperties(refUrl):
propNameList.append(deadProp)
return propNameList
def getProperties(self, path, mode, nameList=None, namesOnly=False):
def getProperties(self, davres, mode, nameList=None, namesOnly=False):
"""Return properties as list of 2-tuples (name, value).
<name> is the property name in Clark notation.
<value> may have different types, depending on the status:
name
is the property name in Clark notation.
value
may have different types, depending on the status:
- string or unicode: for standard property values.
- lxml.etree.Element: for complex values.
- etree.Element: for complex values.
- DAVError in case of errors.
- None: if namesOnly was passed.
@param path:
@param mode: "allprop", "propname", or "named"
@param nameList: list of property names in Clark Notation (only for mode 'named')
@param namesOnly: return None for <value>
@param namesOnly: return None for <value>
@param davres: pass a DAVResource to access cached info
"""
if not mode in ("allprop", "propname", "named"):
raise ValueError("Invalid mode '%s'." % mode)
if mode in ("allprop", "propname"):
# TODO: allprop can have nameList, when <include> option is implemented
# TODO: we create a namelist for the root path, but it should be constructed for every child individually?
assert nameList is None
nameList = self.getPropertyNames(path, mode)
nameList = self.getPropertyNames(davres, mode)
else:
assert nameList is not None
propList = []
for name in nameList:
try:
if namesOnly:
propList.append( (name, None) )
else:
value = self.getPropertyValue(path, name)
value = self.getPropertyValue(davres.path, name, davres)
propList.append( (name, value) )
except DAVError, e:
propList.append( (name, e) )
@ -439,11 +505,17 @@ class DAVProvider(object):
return propList
def getPropertyValue(self, path, name):
def getPropertyValue(self, path, propname, davres=None):
"""Return the value of a property.
name:
path:
resource path.
propname:
is the property name in Clark notation.
davres:
DAVResource instance. Contains cached resource information.
Manadatory for live properties, but may be omitted for
'{DAV:}lockdiscovery' or dead properties.
return value:
may have different types, depending on the status:
@ -455,19 +527,21 @@ class DAVProvider(object):
This default implementation handles ``{DAV:}lockdiscovery`` and
``{DAV:}supportedlock`` using the associated lock manager.
All other *live* properties (i.e. name starts with ``{DAV:}``) are
All other *live* properties (i.e. propname starts with ``{DAV:}``) are
delegated to self.getLivePropertyValue()
Finally, other properties are considered *dead*, and are handled using
the associated property manager.
"""
refUrl = self.getRefUrl(path)
# refUrl = str(refUrl)
if self.lockManager and name == '{DAV:}lockdiscovery':
# print "getPropertyValue(%s, %s)" % (path, propname)
if self.lockManager and propname == "{DAV:}lockdiscovery":
# TODO: we return HTTP_NOT_FOUND if no lockmanager is present. Correct?
lm = self.lockManager
activelocklist = lm.getUrlLockList(refUrl)
lockdiscoveryEL = etree.Element(name)
lockdiscoveryEL = etree.Element(propname)
for lock in activelocklist:
activelockEL = etree.SubElement(lockdiscoveryEL, "{DAV:}activelock")
@ -484,9 +558,9 @@ class DAVProvider(object):
timeout = lock["timeout"]
if timeout < 0:
timeout = 'Infinite'
timeout = "Infinite"
else:
timeout = 'Second-' + str(long(timeout - time.time()))
timeout = "Second-" + str(long(timeout - time.time()))
etree.SubElement(activelockEL, "{DAV:}timeout").text = timeout
locktokenEL = etree.SubElement(activelockEL, "{DAV:}locktoken")
@ -498,10 +572,10 @@ class DAVProvider(object):
return lockdiscoveryEL
elif self.lockManager and name == '{DAV:}supportedlock':
elif self.lockManager and propname == "{DAV:}supportedlock":
# TODO: we return HTTP_NOT_FOUND if no lockmanager is present. Correct?
# TODO: the lockmanager should decide about it's features
supportedlockEL = etree.Element(name)
supportedlockEL = etree.Element(propname)
lockentryEL = etree.SubElement(supportedlockEL, "{DAV:}lockentry")
lockscopeEL = etree.SubElement(lockentryEL, "{DAV:}lockscope")
@ -517,14 +591,15 @@ class DAVProvider(object):
return supportedlockEL
elif name.startswith("{DAV:}"):
elif propname.startswith("{DAV:}"):
assert davres is not None, "Must pass DAVResource for querying live properties"
# Standard live property (raises HTTP_NOT_FOUND if not supported)
return self.getLivePropertyValue(path, name)
return self.getLivePropertyValue(davres, propname)
# Dead property
if self.propManager:
refUrl = self.getRefUrl(path)
value = self.propManager.getProperty(refUrl, name)
value = self.propManager.getProperty(refUrl, propname)
if value is not None:
# return value
return etree.XML(value)
@ -533,7 +608,7 @@ class DAVProvider(object):
raise DAVError(HTTP_NOT_FOUND)
def setPropertyValue(self, path, name, value, dryRun=False):
def setPropertyValue(self, path, propname, value, dryRun=False):
"""Set or remove property value.
value == None means 'remove property'.
@ -544,29 +619,29 @@ class DAVProvider(object):
run, but MUST NOT change any data.
@param path:
@param name: property name in Clark Notation
@param propname: property name in Clark Notation
@param value: value == None means 'remove property'.
@param dryRun: boolean
"""
# if value is not None and not isinstance(value, (unicode, str, etree._Element)):
if value is not None and not isinstance(value, (etree._Element)):
raise ValueError()
if self.lockManager and name in ("{DAV:}lockdiscovery", "{DAV:}supportedlock"):
if self.lockManager and propname in ("{DAV:}lockdiscovery", "{DAV:}supportedlock"):
raise DAVError(HTTP_FORBIDDEN, # TODO: Chun used HTTP_CONFLICT
preconditionCode=PRECONDITION_CODE_ProtectedProperty)
if name.startswith("{DAV:}"):
if propname.startswith("{DAV:}"):
# raises DAVError(HTTP_FORBIDDEN) if read-only, or not supported
return self.setLivePropertyValue(path, name, value, dryRun)
return self.setLivePropertyValue(path, propname, value, dryRun)
# Dead property
if self.propManager:
# TODO: do we write all proprties?
# TODO: accept etree._Element
refUrl = self.getRefUrl(path)
if value is None:
return self.propManager.removeProperty(refUrl, name)
return self.propManager.removeProperty(refUrl, propname)
else:
value = etree.tostring(value, pretty_print=False)
return self.propManager.writeProperty(refUrl, name, value, dryRun)
value = etree.tostring(value)
return self.propManager.writeProperty(refUrl, propname, value, dryRun)
raise DAVError(HTTP_FORBIDDEN) # TODO: Chun used HTTP_CONFLICT
@ -577,77 +652,71 @@ class DAVProvider(object):
self.propManager.removeProperties(self.getRefUrl(path))
def getSupportedLivePropertyNames(self, path):
"""Return list of supported live properties in Clark Notation.
SHOULD NOT add {DAV:}lockdiscovery and {DAV:}supportedlock.
This default implementation uses self.getSupportedInfoTypes() to figure
it out.
"""
types = self.getSupportedInfoTypes(path)
appProps = []
if "created" in types:
appProps.append("{DAV:}creationdate")
if "contentType" in types:
appProps.append("{DAV:}getcontenttype")
if "isCollection" in types:
appProps.append("{DAV:}resourcetype")
if "modified" in types:
appProps.append("{DAV:}getlastmodified")
if "displayName" in types:
appProps.append("{DAV:}displayname")
if "etag" in types:
appProps.append("{DAV:}getetag")
# for non-collections:
if "contentLength" in types:
appProps.append("{DAV:}getcontentlength")
return appProps
def getLivePropertyValue(self, path, name):
def getSupportedLivePropertyNames(self, davres):
"""Return list of supported live properties in Clark Notation.
SHOULD NOT add {DAV:}lockdiscovery and {DAV:}supportedlock.
This default implementation uses self.getInfoDict() to figure it out.
"""
# TODO: this could be more efficient if we could query a list of
# livre props with a single call. Currently getInfoDict is called
# once per prop!
infos = self.getInfoDict(path)
propmap = {"created": "{DAV:}creationdate",
"contentType": "{DAV:}getcontenttype",
"isCollection": "{DAV:}resourcetype",
"modified": "{DAV:}getlastmodified",
"displayName": "{DAV:}displayname",
"etag": "{DAV:}getetag",
}
if not davres.isCollection():
propmap["contentLength"] = "{DAV:}getcontentlength"
appProps = []
for k, v in propmap.items():
if k in davres._dict:
appProps.append(v)
return appProps
if name == '{DAV:}creationdate':
return util.getRfc1123Time(infos["created"])
elif name == '{DAV:}getcontenttype':
return infos["contentType"]
def getLivePropertyValue(self, davres, propname):
"""Return list of supported live properties in Clark Notation.
SHOULD NOT add {DAV:}lockdiscovery and {DAV:}supportedlock.
elif name == '{DAV:}resourcetype':
if infos["isCollection"]:
resourcetypeEL = etree.Element(name)
This default implementation uses self.getInfoDict() to figure it out.
"""
if not davres.exists():
raise DAVError(HTTP_NOT_FOUND)
if propname == "{DAV:}creationdate":
return util.getRfc1123Time(davres.created())
elif propname == "{DAV:}getcontenttype":
return davres.contentType()
elif propname == "{DAV:}resourcetype":
if davres.isCollection():
resourcetypeEL = etree.Element(propname)
etree.SubElement(resourcetypeEL, "{DAV:}collection")
return resourcetypeEL
return ""
elif name == '{DAV:}getlastmodified':
return util.getRfc1123Time(infos["modified"])
elif propname == "{DAV:}getlastmodified":
return util.getRfc1123Time(davres.modified())
elif name == '{DAV:}getcontentlength':
if infos["contentLength"] is not None:
return str(infos["contentLength"])
elif propname == "{DAV:}getcontentlength":
if davres.contentLength() is not None:
return str(davres.contentLength())
elif name == '{DAV:}getetag':
return infos["etag"]
elif propname == "{DAV:}getetag":
return davres.etag()
elif name == '{DAV:}displayname':
return infos["displayName"]
elif propname == "{DAV:}displayname":
return davres.displayName()
# No persistence available, or property not found
raise DAVError(HTTP_NOT_FOUND)
def setLivePropertyValue(self, path, name, value, dryRun=False):
def setLivePropertyValue(self, path, propname, value, dryRun=False):
"""Set or remove a live property value.
value == None means 'remove property'.
@ -663,7 +732,7 @@ class DAVProvider(object):
removed.
@param path:
@param name: property name in Clark Notation
@param propname: property name in Clark Notation
@param value: value == None means 'remove property'.
@param dryRun: boolean
@ -716,7 +785,7 @@ class DAVProvider(object):
raise DAVError(HTTP_FORBIDDEN)
def openResourceForRead(self, path):
def openResourceForRead(self, path, davres=None):
"""Every provider must override this method."""
raise NotImplementedError()

View file

@ -17,7 +17,7 @@ from wsgidav import util
import sys
import threading
__docformat__ = 'reStructuredText'
__docformat__ = "reStructuredText"
class WsgiDavDebugFilter(object):
@ -25,7 +25,7 @@ class WsgiDavDebugFilter(object):
def __init__(self, application):
self._application = application
self.out = sys.stderr
self.passedLitmus = {}
self._dumpheaders = [
@ -63,6 +63,7 @@ class WsgiDavDebugFilter(object):
# "basic: 9",
# "basic: 14",
# "props: 16",
"props: 18",
# "locks: 9",
# "locks: 12",
# "locks: 13",
@ -85,8 +86,8 @@ class WsgiDavDebugFilter(object):
def __call__(self, environ, start_response):
""""""
# TODO: pass srvcfg with constructor instead?
srvcfg = environ['wsgidav.config']
verbose = srvcfg.get('verbose', 2)
srvcfg = environ["wsgidav.config"]
verbose = srvcfg.get("verbose", 2)
debugBreak = False
dumpRequest = False
dumpResponse = False
@ -105,7 +106,7 @@ class WsgiDavDebugFilter(object):
# Turn on max. debugging for selected litmus tests
litmusTag = environ.get("HTTP_X_LITMUS", environ.get("HTTP_X_LITMUS_SECOND"))
if litmusTag and verbose >= 2:
print >> environ['wsgi.errors'], "----\nRunning litmus test '%s'..." % litmusTag
print >> self.out, "----\nRunning litmus test '%s'..." % litmusTag
for litmusSubstring in self._debuglitmus:
if litmusSubstring in litmusTag:
verbose = 3
@ -115,7 +116,7 @@ class WsgiDavDebugFilter(object):
break
for litmusSubstring in self._break_after_litmus:
if litmusSubstring in self.passedLitmus and litmusSubstring not in litmusTag:
print >> environ['wsgi.errors'], " *** break after litmus %s" % litmusTag
print >> self.out, " *** break after litmus %s" % litmusTag
sys.exit(-1)
if litmusSubstring in litmusTag:
self.passedLitmus[litmusSubstring] = True
@ -128,11 +129,11 @@ class WsgiDavDebugFilter(object):
dumpResponse = True
if dumpRequest:
print >> environ['wsgi.errors'], "<======== Request from <%s> %s" % (threading._get_ident(), threading.currentThread())
print >> self.out, "<======== Request from <%s> %s" % (threading._get_ident(), threading.currentThread())
for k, v in environ.items():
if k == k.upper():
print >> environ['wsgi.errors'], "%20s: »%s«" % (k, v)
print >> environ['wsgi.errors'], "\n"
print >> self.out, "%20s: »%s«" % (k, v)
print >> self.out, "\n"
elif verbose >= 2:
# Dump selected headers
printedHeader = False
@ -140,13 +141,13 @@ class WsgiDavDebugFilter(object):
if k in self._dumpheaders:
if not printedHeader:
printedHeader = True
print >> environ['wsgi.errors'], "<======== Request from <%s> %s" % (threading._get_ident(), threading.currentThread())
print >> environ['wsgi.errors'], "%20s: »%s«" % (k, v)
print >> self.out, "<======== Request from <%s> %s" % (threading._get_ident(), threading.currentThread())
print >> self.out, "%20s: »%s«" % (k, v)
# Set debug options to environment
environ['wsgidav.verbose'] = verbose
environ['wsgidav.debug_methods'] = self._debugmethods
environ['wsgidav.debug_break'] = debugBreak
environ["wsgidav.verbose"] = verbose
environ["wsgidav.debug_methods"] = self._debugmethods
environ["wsgidav.debug_break"] = debugBreak
# TODO: add timings and byte/request conters
@ -158,19 +159,19 @@ class WsgiDavDebugFilter(object):
util.log("DebugFilter got exception arg", exc_info)
# raise exc_info
if dumpResponse:
print >> environ['wsgi.errors'], "=========> Response from <%s> %s" % (threading._get_ident(), threading.currentThread())
print >> self.out, "=========> Response from <%s> %s" % (threading._get_ident(), threading.currentThread())
print >> environ['wsgi.errors'], 'Response code:', respcode
print >> self.out, "Response code:", respcode
headersdict = dict(headers)
for envitem in headersdict.keys():
print >> environ['wsgi.errors'], "\t", envitem, ":\t", repr(headersdict[envitem])
print >> environ['wsgi.errors'], "\n"
print >> self.out, "\t", envitem, ":\t", repr(headersdict[envitem])
print >> self.out, "\n"
return start_response(respcode, headers, exc_info)
for v in iter(self._application(environ, start_response_wrapper)):
util.debug("sc", "debug_filter: yield response chunk (%s bytes)" % len(v))
if dumpResponse and environ['REQUEST_METHOD'] != 'GET':
print >> environ['wsgi.errors'], v
# util.log("debug_filter: yield response chunk (%s bytes)" % len(v))
if dumpResponse and environ["REQUEST_METHOD"] != "GET":
print >> self.out, v
yield v
return

View file

@ -1,7 +1,7 @@
# -*- coding: iso-8859-1 -*-
"""
request_server
=============
==============
:Author: Martin Wendt, moogle(at)wwwendt.de
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
@ -15,6 +15,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
from wsgidav.dav_error import DAVError, HTTP_OK, HTTP_MEDIATYPE_NOT_SUPPORTED
from wsgidav.dav_provider import DAVResource
import sys
import urllib
import util
@ -35,10 +36,17 @@ class WsgiDavDirBrowser(object):
dav = environ["wsgidav.provider"]
if environ["REQUEST_METHOD"] in ("GET", "HEAD" ) and dav and dav.isCollection(path):
# TODO: do we nee to handle IF headers?
# TODO: do we need to handle IF headers?
# self._evaluateIfHeaders(path, environ)
if environ["REQUEST_METHOD"] =="HEAD":
return util.sendSimpleResponse(environ, start_response, HTTP_OK)
# if True:
# from cProfile import Profile
# profile = Profile()
# profile.runcall(self._listDirectory, environ, start_response)
# # sort: 0:"calls",1:"time", 2: "cumulative"
# profile.print_stats(sort=2)
return self._listDirectory(environ, start_response)
return self._application(environ, start_response)
@ -67,8 +75,10 @@ class WsgiDavDirBrowser(object):
displaypath = urllib.unquote(dav.getHref(path))
trailer = environ.get("wsgidav.config", {}).get("response_trailer")
o_list = []
o_list.append('<html><head><title>WsgiDAV - Index of %s </title>' % displaypath)
o_list = []
o_list.append("<html><head>")
o_list.append("<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />")
o_list.append("<title>WsgiDAV - Index of %s </title>" % displaypath)
o_list.append("""\
<style type="text/css">
img { border: 0; padding: 0 2px; vertical-align: text-bottom; }
@ -80,22 +90,33 @@ class WsgiDavDirBrowser(object):
</head>
<body>
""")
o_list.append('<H1>%s</H1>' % (displaypath,))
o_list.append("<H1>%s</H1>" % displaypath)
o_list.append("<hr/><table>")
if path in ("", "/"):
o_list.append('<tr><td colspan="4">Top level share</td></tr>')
o_list.append("<tr><td colspan='4'>Top level share</td></tr>")
else:
o_list.append('<tr><td colspan="4"><a href="' + dav.getHref(dav.getParent(path)) + '">Up to higher level</a></td></tr>')
o_list.append("<tr><td colspan='4'><a href='" + dav.getHref(dav.getParent(path)) + "'>Up to higher level</a></td></tr>")
for name in dav.getMemberNames(path):
childPath = path.rstrip("/") + "/" + name
infoDict = dav.getInfoDict(childPath)
infoDict["strModified"] = util.getRfc1123Time(infoDict["modified"])
if infoDict["contentLength"] or not infoDict["isCollection"]:
infoDict["strSize"] = "%s B" % infoDict["contentLength"]
res = DAVResource(dav, childPath)
infoDict = res._dict
if not infoDict:
print >>sys.stderr, "WARNING: WsgiDavDirBrowser could not getInfoDict for '%s'" % childPath
continue
if infoDict["modified"] is not None:
infoDict["strModified"] = util.getRfc1123Time(infoDict["modified"])
else:
infoDict["strModified"] = ""
if infoDict["contentLength"] is not None and not infoDict["isCollection"]:
# infoDict["strSize"] = "%s Bytes" % infoDict["contentLength"]
infoDict["strSize"] = util.byteNumberString(infoDict["contentLength"])
else:
infoDict["strSize"] = ""
infoDict["url"] = dav.getHref(path).rstrip("/") + "/" + urllib.quote(name)
if infoDict["isCollection"]:
infoDict["url"] = infoDict["url"] + "/"
@ -105,25 +126,13 @@ class WsgiDavDirBrowser(object):
<td>%(displayType)s</td>
<td>%(strSize)s</td>
<td>%(strModified)s</td></tr>\n""" % infoDict)
# for name in dav.getMemberNames(path):
# childPath = path.rstrip("/") + "/" + name
# infoDict = dav.getResourceInfo(childPath)
# infoDict["strModified"] = util.getRfc1123Time(infoDict["modified"])
# if infoDict["size"] or not infoDict["isCollection"]:
# infoDict["strSize"] = "%s B" % infoDict["size"]
# else:
# infoDict["strSize"] = ""
# infoDict["url"] = dav.getHref(path).rstrip("/") + "/" + urllib.quote(name)
# if infoDict["isCollection"]:
# infoDict["url"] = infoDict["url"] + "/"
#
# o_list.append("""\
# <tr><td><a href="%(url)s">%(displayName)s</a></td>
# <td>%(resourceType)s</td>
# <td>%(strSize)s</td>
# <td>%(strModified)s</td></tr>\n""" % infoDict)
o_list.append("</table>\n")
if "http_authenticator.username" in environ:
o_list.append("<p>Authenticated user: '%s', realm: '%s'.</p>" % (environ.get("http_authenticator.username"),
environ.get("http_authenticator.realm")))
if trailer:
o_list.append("%s\n" % trailer)
o_list.append("<hr/>\n<a href='http://wsgidav.googlecode.com/'>WsgiDAV server</a> - %s\n" % util.getRfc1123Time())
@ -133,4 +142,4 @@ class WsgiDavDirBrowser(object):
start_response("200 OK", [("Content-Type", "text/html"),
("Date", util.getRfc1123Time()),
])
return [ "\n".join(o_list) ] # TODO: no need to join?
return [ "\n".join(o_list) ]

View file

@ -37,7 +37,8 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
__docformat__ = 'reStructuredText'
import sys
__docformat__ = "reStructuredText"
class WsgiDAVDomainController(object):
@ -46,33 +47,51 @@ class WsgiDAVDomainController(object):
# self.allowAnonymous = allowAnonymous
def __repr__(self):
return self.__class__.__name__
def getDomainRealm(self, inputURL, environ):
"""Resolve a relative url to the appropriate realm name."""
# we don't get the realm here, its already been resolved in request_resolver
davProvider = environ["wsgidav.provider"]
if not davProvider:
if environ['wsgidav.verbose'] >= 2:
print >> environ['wsgi.errors'], "getDomainRealm(%s): '%s'" %(inputURL, None)
if environ["wsgidav.verbose"] >= 2:
print >>sys.stderr, "getDomainRealm(%s): '%s'" %(inputURL, None)
return None
# if environ['wsgidav.verbose'] >= 2:
# print >> environ['wsgi.errors'], "getDomainRealm(%s): '%s'" %(inputURL, davProvider.sharePath)
return davProvider.sharePath
realm = davProvider.sharePath
if realm == "":
realm = "/"
# if environ["wsgidav.verbose"] >= 2:
# print >>sys.stderr, "getDomainRealm(%s): '%s'" %(inputURL, realm)
return realm
def requireAuthentication(self, realmname, environ):
"""Return True if this realm requires authentication or False if it is
available for general access."""
# TODO: Should check for --allow_anonymous?
# assert realmname in environ['wsgidav.config']['user_mapping'], "Currently there must be at least on user mapping for this realm"
# assert realmname in environ["wsgidav.config"]["user_mapping"], "Currently there must be at least on user mapping for this realm"
return realmname in self.userMap
def isRealmUser(self, realmname, username, environ):
# if environ['wsgidav.verbose'] >= 2:
# print >> environ['wsgi.errors'], "isRealmUser('%s', '%s'): %s" %(realmname, username, realmname in self.userMap and username in self.userMap[realmname])
"""Returns True if this username is valid for the realm, False otherwise."""
# if environ["wsgidav.verbose"] >= 2:
# print >>sys.stderr, "isRealmUser('%s', '%s'): %s" %(realmname, username, realmname in self.userMap and username in self.userMap[realmname])
return realmname in self.userMap and username in self.userMap[realmname]
def getRealmUserPassword(self, realmname, username, environ):
"""Return the password for the given username for the realm.
Used for digest authentication.
"""
return self.userMap.get(realmname, {}).get(username, {}).get("password")
def authDomainUser(self, realmname, username, password, environ):
return password == self.getRealmUserPassword(realmname, username, environ)
"""Returns True if this username/password pair is valid for the realm,
False otherwise. Used for basic authentication."""
user = self.userMap.get(realmname, {}).get(username)
return user is not None and password == user.get("password")

View file

@ -82,7 +82,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
__docformat__ = 'reStructuredText'
__docformat__ = "reStructuredText"
import util
from dav_error import DAVError, getHttpStatusString, asDAVError,\
@ -111,10 +111,10 @@ class ErrorPrinter(object):
util.debug("sc", "ErrorPrinter: yield start")
yield v
util.debug("sc", "ErrorPrinter: yield end")
except GeneratorExit:
# TODO: required?
util.debug("sc", "GeneratorExit")
raise
# except GeneratorExit:
# # TODO: required?
# util.debug("sc", "GeneratorExit")
# raise
except DAVError, e:
util.debug("sc", "re-raising %s" % e)
raise
@ -122,7 +122,8 @@ class ErrorPrinter(object):
util.debug("sc", "re-raising 2 - %s" % e)
if self._catch_all_exceptions:
# Catch all exceptions to return as 500 Internal Error
traceback.print_exc(10, sys.stderr)
# traceback.print_exc(10, sys.stderr)
traceback.print_exc(10, environ.get("wsgi.errors") or sys.stderr)
raise asDAVError(e)
else:
util.log("ErrorPrinter: caught Exception")
@ -136,26 +137,26 @@ class ErrorPrinter(object):
if evalue == HTTP_INTERNAL_ERROR:# and e.srcexception:
print >>sys.stderr, "ErrorPrinter: caught HTTPRequestException(HTTP_INTERNAL_ERROR)"
traceback.print_exc(10, sys.stderr) # TODO: inserted this for debugging
traceback.print_exc(10, environ.get("wsgi.errors") or sys.stderr) # TODO: inserted this for debugging
print >>sys.stderr, "e.srcexception:\n%s" % e.srcexception
if evalue in ERROR_RESPONSES:
respbody = '<html><head><title>' + respcode + '</title></head><body><h1>' + respcode + '</h1>'
respbody = respbody + '<p>' + ERROR_RESPONSES[evalue] + '</p>'
respbody = "<html><head><title>" + respcode + "</title></head><body><h1>" + respcode + "</h1>"
respbody = respbody + "<p>" + ERROR_RESPONSES[evalue] + "</p>"
if e.contextinfo:
respbody += "%s\n" % e.contextinfo
respbody += '<hr>\n'
respbody += "<hr>\n"
if self._server_descriptor:
respbody = respbody + self._server_descriptor + '<hr>'
respbody = respbody + datestr + '</body></html>'
respbody = respbody + self._server_descriptor + "<hr>"
respbody = respbody + datestr + "</body></html>"
else:
# TODO: added html body, to see if this fixes 'connection closed' bug
respbody = '<html><head><title>' + respcode + '</title></head><body><h1>' + respcode + '</h1></body></html>'
respbody = "<html><head><title>" + respcode + "</title></head><body><h1>" + respcode + "</h1></body></html>"
util.debug("sc", "Return error html %s: %s" % (respcode, respbody))
start_response(respcode,
[('Content-Type', 'text/html'),
('Date', datestr)
[("Content-Type", "text/html"),
("Date", datestr)
],
# sys.exc_info() # TODO: Always provide exc_info when beginning an error response?
)

View file

@ -19,7 +19,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
__docformat__ = 'reStructuredText'
__docformat__ = "reStructuredText"
from dav_provider import DAVProvider
@ -32,6 +32,8 @@ import stat
from dav_error import DAVError, HTTP_FORBIDDEN
_logger = util.getModuleLogger(__name__)
BUFFER_SIZE = 8192
@ -41,75 +43,65 @@ BUFFER_SIZE = 8192
class ReadOnlyFilesystemProvider(DAVProvider):
def __init__(self, rootFolderPath):
if not rootFolderPath or not os.path.exists(rootFolderPath):
raise ValueError("Invalid root path: %s" % rootFolderPath)
super(ReadOnlyFilesystemProvider, self).__init__()
self.rootFolderPath = rootFolderPath
self.rootFolderPath = os.path.abspath(rootFolderPath)
def __repr__(self):
# return "%s for '%s', path '%s'" % (self.__class__.__name__, self., self.rootFolderPath)
return "%s for path '%s'" % (self.__class__.__name__, self.rootFolderPath)
def _locToFilePath(self, path):
"""Convert resource path to an absolute file path."""
"""Convert resource path to a unicode absolute file path."""
# TODO: cache results
# print "_locToFilePath(%s)..." % (path)
assert self.rootFolderPath is not None
pathInfoParts = path.strip("/").split("/")
r = os.path.join(self.rootFolderPath, *pathInfoParts)
r = os.path.abspath(r)
r = os.path.abspath(os.path.join(self.rootFolderPath, *pathInfoParts))
if not r.startswith(self.rootFolderPath):
raise RuntimeError("Security exception: tried to access file outside root.")
# if not isinstance(r, unicode):
# util.log("_locToFilePath(%s): %r" % (path, r))
# r = r.decode("utf8")
r = util.toUnicode(r)
# print "_locToFilePath(%s): %s" % (path, r)
return r
def getMemberNames(self, path):
"""Return list of (direct) collection member names (ISO-8859-1 byte strings)."""
"""Return list of (direct) collection member names (UTF-8 byte strings)."""
fp = self._locToFilePath(path)
# TODO: iso_8859_1 doesn't know EURO sign
# On Windows NT/2k/XP and Unix, if path is a Unicode object, the result
# will be a list of Unicode objects.
# Undecodable filenames will still be returned as string objects
# If we don't request unicode, for example Vista may return a '?'
# instead of a special character. The name would then be unusable to
# build a URL that references this resource.
# fp = unicode(fp)
nameList = []
for name in os.listdir(fp):
# print "%r" % name
assert isinstance(name, unicode)
name = name.encode("utf8")
# print "-> %r" % name
nameList.append(name)
# if isinstance(name, unicode):
# print "->%r" % name.encode("iso_8859_1")
# nameList.append(name.encode("iso_8859_1"))
# else:
# nameList.append(name)
return nameList
def getSupportedInfoTypes(self, path):
"""Return a list of supported information types.
See DAVProvider.getSupportedInfoTypes()
"""
infoTypes = ["created",
"contentType",
"isCollection",
"modified",
"displayName",
]
if not self.isCollection(path):
infoTypes.append("contentLength")
infoTypes.append("etag")
return infoTypes
def getInfoDict(self, path, typeList=None):
"""Return info dictionary for path.
See DAVProvider.getInfoDict()
"""
fp = self._locToFilePath(path)
# print >>sys.stderr, "getInfoDict(%s)" % (path, )
if not os.path.exists(fp):
return None
# Early out,if typeList is [] (i.e. test for resource existence only)
@ -120,7 +112,10 @@ class ReadOnlyFilesystemProvider(DAVProvider):
# The file system may have non-files (e.g. links)
isFile = os.path.isfile(fp)
name = util.getUriName(self.getPreferredPath(path))
# print "name(%s)=%s" % (path, name)
# TODO: this line in: browser doesn't work, but DAVEx does
# name = name.decode("utf8")
# util.log("getInfoDict(%s): name='%s'" % (path, name))
displayType = "File"
if isCollection:
@ -166,6 +161,9 @@ class ReadOnlyFilesystemProvider(DAVProvider):
def isResource(self, path):
# TODO: does it make sense to treat non-files/non-collections as
# existing, but neither isCollection nor isResource?
# Maybe we should define existing as 'isCollection or isResource'
fp = self._locToFilePath(path)
return os.path.isfile(fp)
@ -182,14 +180,17 @@ class ReadOnlyFilesystemProvider(DAVProvider):
raise DAVError(HTTP_FORBIDDEN)
def openResourceForRead(self, path):
def openResourceForRead(self, path, davres=None):
fp = self._locToFilePath(path)
# mime = self.getContentType(path)
mime = self.getInfo(path, "contentType")
if mime.startswith("text"):
return file(fp, 'r', BUFFER_SIZE)
if davres:
mime = davres.contentType()
else:
return file(fp, 'rb', BUFFER_SIZE)
mime = self.getInfoDict(path, ["contentType"]).get("contentType")
if mime.startswith("text"):
return file(fp, "r", BUFFER_SIZE)
else:
return file(fp, "rb", BUFFER_SIZE)
def openResourceForWrite(self, path, contenttype=None):
@ -230,14 +231,19 @@ class FilesystemProvider(ReadOnlyFilesystemProvider):
def openResourceForWrite(self, path, contenttype=None):
fp = self._locToFilePath(path)
if contenttype is None:
istext = False
else:
istext = contenttype.startswith("text")
if istext:
return file(fp, 'w', BUFFER_SIZE)
else:
return file(fp, 'wb', BUFFER_SIZE)
# if contenttype is None:
# istext = False
# else:
# istext = contenttype.startswith("text")
# if istext:
# return file(fp, "w", BUFFER_SIZE)
# else:
# return file(fp, "wb", BUFFER_SIZE)
mode = "wb"
if contenttype and contenttype.startswith("text"):
mode = "w"
_logger.debug("openResourceForWrite: %s, %s" % (fp, mode))
return file(fp, mode, BUFFER_SIZE)
def deleteResource(self, path):
fp = self._locToFilePath(path)

View file

@ -3,11 +3,13 @@ http_authenticator
==================
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
:Author: Martin Wendt, moogle(at)wwwendt.de
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
WSGI middleware for HTTP basic and digest authentication.
Usage::
from http_authenticator import HTTPAuthenticator
WSGIApp = HTTPAuthenticator(ProtectedWSGIApp, domain_controller, acceptbasic,
@ -33,8 +35,8 @@ Usage::
The HTTPAuthenticator will put the following authenticated information in the
environ dictionary::
environ['http_authenticator.realm'] = realm name
environ['http_authenticator.username'] = username
environ["http_authenticator.realm"] = realm name
environ["http_authenticator.username"] = username
Domain Controllers
@ -60,6 +62,7 @@ in a single realm name (for display) and a single dictionary of username (key)
and password (value) string pairs
Usage::
from http_authenticator import SimpleDomainController
users = dict(({'John Smith': 'YouNeverGuessMe', 'Dan Brown': 'DontGuessMeEither'})
realm = 'Sample Realm'
@ -79,19 +82,26 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
__docformat__ = 'reStructuredText'
__docformat__ = "reStructuredText"
import random
import base64
import md5
try:
from hashlib import md5
except ImportError:
from md5 import md5
import time
import re
import util
_logger = util.getModuleLogger(__name__, True)
class SimpleDomainController(object):
"""SimpleDomainController : Simple domain controller for HTTPAuthenticator."""
def __init__(self, dictusers = None, realmname = 'SimpleDomain'):
def __init__(self, dictusers = None, realmname = "SimpleDomain"):
if dictusers is None:
self._users = dict({'John Smith': 'YouNeverGuessMe'})
self._users = dict({"John Smith": "YouNeverGuessMe"})
else:
self._users = dictusers
self._realmname = realmname
@ -108,16 +118,17 @@ class SimpleDomainController(object):
def getRealmUserPassword(self, realmname, username, environ):
if username in self._users:
return self._users[username]
else:
return None
return None
def authRealmUser(self, realmname, username, password, environ):
def authDomainUser(self, realmname, username, password, environ):
if username in self._users:
return self._users[username] == password
else:
return False
return False
#===============================================================================
# HTTPAuthenticator
#===============================================================================
class HTTPAuthenticator(object):
"""WSGI Middleware for basic and digest authenticator."""
def __init__(self, application, domaincontroller, acceptbasic=True, acceptdigest=True, defaultdigest=True):
@ -131,86 +142,100 @@ class HTTPAuthenticator(object):
self._acceptbasic = acceptbasic
self._acceptdigest = acceptdigest
self._defaultdigest = defaultdigest
def __call__(self, environ, start_response):
realmname = self._domaincontroller.getDomainRealm(environ['PATH_INFO'], environ)
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"], environ)
if environ.get("REQUEST_METHOD") == "PUT":
pass
if not self._domaincontroller.requireAuthentication(realmname, environ):
# no authentication needed
if environ['wsgidav.verbose'] >= 2:
print "No authorization required for realm '%s'" % realmname
environ['http_authenticator.realm'] = realmname
environ['http_authenticator.username'] = ''
_logger.debug("No authorization required for realm '%s'" % realmname)
environ["http_authenticator.realm"] = realmname
environ["http_authenticator.username"] = ""
return self._application(environ, start_response)
if 'HTTP_AUTHORIZATION' in environ:
authheader = environ['HTTP_AUTHORIZATION']
if "HTTP_AUTHORIZATION" in environ:
authheader = environ["HTTP_AUTHORIZATION"]
authmatch = self._headermethod.search(authheader)
authmethod = "None"
if authmatch:
authmethod = authmatch.group(1).lower()
if authmethod == 'digest' and self._acceptdigest:
if authmethod == "digest" and self._acceptdigest:
return self.authDigestAuthRequest(environ, start_response)
elif authmethod == 'basic' and self._acceptbasic:
return self.authBasicAuthRequest(environ, start_response)
else:
start_response("400 Bad Request", [('Content-Length', '0')])
return ['']
else:
if self._defaultdigest:
return self.sendDigestAuthResponse(environ, start_response)
else:
elif authmethod == "digest" and self._acceptbasic:
return self.sendBasicAuthResponse(environ, start_response)
elif authmethod == "basic" and self._acceptbasic:
return self.authBasicAuthRequest(environ, start_response)
util.log("HTTPAuthenticator: respond with 400 Bad request; Auth-Method: %s" % authmethod)
start_response("400 Bad Request", [("Content-Length", "0"),
("Date", util.getRfc1123Time()),
])
return [""]
if self._defaultdigest:
return self.sendDigestAuthResponse(environ, start_response)
return self.sendBasicAuthResponse(environ, start_response)
return ['']
def sendBasicAuthResponse(self, environ, start_response):
realmname = self._domaincontroller.getDomainRealm(environ['PATH_INFO'] , environ)
if environ['wsgidav.verbose'] >= 2:
print >> environ['wsgi.errors'], "401 Not Authorized for realm '%s'" % realmname
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
_logger.info("401 Not Authorized for realm '%s' (basic)" % realmname)
wwwauthheaders = "Basic realm=\"" + realmname + "\""
start_response("401 Not Authorized", [('WWW-Authenticate', wwwauthheaders)])
return [self.getErrorMessage()]
start_response("401 Not Authorized", [("WWW-Authenticate", wwwauthheaders),
("Content-Type", "text/html"),
("Date", util.getRfc1123Time()),
])
return [ self.getErrorMessage() ]
def authBasicAuthRequest(self, environ, start_response):
realmname = self._domaincontroller.getDomainRealm(environ['PATH_INFO'] , environ)
authheader = environ['HTTP_AUTHORIZATION']
authvalue = ''
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
authheader = environ["HTTP_AUTHORIZATION"]
authvalue = ""
try:
authvalue = authheader[len("Basic "):]
except:
authvalue = ''
authvalue = authvalue.strip().decode('base64')
username, password = authvalue.split(':',1)
authvalue = ""
authvalue = authvalue.strip().decode("base64")
username, password = authvalue.split(":",1)
if self._domaincontroller.authRealmUser(realmname, username, password, environ):
environ['http_authenticator.realm'] = realmname
environ['http_authenticator.username'] = username
if self._domaincontroller.authDomainUser(realmname, username, password, environ):
environ["http_authenticator.realm"] = realmname
environ["http_authenticator.username"] = username
return self._application(environ, start_response)
else:
return self.sendBasicAuthResponse(environ, start_response)
return self.sendBasicAuthResponse(environ, start_response)
def sendDigestAuthResponse(self, environ, start_response):
realmname = self._domaincontroller.getDomainRealm(environ['PATH_INFO'] , environ)
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
random.seed()
serverkey = hex(random.getrandbits(32))[2:]
etagkey = md5.new(environ['PATH_INFO']).hexdigest()
etagkey = md5(environ["PATH_INFO"]).hexdigest()
timekey = str(time.time())
nonce = base64.b64encode(timekey + md5.new(timekey + ":" + etagkey + ":" + serverkey).hexdigest())
nonce = base64.b64encode(timekey + md5(timekey + ":" + etagkey + ":" + serverkey).hexdigest())
wwwauthheaders = "Digest realm=\"" + realmname + "\", nonce=\"" + nonce + \
"\", algorithm=\"MD5\", qop=\"auth\""
if environ['wsgidav.verbose'] >= 2:
print >> environ['wsgi.errors'], "401 Not Authorized for realm '%s': %s" % (realmname, wwwauthheaders)
start_response("401 Not Authorized", [('WWW-Authenticate', wwwauthheaders)])
return [self.getErrorMessage()]
_logger.info("401 Not Authorized for realm '%s' (digest): %s" % (realmname, wwwauthheaders))
start_response("401 Not Authorized", [("WWW-Authenticate", wwwauthheaders),
("Content-Type", "text/html"),
("Date", util.getRfc1123Time()),
])
return [ self.getErrorMessage() ]
def authDigestAuthRequest(self, environ, start_response):
realmname = self._domaincontroller.getDomainRealm(environ['PATH_INFO'] , environ)
realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"] , environ)
isinvalidreq = False
authheaderdict = dict([])
authheaders = environ['HTTP_AUTHORIZATION'] + ','
authheaders = environ["HTTP_AUTHORIZATION"] + ","
if not authheaders.lower().strip().startswith("digest"):
isinvalidreq = True
authheaderlist = self._headerparser.findall(authheaders)
@ -218,9 +243,11 @@ class HTTPAuthenticator(object):
authheaderkey = authheader[0]
authheadervalue = authheader[1].strip().strip("\"")
authheaderdict[authheaderkey] = authheadervalue
_logger.info("authDigestAuthRequest: %s" % authheaderdict)
if 'username' in authheaderdict:
req_username = authheaderdict['username']
if "username" in authheaderdict:
req_username = authheaderdict["username"]
req_username_org = req_username
# Fix for Windows XP:
# net use W: http://127.0.0.1/dav /USER:DOMAIN\tester tester
@ -228,87 +255,85 @@ class HTTPAuthenticator(object):
# but send the digest for the simple name ('DOMAIN\tester').
if r"\\" in req_username:
req_username = req_username.replace("\\\\", "\\")
if environ['wsgidav.verbose'] >= 2:
print >> environ['wsgi.errors'], "Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username)
_logger.info("Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username))
if not self._domaincontroller.isRealmUser(realmname, req_username, environ):
isinvalidreq = True
else:
isinvalidreq = True
# TODO: ??
# TODO: Chun added this comments
# Do not do realm checking - a hotfix for WinXP using some other realm's
# auth details for this realm - if user/password match
#if 'realm' in authheaderdict:
# if authheaderdict['realm'].upper() != realmname.upper():
# if authheaderdict["realm"].upper() != realmname.upper():
# isinvalidreq = True
if 'algorithm' in authheaderdict:
if authheaderdict['algorithm'].upper() != "MD5":
if "algorithm" in authheaderdict:
if authheaderdict["algorithm"].upper() != "MD5":
isinvalidreq = True # only MD5 supported
if 'uri' in authheaderdict:
req_uri = authheaderdict['uri']
if "uri" in authheaderdict:
req_uri = authheaderdict["uri"]
if 'nonce' in authheaderdict:
req_nonce = authheaderdict['nonce']
if "nonce" in authheaderdict:
req_nonce = authheaderdict["nonce"]
else:
isinvalidreq = True
req_hasqop = False
if 'qop' in authheaderdict:
if "qop" in authheaderdict:
req_hasqop = True
req_qop = authheaderdict['qop']
req_qop = authheaderdict["qop"]
if req_qop.lower() != "auth":
isinvalidreq = True # only auth supported, auth-int not supported
else:
req_qop = None
if 'cnonce' in authheaderdict:
req_cnonce = authheaderdict['cnonce']
if "cnonce" in authheaderdict:
req_cnonce = authheaderdict["cnonce"]
else:
req_cnonce = None
if req_hasqop:
isinvalidreq = True
if 'nc' in authheaderdict: # is read but nonce-count checking not implemented
req_nc = authheaderdict['nc']
if "nc" in authheaderdict: # is read but nonce-count checking not implemented
req_nc = authheaderdict["nc"]
else:
req_nc = None
if req_hasqop:
isinvalidreq = True
if 'response' in authheaderdict:
req_response = authheaderdict['response']
if "response" in authheaderdict:
req_response = authheaderdict["response"]
else:
isinvalidreq = True
if not isinvalidreq:
req_password = self._domaincontroller.getRealmUserPassword(realmname, req_username, environ)
req_method = environ['REQUEST_METHOD']
req_method = environ["REQUEST_METHOD"]
required_digest = self.computeDigestResponse(req_username, realmname, req_password, req_method, req_uri, req_nonce, req_cnonce, req_qop, req_nc)
if required_digest != req_response:
if environ['wsgidav.verbose'] >= 2:
print >> environ['wsgi.errors'], "computeDigestResponse('%s', '%s', ...): %s != %s" % (realmname, req_username, required_digest, req_response)
_logger.warning("computeDigestResponse('%s', '%s', ...): %s != %s" % (realmname, req_username, required_digest, req_response))
isinvalidreq = True
else:
# if environ['wsgidav.verbose'] >= 2:
# print >> environ['wsgi.errors'], "digest suceeded for realm '%s', user '%s'" % (realmname, req_username)
# _logger.debug("digest succeeded for realm '%s', user '%s'" % (realmname, req_username))
pass
if isinvalidreq:
if environ['wsgidav.verbose'] >= 2:
print >> environ['wsgi.errors'], "Authentication failed for user '%s', realm '%s'" % (req_username, realmname)
_logger.warning("Authentication failed for user '%s', realm '%s'" % (req_username, realmname))
return self.sendDigestAuthResponse(environ, start_response)
environ['http_authenticator.realm'] = realmname
environ['http_authenticator.username'] = req_username
environ["http_authenticator.realm"] = realmname
environ["http_authenticator.username"] = req_username
return self._application(environ, start_response)
return self.sendDigestAuthResponse(environ, start_response)
def computeDigestResponse(self, username, realm, password, method, uri, nonce, cnonce, qop, nc):
A1 = username + ":" + realm + ":" + password
A2 = method + ":" + uri
@ -318,12 +343,15 @@ class HTTPAuthenticator(object):
digestresp = self.md5kd( self.md5h(A1), nonce + ":" + self.md5h(A2))
return digestresp
def md5h(self, data):
return md5.new(data).hexdigest()
return md5(data).hexdigest()
def md5kd(self, secret, data):
return self.md5h(secret + ':' + data)
return self.md5h(secret + ":" + data)
def getErrorMessage(self):
message = """\
<html><head><title>401 Access not authorized</title></head>
@ -332,4 +360,4 @@ class HTTPAuthenticator(object):
</body>
</html>
"""
return message
return message

View file

@ -1,6 +1,19 @@
class IDAVProvider(object):
"""
TODO: not sure, if we really need interfaces if we have an abstract base class.
For now, see wsgidav.DAVProvider.
+----------------------------------------------------------------------+
| TODO: document this interface |
| For now, see wsgidav.DAVProvider instead. |
+----------------------------------------------------------------------+
This class is an interface for a WebDAV provider.
Implementations in WsgiDAV include::
wsgidav.DAVProvider (abstract base class)
wsgidav.fs_dav_provider.ReadOnlyFilesystemProvider
wsgidav.fs_dav_provider.FilesystemProvider
wsgidav.addons.mysql_dav_provider.MySQLBrowserProvider
wsgidav.addons.VirtualResourceProvider
All methods must be implemented.
"""

View file

@ -1,64 +1,36 @@
class IDomainController(object):
"""
+-------------------------------------------------------------------------------+
| The following documentation was taken over from PyFileServer and is outdated! |
+-------------------------------------------------------------------------------+
This class is an interface for a PropertyManager. Implementations for the
property manager in WsgiDAV include::
"""
+----------------------------------------------------------------------+
| TODO: document this interface |
| For now, see wsgidav.domain_controller instead |
+----------------------------------------------------------------------+
This class is an interface for a domain controller.
Implementations in WsgiDAV include::
wsgidav.domain_controller.WsgiDAVDomainController
wsgidav.addons.windowsdomaincontroller.SimpleWindowsDomainController
wsgidav.domain_controller.WsgiDAVDomainController
wsgidav.addons.nt_domain_controller.NTDomainController
All methods must be implemented.
All methods must be implemented.
The environ variable here is the WSGI 'environ' dictionary. It is passed to
all methods of the domain controller as a means for developers to pass information
from previous middleware or server config (if required).
"""
The environ variable here is the WSGI 'environ' dictionary. It is passed to
all methods of the domain controller as a means for developers to pass information
from previous middleware or server config (if required).
"""
Domain Controllers
------------------
Domain Controllers
------------------
The HTTP basic and digest authentication schemes are based on the following
concept:
The HTTP basic and digest authentication schemes are based on the following
concept:
Each requested relative URI can be resolved to a realm for authentication,
for example:
/fac_eng/courses/ee5903/timetable.pdf -> might resolve to realm 'Engineering General'
/fac_eng/examsolns/ee5903/thisyearssolns.pdf -> might resolve to realm 'Engineering Lecturers'
/med_sci/courses/m500/surgery.htm -> might resolve to realm 'Medical Sciences General'
and each realm would have a set of username and password pairs that would
allow access to the resource.
Each requested relative URI can be resolved to a realm for authentication,
for example:
/fac_eng/courses/ee5903/timetable.pdf -> might resolve to realm 'Engineering General'
/fac_eng/examsolns/ee5903/thisyearssolns.pdf -> might resolve to realm 'Engineering Lecturers'
/med_sci/courses/m500/surgery.htm -> might resolve to realm 'Medical Sciences General'
and each realm would have a set of username and password pairs that would
allow access to the resource.
A domain controller provides this information to the HTTPAuthenticator.
"""
def getDomainRealm(self, inputURL, environ):
"""
resolves a relative url to the appropriate realm name
"""
def requireAuthentication(self, realmname, environ):
"""
returns True if this realm requires authentication
or False if it is available for general access
"""
def isRealmUser(self, realmname, username, environ):
"""
returns True if this username is valid for the realm, False otherwise
"""
def getRealmUserPassword(self, realmname, username, environ):
"""
returns the password for the given username for the realm.
Used for digest authentication.
"""
def authDomainUser(self, realmname, username, password, environ):
"""
returns True if this username/password pair is valid for the realm,
False otherwise. Used for basic authentication.
"""
A domain controller provides this information to the HTTPAuthenticator.
"""

View file

@ -1,97 +1,19 @@
class LockManagerInterface(object):
"""
+-------------------------------------------------------------------------------+
| The following documentation was taken over from PyFileServer and is outdated! |
| See wsgidav.lock_manager instead |
+-------------------------------------------------------------------------------+
This class is an interface for a LockManager. Implementations for the lock manager
in WsgiDAV include::
"""
+----------------------------------------------------------------------+
| TODO: document this interface |
| For now, see wsgidav.lock_manager instead |
+----------------------------------------------------------------------+
This class is an interface for a LockManager.
Implementations for the lock manager in WsgiDAV include::
wsgidav.lock_manager.LockManager
wsgidav.lock_manager.LockManager
wsgidav.lock_manager.ShelveLockManager
All methods must be implemented.
All methods must be implemented.
The url variable in methods refers to the relative URL of a resource. e.g. the
resource http://server/share1/dir1/dir2/file3.txt would have a url of
'/share1/dir1/dir2/file3.txt'
"""
def generateLock(self, username, locktype, lockscope, lockdepth, lockowner, lockheadurl, timeout):
"""
returns a new locktoken for the following lock:
username - username of user performing the lock
locktype - only the locktype "write" is defined in the webdav specification
lockscope - "shared" or "exclusive"
lockdepth - depth of lock. "0" or "infinity"
lockowner - a arbitrary field provided by the client at lock time
lockheadurl - the url the lock is being performed on
timeout - -1 for infinite, positive value for number of seconds.
Could be None, fall back to a default.
"""
def deleteLock(self, locktoken):
"""
deletes a lock specified by locktoken
"""
def isTokenLockedByUser(self, locktoken, username):
"""
returns True if locktoken corresponds to a lock locked by username
"""
def isUrlLocked(self, url):
"""
returns True if the resource at url is locked
"""
def getUrlLockScope(self, url):
"""
returns the lockscope of all locks on url. 'shared' or 'exclusive'
"""
def getLockProperty(self, locktoken, lockproperty):
"""
returns the value for the following properties for the lock specified by
locktoken:
'LOCKUSER', 'LOCKTYPE', 'LOCKSCOPE', 'LOCKDEPTH', 'LOCKOWNER', 'LOCKHEADURL'
and
'LOCKTIME' - number of seconds left on the lock.
"""
def isUrlLockedByToken(self, url, locktoken):
"""
returns True if the resource at url is locked by lock specified by locktoken
"""
def getTokenListForUrl(self, url):
"""
returns a list of locktokens corresponding to locks on url.
"""
def getTokenListForUrlByUser(self, url, username):
"""
returns a list of locktokens corresponding to locks on url by user username.
"""
def addUrlToLock(self, url, locktoken):
"""
adds url to be locked by lock specified by locktoken.
more than one url can be locked by a lock - depth infinity locks.
"""
def removeAllLocksFromUrl(self, url):
"""
removes all locks from a url.
This usually happens when the resource specified by url is being deleted.
"""
def refreshLock(self, locktoken, timeout):
"""
refreshes the lock specified by locktoken.
timeout : -1 for infinite, positive value for number of seconds.
Could be None, fall back to a default.
"""
The url variable in methods refers to the relative URL of a resource. e.g. the
resource http://server/share1/dir1/dir2/file3.txt would have a url of
'/share1/dir1/dir2/file3.txt'
"""

View file

@ -1,27 +1,32 @@
class PropertyManagerInterface(object):
"""
+----------------------------------------------------------------------+
| TODO: document this interface |
| For now, see wsgidav.lock_manager instead |
+----------------------------------------------------------------------+
"""
+-------------------------------------------------------------------------------+
| The following documentation was taken over from PyFileServer and is outdated! |
| See wsgidav.property_manager instead |
+-------------------------------------------------------------------------------+
This class is an interface for a PropertyManager. Implementations for the
property manager in WsgiDAV include::
This class is an interface for a PropertyManager.
Implementations of a property manager in WsgiDAV include::
wsgidav.property_manager.PropertyManager
<wsgidav.property_manager.PropertyManager>_
wsgidav.property_manager.ShelvePropertyManager
All methods must be implemented.
All methods must be implemented.
The url variables in methods refers to the relative URL of a resource. e.g. the
resource http://server/share1/dir1/dir2/file3.txt would have a url of
'/share1/dir1/dir2/file3.txt'
The url variable in methods refers to the relative URL of a resource. e.g. the
resource http://server/share1/dir1/dir2/file3.txt would have a url of
'/share1/dir1/dir2/file3.txt'
All methods must be implemented.
The url variables in methods refers to the relative URL of a resource. e.g. the
resource http://server/share1/dir1/dir2/file3.txt would have a url of
'/share1/dir1/dir2/file3.txt'
"""
"""
Properties and WsgiDAV
---------------------------
----------------------
Properties of a resource refers to the attributes of the resource. A property
is referenced by the property name and the property namespace. We usually
refer to the property as ``{property namespace}property name``
@ -63,40 +68,3 @@ class PropertyManagerInterface(object):
``wsgidav.property_manager``
"""
def getProperties(self, normurl):
"""
return a list of properties for url specified by normurl
return list is a list of tuples (a, b) where a is the property namespace
and b the property name
"""
def getProperty(self, normurl, propname, propns):
"""
return the value of the property for url specified by normurl where
propertyname is propname and property namespace is propns
"""
def writeProperty(self, normurl, propname, propns, propertyvalue):
"""
write propertyvalue as value of the property for url specified by
normurl where propertyname is propname and property namespace is propns
"""
def removeProperty(self, normurl, propname, propns):
"""
delete the property for url specified by normurl where
propertyname is propname and property namespace is propns
"""
def removeProperties(self, normurl):
"""
delete all properties from url specified by normurl
"""
def copyProperties(self, origurl, desturl):
"""
copy all properties from url specified by origurl to url specified by desturl
"""

View file

@ -8,14 +8,9 @@ lock_manager
:Author: Martin Wendt, moogle(at)wwwendt.de
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
A low performance lock manager implementation using shelve.
Implements two lock managers: one in-memory (dict-based), and one persistent low
performance variant using shelve.
This module consists of a number of miscellaneous functions for the locks
features of WebDAV.
It also includes an implementation of a LockManager for
storage of locks. This implementation use
shelve for file storage. See request_server.py for details.
LockManagers must provide the methods as described in
lockmanagerinterface_
@ -29,15 +24,18 @@ from pprint import pprint
from dav_error import DAVError, \
HTTP_LOCKED, PRECONDITION_CODE_LockConflict, HTTP_FORBIDDEN,\
HTTP_PRECONDITION_FAILED
import os
import sys
import util
import shelve
import threading
import random
import re
import time
from rw_lock import ReadWriteLock
__docformat__ = 'reStructuredText'
__docformat__ = "reStructuredText"
_logger = util.getModuleLogger(__name__)
# TODO: comment's from Ian Bicking (2005)
#@@: Use of shelve means this is only really useful in a threaded environment.
@ -54,59 +52,64 @@ __docformat__ = 'reStructuredText'
class LockManager(object):
"""
A low performance lock manager implementation using shelve.
An in-memory lock manager implementation using a dictionary.
This is obviously not persistent, but should be enough in some cases.
For a persistent implementation, see lock_manager.ShelveLockManager().
"""
LOCK_TIME_OUT_DEFAULT = 604800 # 1 week, in seconds
# any numofsecs above the following limit is regarded as infinite
MAX_FINITE_TIMEOUT_LIMIT = 10*365*24*60*60 #approx 10 years
def __init__(self, persiststore):
def __init__(self):
self._loaded = False
self._dict = None
self._init_lock = threading.RLock()
self._write_lock = threading.RLock()
self._persiststorepath = persiststore
self._lock = ReadWriteLock()
self._verbose = 2
def _performInitialization(self):
self._init_lock.acquire(True)
try:
if self._loaded: # test again within the critical section
# self._lock.release()
return True
self._dict = shelve.open(self._persiststorepath)
self._loaded = True
self._cleanup()
if self._verbose >= 2:
self._dump("After shelve.open()")
finally:
self._init_lock.release()
def __repr__(self):
return repr(self._dict)
return "LockManager"
def __del__(self):
if self._loaded:
self._dict.close()
# if self._loaded and hasattr(self._dict, "close"):
self._close()
def _lazyOpen(self):
_logger.debug("_lazyOpen()")
self._lock.acquireWrite()
try:
self._dict = {}
self._loaded = True
finally:
self._lock.release()
def _sync(self):
pass
def _close(self):
_logger.debug("_close()")
self._lock.acquireWrite()
try:
self._dict = None
self._loaded = False
finally:
self._lock.release()
def _cleanup(self):
"""TODO: Purge expired locks."""
pass
def _log(self, msg):
if self._verbose >= 2:
util.log(msg)
def _dump(self, msg="", out=None):
if not self._loaded:
self._performInitialization()
self._lazyOpen()
if not out and self._verbose >= 2:
return # Already dumped on init
if out is None:
@ -120,7 +123,7 @@ class LockManager(object):
def _splitToken(key):
return key.split(":", 1)[1]
print >>out, "LockManager(%s): %s" % (self._persiststorepath, msg)
print >>out, "%s: %s" % (self, msg)
for k, v in self._dict.items():
if k.startswith("URL2TOKEN:"):
@ -155,15 +158,32 @@ class LockManager(object):
pprint(ownerDict, indent=4, width=255, stream=out)
def generateLock(self, username, locktype, lockscope, lockdepth, lockowner, lockroot, timeout):
def _generateLock(self, username, locktype, lockscope, lockdepth, lockowner, lockroot, timeout):
"""Acquire lock and return lockDict.
username
Name of the principal.
locktype
Must be 'write'.
lockscope
Must be 'shared' or 'exclusive'.
lockdepth
Must be '0' or 'infinity'.
lockowner
String identifying the owner.
lockroot
Resource URL.
timeout
Seconds to live
This function does NOT check, if the new lock creates a conflict!
"""
# self._log("generateLock(%s, %s, %s, %s, %s, %s, %s)" % (username, lockscope, lockdepth, lockowner, lockroot, timeout))
assert locktype == "write"
assert lockscope in ("shared", "exclusive")
assert lockdepth in ("0", "infinity")
assert isinstance(lockowner, str)
assert isinstance(lockroot, str)
# assert not lockroot.endswith("/")
if timeout is None:
timeout = LockManager.LOCK_TIME_OUT_DEFAULT
@ -173,8 +193,6 @@ class LockManager(object):
timeout = time.time() + timeout
randtoken = "opaquelocktoken:" + str(hex(random.getrandbits(256)))
while randtoken in self._dict:
randtoken = "opaquelocktoken:" + str(hex(random.getrandbits(256)))
lockDict = {"root": lockroot,
"type": locktype,
@ -185,12 +203,12 @@ class LockManager(object):
"principal": username,
"token": randtoken,
}
self._log("generateLock %s" % _lockString(lockDict))
_logger.debug("_generateLock %s" % _lockString(lockDict))
self._write_lock.acquire(True)
self._lock.acquireWrite()
try:
if not self._loaded:
self._performInitialization()
self._lazyOpen()
#
self._dict[randtoken] = lockDict
@ -203,50 +221,49 @@ class LockManager(object):
tokList.append(randtoken)
self._dict[key] = tokList
self._dict.sync()
# if self._verbose:
# self._dump("After generateLock(%s)" % lockroot)
self._sync()
# if self._verbose >= 2:
# self._dump("After _generateLock(%s)" % lockroot)
return lockDict
finally:
self._write_lock.release()
self._lock.release()
def getCheckedLock(self, resourceAL,
lockroot, locktype, lockscope, lockdepth, lockowner, timeout,
user, tokenList):
def acquire(self, lockroot, locktype, lockscope, lockdepth,
lockowner, timeout, user, tokenList):
"""Check for permissions and acquire a lock.
On success, return length-1 list: [ {newLockDict, None} ]
On success, return a one-element list with a tuple: [ (newLockDict, None) ]
On error return a list of conflicts (@see self.checkLockPermission)
"""
self._write_lock.acquire(True)
self._lock.acquireWrite()
try:
if not self._loaded:
self._performInitialization()
conflictList = self.checkLockPermission(resourceAL, lockroot, locktype, lockscope, lockdepth, tokenList, user)
self._lazyOpen()
conflictList = self.checkLockPermission(lockroot, locktype, lockscope, lockdepth, tokenList, user)
if len(conflictList) > 0:
return conflictList
lockDict = self.generateLock(user, locktype, lockscope, lockdepth, lockowner, lockroot, timeout)
lockDict = self._generateLock(user, locktype, lockscope, lockdepth, lockowner, lockroot, timeout)
return [ (lockDict, None) ]
finally:
self._write_lock.release()
self._lock.release()
def refreshLock(self, locktoken, timeout=None):
def refresh(self, locktoken, timeout=None):
"""Set new timeout for lock, if existing and valid."""
if timeout is None:
timeout = LockManager.LOCK_TIME_OUT_DEFAULT
self._write_lock.acquire(True)
self._lock.acquireWrite()
try:
lock = self.getLock(locktoken)
self._log("refreshLock %s" % _lockString(lock))
_logger.debug("refresh %s" % _lockString(lock))
if lock:
lock["timeout"] = time.time() + timeout
self._dict[locktoken] = lock
self._dict.sync()
self._sync()
return lock
finally:
self._write_lock.release()
self._lock.release()
def getLock(self, locktoken, key=None):
@ -256,36 +273,40 @@ class LockManager(object):
Side effect: if lock is expired, it will be purged and None is returned.
"""
assert key in (None, "type", "scope", "depth", "owner", "root", "timeout", "principal", "token")
if not self._loaded:
self._performInitialization()
lock = self._dict.get(locktoken)
if lock is None: # Lock not found: purge dangling URL2TOKEN entries
self.deleteLock(locktoken)
return None
timeout = lock["timeout"]
if timeout >= 0 and timeout < time.time():
self.deleteLock(locktoken)
return None
if key is None:
return lock
else:
return lock[key]
def deleteLock(self, locktoken):
"""Delete lock and url2token mapping."""
self._write_lock.acquire(True)
self._lock.acquireRead()
try:
if not self._loaded:
self._performInitialization()
self._lazyOpen()
lock = self._dict.get(locktoken)
self._log("deleteLock %s" % _lockString(lock))
if lock is None: # Lock not found: purge dangling URL2TOKEN entries
self.release(locktoken)
return None
timeout = lock["timeout"]
if timeout >= 0 and timeout < time.time():
self.release(locktoken)
return None
if key is None:
return lock
else:
return lock[key]
finally:
self._lock.release()
def release(self, locktoken):
"""Delete lock and url2token mapping."""
self._lock.acquireWrite()
try:
if not self._loaded:
self._lazyOpen()
lock = self._dict.get(locktoken)
_logger.debug("release %s" % _lockString(lock))
if lock is None:
return False
# Remove url to lock mapping
key = "URL2TOKEN:%s" % lock.get("root")
if key in self._dict:
# self._log(" delete token %s from url %s" % (locktoken, lock.get("root")))
# _logger.debug(" delete token %s from url %s" % (locktoken, lock.get("root")))
tokList = self._dict[key]
if len(tokList) > 1:
# Note: shelve dictionary returns copies, so we must reassign values:
@ -296,11 +317,11 @@ class LockManager(object):
# Remove the lock
del self._dict[locktoken]
# if self._verbose:
# self._dump("After deleteLock(%s)" % locktoken)
self._dict.sync()
# if self._verbose >= 2:
# self._dump("After release(%s)" % locktoken)
self._sync()
finally:
self._write_lock.release()
self._lock.release()
def isTokenLockedByUser(self, token, username):
@ -312,16 +333,21 @@ class LockManager(object):
"""Return list of lockDict, if <url> is protected by at least one direct, valid lock.
Side effect: expired locks for this url are purged.
"""
if not self._loaded:
self._performInitialization()
key = "URL2TOKEN:%s" % url
lockList = []
for tok in self._dict.get(key, []):
lock = self.getLock(tok)
if lock and (username is None or username == lock["principal"]):
lockList.append(lock)
return lockList
"""
# assert url and not url.endswith("/")
self._lock.acquireRead()
try:
if not self._loaded:
self._lazyOpen()
key = "URL2TOKEN:%s" % url
lockList = []
for tok in self._dict.get(key, []):
lock = self.getLock(tok)
if lock and (username is None or username == lock["principal"]):
lockList.append(lock)
return lockList
finally:
self._lock.release()
def getIndirectUrlLockList(self, url, username=None):
@ -330,22 +356,26 @@ class LockManager(object):
If a username is given, only locks owned by this principal are returned.
Side effect: expired locks for this url and all parents are purged.
"""
lockList = []
u = url
while u:
# TODO: check, if expired
ll = self.getUrlLockList(u)
for l in ll:
if u != url and l["depth"] != "infinity":
continue # We only consider parents with Depth: inifinity
# TODO: handle shared locks in some way?
# if l["scope"] == "shared" and lockscope == "shared" and username != l["principal"]:
# continue # Only compatible with shared locks by other users
if username == l["principal"]:
lockList.append(l)
u = util.getUriParent(u)
# self._log("getIndirectUrlLockList(%s, %s): %s" % (url, username, lockList))
return lockList
self._lock.acquireRead()
try:
lockList = []
u = url
while u:
# TODO: check, if expired
ll = self.getUrlLockList(u)
for l in ll:
if u != url and l["depth"] != "infinity":
continue # We only consider parents with Depth: infinity
# TODO: handle shared locks in some way?
# if l["scope"] == "shared" and lockscope == "shared" and username != l["principal"]:
# continue # Only compatible with shared locks by other users
if username == l["principal"]:
lockList.append(l)
u = util.getUriParent(u)
# _logger.debug("getIndirectUrlLockList(%s, %s): %s" % (url, username, lockList))
return lockList
finally:
self._lock.release()
def isUrlLocked(self, url):
@ -354,32 +384,31 @@ class LockManager(object):
return len(lockList) > 0
def getUrlLockScope(self, url):
lockList = self.getUrlLockList(url)
# either one exclusive lock, or many shared locks - first lock will give lock scope
if len(lockList) > 0:
return lockList[0].get("scope")
return None
# def getUrlLockScope(self, url):
# lockList = self.getUrlLockList(url)
# # either one exclusive lock, or many shared locks - first lock will give lock scope
# if len(lockList) > 0:
# return lockList[0].get("scope")
# return None
def isUrlLockedByToken(self, url, locktoken):
"""Check, if url is directly or indirectly locked by locktoken."""
"""Check, if url (or any of it's parents) is locked by locktoken."""
lockUrl = self.getLock(locktoken, "root")
# FIXME: make sure '/a/b' doesn't match '/a/bb'
return ( lockUrl and url.startswith(lockUrl) )
return lockUrl and util.isEqualOrChildUri(lockUrl, url)
def removeAllLocksFromUrl(self, url):
self._write_lock.acquire(True)
self._lock.acquireWrite()
try:
lockList = self.getUrlLockList(url)
for lock in lockList:
self.deleteLock(lock["token"])
self.release(lock["token"])
finally:
self._write_lock.release()
self._lock.release()
def checkLockPermission(self, resourceAL, lockroot, locktype, lockscope, lockdepth, tokenList, user):
def checkLockPermission(self, lockroot, locktype, lockscope, lockdepth, tokenList, user):
"""Check, if <user> can lock <lockroot>, otherwise return a list of conflicting locks.
An empty list is returned, if the lock would be granted.
@ -394,6 +423,7 @@ class LockManager(object):
@see http://www.webdav.org/specs/rfc4918.html#lock-model
TODO: verify assumptions:
- Parent locks WILL NOT be conflicting, if they are depth-0.
- Exclusive depth-infinity parent locks WILL be conflicting, even if they
are owned by <user>.
@ -403,7 +433,7 @@ class LockManager(object):
different applications on his client.)
- TODO: Can <user> lock-exclusive, if she holds a parent shared-lock?
(currently NO; it would only make sense, if he was the only shared-lock holder)
- TODO: litmus tries to acquire a shared lock one resource twice (locks: 27 'double_sharedlock')
- TODO: litmus tries to acquire a shared lock on one resource twice (locks: 27 'double_sharedlock')
and fails, when we return HTTP_LOCKED. So we allow multi shared locks
on a resource even for the same principal
@ -424,52 +454,56 @@ class LockManager(object):
assert lockscope in ("shared", "exclusive")
assert lockdepth in ("0", "infinity")
self._log("checkLockPermission(%s, %s, %s, %s)" % (lockroot, lockscope, lockdepth, user))
_logger.debug("checkLockPermission(%s, %s, %s, %s)" % (lockroot, lockscope, lockdepth, user))
conflictLockList = []
# Check lockroot and all parents for conflicting locks
u = lockroot
while u:
# TODO: check, if expired
ll = self.getUrlLockList(u)
for l in ll:
self._log(" check parent %s, %s" % (u, l))
if u != lockroot and l["depth"] != "infinity":
continue # We only consider parents with Depth: infinity
elif l["scope"] == "shared" and lockscope == "shared": # and user != l["principal"]:
continue # Only compatible with shared locks (even by same principal)
# Return first lock as a list (in a sane system there can be max. one anyway)
self._log(" -> DENIED due to locked parent %s" % _lockString(l))
return [ (l, DAVError(HTTP_LOCKED)) ]
# TODO: this also will check the realm level itself
u = util.getUriParent(u)
if lockdepth == "0":
return conflictLockList
# TODO: we could exit also, if lockroot is not a collection or assert that depth=0 in this case
# Check child urls for conflicting locks
prefix = "URL2TOKEN:" + lockroot
if not prefix.endswith("/"):
prefix += "/"
for u, ll in self._dict.items():
if not u.startswith(prefix):
continue # Not a child
# TODO: check, if expired
self._lock.acquireRead()
try:
conflictLockList = []
# Check lockroot and all parents for conflicting locks
u = lockroot
while u:
# TODO: check, if expired
ll = self.getUrlLockList(u)
for l in ll:
_logger.debug(" check parent %s, %s" % (u, l))
if u != lockroot and l["depth"] != "infinity":
continue # We only consider parents with Depth: infinity
elif l["scope"] == "shared" and lockscope == "shared": # and user != l["principal"]:
continue # Only compatible with shared locks (even by same principal)
# Return first lock as a list (in a sane system there can be max. one anyway)
_logger.debug(" -> DENIED due to locked parent %s" % _lockString(l))
return [ (l, DAVError(HTTP_LOCKED)) ]
# TODO: this also will check the realm level itself
u = util.getUriParent(u)
if lockdepth == "0":
return conflictLockList
for l in ll:
lockDict = self.getLock(l)
# self._log(" check child %s, %s" % (u, l))
# TODO: no-conflicting-lock can pass a list of href elements too:
conflictLockList.append((lockDict,
DAVError(HTTP_PRECONDITION_FAILED,
preconditionCode=PRECONDITION_CODE_LockConflict)
))
self._log(" -> DENIED due to locked child %s" % _lockString(lockDict))
return conflictLockList
# TODO: we could exit also, if lockroot is not a collection or assert that depth=0 in this case
# Check child urls for conflicting locks
prefix = "URL2TOKEN:" + lockroot
if not prefix.endswith("/"):
prefix += "/"
for u, ll in self._dict.items():
if not u.startswith(prefix):
continue # Not a child
# TODO: check, if expired
for l in ll:
lockDict = self.getLock(l)
_logger.debug(" check child %s, %s" % (u, l))
# TODO: no-conflicting-lock can pass a list of href elements too:
conflictLockList.append((lockDict,
DAVError(HTTP_PRECONDITION_FAILED,
preconditionCode=PRECONDITION_CODE_LockConflict)
))
_logger.debug(" -> DENIED due to locked child %s" % _lockString(lockDict))
return conflictLockList
finally:
self._lock.release()
def checkAccessPermission(self, resourceAL, url, tokenList, accesstype, accessdepth, user):
@ -504,71 +538,139 @@ class LockManager(object):
assert accesstype == "write"
assert accessdepth in ("0", "infinity")
conflictLockList = []
self._log("checkAccessPermission(%s, %s, %s, %s)" % (url, tokenList, accessdepth, user))
_logger.debug("checkAccessPermission(%s, %s, %s, %s)" % (url, tokenList, accessdepth, user))
# Check url and all parents for conflicting locks
u = url
while u:
# TODO: check, if expired
ll = self.getUrlLockList(u)
# self._log(" checking %s" % u)
for l in ll:
self._log(" l=%s" % l)
if u != url and l["depth"] != "infinity":
continue # We only consider parents with Depth: inifinity
elif user == l["principal"] and l["token"] in tokenList:
continue # User owns this lock
elif l["token"] in tokenList:
# Token is owned by another user
conflictLockList.append((l, DAVError(HTTP_FORBIDDEN)))
self._log(" -> DENIED due to locked parent %s" % _lockString(l))
else:
# Token is owned by user, but not passed with lock list
# TODO: no-conflicting-lock can pass a list of href elements too:
conflictLockList.append((l, DAVError(HTTP_LOCKED,
preconditionCode=PRECONDITION_CODE_LockConflict)))
self._log(" -> DENIED due to locked parent %s" % _lockString(l))
u = util.getUriParent(u)
if accessdepth == "0":
# We only request flat access, so no need to check for child locks
return conflictLockList
# TODO: we could exit also, if <url> is not a collection
# Check child urls for conflicting locks
prefix = "URL2TOKEN:" + url
if not prefix.endswith("/"):
prefix += "/"
for u, ll in self._dict.items():
if not u.startswith(prefix):
continue # Not a child
# TODO: check, if expired
self._lock.acquireRead()
try:
conflictLockList = []
# Check url and all parents for conflicting locks
u = url
while u:
# print "checking ", u
# if u != "/":
# u = u.rstrip("/")
# TODO: check, if expired
ll = self.getUrlLockList(u)
# _logger.debug(" checking %s" % u)
for l in ll:
_logger.debug(" l=%s" % l)
if u != url and l["depth"] != "infinity":
continue # We only consider parents with Depth: inifinity
elif user == l["principal"] and l["token"] in tokenList:
continue # User owns this lock
elif l["token"] in tokenList:
# Token is owned by another user
conflictLockList.append((l, DAVError(HTTP_FORBIDDEN)))
_logger.debug(" -> DENIED due to locked parent %s" % _lockString(l))
else:
# Token is owned by user, but not passed with lock list
# TODO: no-conflicting-lock can pass a list of href elements too:
conflictLockList.append((l, DAVError(HTTP_LOCKED,
preconditionCode=PRECONDITION_CODE_LockConflict)))
_logger.debug(" -> DENIED due to locked parent %s" % _lockString(l))
u = util.getUriParent(u)
if accessdepth == "0":
# We only request flat access, so no need to check for child locks
return conflictLockList
for tok in ll:
l = self.getLock(tok)
# TODO: no-conflicting-lock can pass a list of href elements too:
conflictLockList.append((l,
DAVError(HTTP_LOCKED,
preconditionCode=PRECONDITION_CODE_LockConflict)
))
self._log(" -> DENIED due to locked child %s" % _lockString(l))
return conflictLockList
# TODO: we could exit also, if <url> is not a collection
# Check child urls for conflicting locks
prefix = "URL2TOKEN:" + url
if not prefix.endswith("/"):
prefix += "/"
for u, ll in self._dict.items():
if not u.startswith(prefix):
continue # Not a child
# TODO: check, if expired
for tok in ll:
l = self.getLock(tok)
# TODO: no-conflicting-lock can pass a list of href elements too:
conflictLockList.append((l,
DAVError(HTTP_LOCKED,
preconditionCode=PRECONDITION_CODE_LockConflict)
))
_logger.debug(" -> DENIED due to locked child %s" % _lockString(l))
return conflictLockList
finally:
self._lock.release()
#===============================================================================
# ShelveLockManager
#===============================================================================
class ShelveLockManager(LockManager):
"""
A low performance lock manager implementation using shelve.
"""
def __init__(self, storagePath):
self._storagePath = os.path.abspath(storagePath)
super(ShelveLockManager, self).__init__()
def __repr__(self):
return "ShelveLockManager(%s)" % self._storagePath
def _lazyOpen(self):
_logger.debug("_lazyOpen(%s)" % self._storagePath)
self._lock.acquireWrite()
try:
# Test again within the critical section
if self._loaded:
return True
# Open with writeback=False, which is faster, but we have to be
# careful to re-assign values to _dict after modifying them
self._dict = shelve.open(self._storagePath,
writeback=False)
self._loaded = True
if __debug__ and self._verbose >= 2:
# self._check("After shelve.open()")
self._dump("After shelve.open()")
finally:
self._lock.release()
def _sync(self):
"""Write persistent dictionary to disc."""
_logger.debug("_sync()")
self._lock.acquireWrite() # TODO: read access is enough?
try:
if self._loaded:
self._dict.sync()
finally:
self._lock.release()
def _close(self):
_logger.debug("_close()")
self._lock.acquireWrite()
try:
if self._loaded:
self._dict.close()
self._dict = None
self._loaded = False
finally:
self._lock.release()
#===============================================================================
# Tool functions
#===============================================================================
reSecondsReader = re.compile(r'second\-([0-9]+)', re.I)
def readTimeoutValueHeader(timeoutvalue):
"""Return -1 if infinite, else return numofsecs."""
timeoutsecs = 0
timeoutvaluelist = timeoutvalue.split(',')
timeoutvaluelist = timeoutvalue.split(",")
for timeoutspec in timeoutvaluelist:
timeoutspec = timeoutspec.strip()
if timeoutspec.lower() == 'infinite':
if timeoutspec.lower() == "infinite":
return -1
else:
listSR = reSecondsReader.findall(timeoutspec)
@ -595,8 +697,8 @@ def _lockString(lockDict):
def test():
l = LockManager("wsgidav-locks.shelve")
l._performInitialization()
l = ShelveLockManager("wsgidav-locks.shelve")
l._lazyOpen()
l._dump()
# l.generateLock("martin", "", lockscope, lockdepth, lockowner, lockroot, timeout)

View file

@ -6,7 +6,8 @@ property_manager
:Author: Martin Wendt, moogle(at)wwwendt.de
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
A low performance property manager implementation using shelve.
Implements two property managers: one in-memory (dict-based), and one
persistent low performance variant using shelve.
This module consists of a number of miscellaneous functions for the dead
properties features of WebDAV.
@ -67,7 +68,10 @@ Dead properties
``wsgidav.property_manager``.
"""
from wsgidav import util
import traceback
import os
import sys
import shelve
from rw_lock import ReadWriteLock
# TODO: comment's from Ian Bicking (2005)
#@@: Use of shelve means this is only really useful in a threaded environment.
@ -80,41 +84,64 @@ import traceback
# in a parallel directory structure to the files you are describing.
# Pickle is expedient, but later you could use something more readable
# (pickles aren't particularly readable)
import sys
__docformat__ = 'reStructuredText'
__docformat__ = "reStructuredText"
import shelve
import threading
_logger = util.getModuleLogger(__name__)
#===============================================================================
# PropertyManager
#===============================================================================
class PropertyManager(object):
"""
A low performance property manager implementation using shelve
An in-memory property manager implementation using a dictionary.
This is obviously not persistent, but should be enough in some cases.
For a persistent implementation, see property_manager.ShelvePropertyManager().
"""
def __init__(self, persiststore):
self._loaded = False
def __init__(self):
self._dict = None
self._init_lock = threading.RLock()
self._write_lock = threading.RLock()
self._persiststorepath = persiststore
self._loaded = False
self._lock = ReadWriteLock()
self._verbose = 2
def _performInitialization(self):
self._init_lock.acquire(True)
def __repr__(self):
return "PropertyManager"
def __del__(self):
if __debug__ and self._verbose >= 2:
self._check()
self._close()
def _lazyOpen(self):
_logger.debug("_lazyOpen()")
self._lock.acquireWrite()
try:
if self._loaded: # test again within the critical section
return True
self._dict = shelve.open(self._persiststorepath)
self._dict = {}
self._loaded = True
if self._verbose >= 2:
self._dump("After shelve.open()")
finally:
self._init_lock.release()
self._lock.release()
def _sync(self):
pass
def _close(self):
_logger.debug("_close()")
self._lock.acquireWrite()
try:
self._dict = None
self._loaded = False
finally:
self._lock.release()
def _check(self, msg=""):
try:
if not self._loaded:
@ -124,27 +151,24 @@ class PropertyManager(object):
# print " -> %s" % self._dict[k]
for k, v in self._dict.items():
_ = "%s, %s" % (k, v)
self._log("PropertyManager checks ok " + msg)
_logger.debug("%s checks ok %s" % (self.__class__.__name__, msg))
return True
except Exception:
traceback.print_exc()
_logger.exception("%s _check: ERROR %s" % (self.__class__.__name__, msg))
# traceback.print_exc()
# raise
# sys.exit(-1)
return False
def _log(self, msg):
if self._verbose >= 2:
util.log(msg)
def _dump(self, msg="", out=None):
if out is None:
out = sys.stdout
print >>out, "PropertyManager(%s): %s" % (self._persiststorepath, msg)
print >>out, "%s(%s): %s" % (self.__class__.__name__, self.__repr__(), msg)
if not self._loaded:
self._performInitialization()
self._lazyOpen()
if self._verbose >= 2:
return # Already dumped in _performInitialization
return # Already dumped in _lazyOpen
try:
for k, v in self._dict.items():
print >>out, " ", k
@ -154,107 +178,180 @@ class PropertyManager(object):
except Exception, e:
print >>out, " %s: ERROR %s" % (k2, e)
except Exception, e:
print >>sys.stderr, "PropertyManager._dump() ERROR: %s" % e
print >>sys.stderr, "PropertyManager._dump() ERROR: %s" % e
def getProperties(self, normurl):
if not self._loaded:
self._performInitialization()
returnlist = []
if normurl in self._dict:
for propdata in self._dict[normurl].keys():
returnlist.append(propdata)
return returnlist
_logger.debug("getProperties(%s)" % normurl)
self._lock.acquireRead()
try:
if not self._loaded:
self._lazyOpen()
returnlist = []
if normurl in self._dict:
for propdata in self._dict[normurl].keys():
returnlist.append(propdata)
return returnlist
finally:
self._lock.release()
def getProperty(self, normurl, propname):
if not self._loaded:
self._performInitialization()
if normurl not in self._dict:
return None
# TODO: sometimes we get exceptions here: (catch or otherwise make more robust?)
_logger.debug("getProperty(%s, %s)" % (normurl, propname))
self._lock.acquireRead()
try:
resourceprops = self._dict[normurl]
except Exception, e:
util.log("getProperty(%s, %s) failed : %s" % (normurl, propname, e))
raise
return resourceprops.get(propname)
if not self._loaded:
self._lazyOpen()
if normurl not in self._dict:
return None
# TODO: sometimes we get exceptions here: (catch or otherwise make more robust?)
try:
resourceprops = self._dict[normurl]
except Exception, e:
_logger.exception("getProperty(%s, %s) failed : %s" % (normurl, propname, e))
raise
return resourceprops.get(propname)
finally:
self._lock.release()
def writeProperty(self, normurl, propname, propertyvalue, dryRun=False):
self._log("writeProperty(%s, %s, dryRun=%s):\n\t%s" % (normurl, propname, dryRun, propertyvalue))
# self._log("writeProperty(%s, %s, dryRun=%s):\n\t%s" % (normurl, propname, dryRun, propertyvalue))
assert normurl and normurl.startswith("/")
assert propname #and propname.startswith("{")
assert propertyvalue is not None
_logger.debug("writeProperty(%s, %s, dryRun=%s):\n\t%s" % (normurl, propname, dryRun, propertyvalue))
if dryRun:
return # TODO: can we check anything here?
propertyname = propname
self._write_lock.acquire(True)
self._lock.acquireWrite()
try:
if not self._loaded:
self._performInitialization()
self._lazyOpen()
if normurl in self._dict:
locatordict = self._dict[normurl]
else:
locatordict = dict([])
locatordict[propertyname] = propertyvalue
locatordict = {} #dict([])
locatordict[propname] = propertyvalue
# This re-assignment is important, so Shelve realizes the change:
self._dict[normurl] = locatordict
self._dict.sync()
self._sync()
if __debug__ and self._verbose >= 2:
self._check()
finally:
self._write_lock.release()
self._check()
self._lock.release()
def removeProperty(self, normurl, propname, dryRun=False):
"""
Specifying the removal of a property that does not exist is NOT an error.
"""
self._log("removeProperty(%s, %s, dryRun=%s)" % (normurl, propname, dryRun))
_logger.debug("removeProperty(%s, %s, dryRun=%s)" % (normurl, propname, dryRun))
if dryRun:
# TODO: can we check anything here?
return
propertyname = propname
self._write_lock.acquire(True)
self._lock.acquireWrite()
try:
if not self._loaded:
self._performInitialization()
self._lazyOpen()
if normurl in self._dict:
locatordict = self._dict[normurl]
if propertyname in locatordict:
del locatordict[propertyname]
if propname in locatordict:
del locatordict[propname]
# This re-assignment is important, so Shelve realizes the change:
self._dict[normurl] = locatordict
self._dict.sync()
self._sync()
if __debug__ and self._verbose >= 2:
self._check()
finally:
self._write_lock.release()
self._check()
self._lock.release()
def removeProperties(self, normurl):
self._write_lock.acquire(True)
self._log("removeProperties(%s)" % normurl)
_logger.debug("removeProperties(%s)" % normurl)
self._lock.acquireWrite()
try:
if not self._loaded:
self._performInitialization()
self._lazyOpen()
if normurl in self._dict:
del self._dict[normurl]
self._sync()
finally:
self._write_lock.release()
self._lock.release()
def copyProperties(self, origurl, desturl):
self._write_lock.acquire(True)
self._log("copyProperties(%s, %s)" % (origurl, desturl))
self._check()
def copyProperties(self, srcurl, desturl):
_logger.debug("copyProperties(%s, %s)" % (srcurl, desturl))
self._lock.acquireWrite()
try:
if __debug__ and self._verbose >= 2:
self._check()
if not self._loaded:
self._performInitialization()
if origurl in self._dict:
self._dict[desturl] = self._dict[origurl].copy()
self._lazyOpen()
if srcurl in self._dict:
self._dict[desturl] = self._dict[srcurl].copy()
self._sync()
if __debug__ and self._verbose >= 2:
self._check("after copy")
finally:
self._write_lock.release()
self._check("after copy")
self._lock.release()
#===============================================================================
# ShelvePropertyManager
#===============================================================================
class ShelvePropertyManager(PropertyManager):
"""
A low performance property manager implementation using shelve
"""
def __init__(self, storagePath):
self._storagePath = os.path.abspath(storagePath)
super(ShelvePropertyManager, self).__init__()
def __repr__(self):
return repr(self._dict)
return "ShelvePropertyManager(%s)" % self._storagePath
def __del__(self):
self._check()
if self._loaded:
self._dict.close()
self._check()
def _lazyOpen(self):
_logger.debug("_lazyOpen(%s)" % self._storagePath)
self._lock.acquireWrite()
try:
# Test again within the critical section
if self._loaded:
return True
# Open with writeback=False, which is faster, but we have to be
# careful to re-assign values to _dict after modifying them
self._dict = shelve.open(self._storagePath,
writeback=False)
self._loaded = True
if __debug__ and self._verbose >= 2:
self._check("After shelve.open()")
self._dump("After shelve.open()")
finally:
self._lock.release()
def _sync(self):
"""Write persistent dictionary to disc."""
_logger.debug("_sync()")
self._lock.acquireWrite() # TODO: read access is enough?
try:
if self._loaded:
self._dict.sync()
finally:
self._lock.release()
def _close(self):
_logger.debug("_close()")
self._lock.acquireWrite()
try:
if self._loaded:
self._dict.close()
self._dict = None
self._loaded = False
finally:
self._lock.release()

View file

@ -1,6 +1,6 @@
"""
request_resolver
===============
================
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
:Author: Martin Wendt, moogle(at)wwwendt.de
@ -9,6 +9,8 @@ request_resolver
WSGI middleware that finds the registered mapped DAV-Provider, creates a new
RequestServer instance, and dispatches the request.
+-------------------------------------------------------------------------------+
| The following documentation was taken over from PyFileServer and is outdated! |
+-------------------------------------------------------------------------------+
@ -95,13 +97,10 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
import util
#import urllib
from dav_error import DAVError, HTTP_NOT_FOUND
from request_server import RequestServer
#from threading import local
#_tls = local() # Thread local storage dict
__docformat__ = 'reStructuredText'
__docformat__ = "reStructuredText"
# NOTE (Martin Wendt, 2009-05):
# The following remarks were made by Ian Bicking when reviewing PyFileServer in 2005.
@ -168,7 +167,7 @@ class RequestResolver(object):
# Hotfix for WinXP / Vista: check for '/' also
if environ["REQUEST_METHOD"] == "OPTIONS" and path in ("/", "*"):
# Answer HTTP 'OPTIONS' method on server-level.
# From RFC 2616
# From RFC 2616:
# If the Request-URI is an asterisk ("*"), the OPTIONS request is
# intended to apply to the server in general rather than to a specific
# resource. Since a server's communication options typically depend on
@ -176,13 +175,13 @@ class RequestResolver(object):
# type of method; it does nothing beyond allowing the client to test the
# capabilities of the server. For example, this can be used to test a
# proxy for HTTP/1.1 compliance (or lack thereof).
start_response('200 OK', [('Content-Type', 'text/html'),
('Content-Length', '0'),
('DAV', '1,2'),
('Server', 'DAV/2'),
('Date', util.getRfc1123Time()),
start_response("200 OK", [("Content-Type", "text/html"),
("Content-Length", "0"),
("DAV", "1,2"),
("Server", "DAV/2"),
("Date", util.getRfc1123Time()),
])
# return ['']
# return [""]
yield [ "" ]
return
@ -192,16 +191,13 @@ class RequestResolver(object):
"Could not find resource provider for '%s'" % path)
# Let the appropriate resource provider for the realm handle the request
# **************************
# ************************** only to make sure we have no __del__
# **************************
# **************************
app = RequestServer(provider)
environ["wsgidav.request_server"] = app # Keep it alive for the whole request lifetime
# environ["wsgidav.request_server"] = app # Keep it alive for the whole request lifetime
# _tls.app = app # TODO: try to avoid Server socket closed
for v in app(environ, start_response):
util.debug("sc", "RequestResolver: yield start")
yield v
util.log("Response", v)
util.debug("sc", "RequestResolver: yield end")
return
# return app(environ, start_response)

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
"""
Running WsgiDAV
====================
===============
WsgiDAV comes bundled with a simple WSGI webserver.
@ -57,6 +57,7 @@ It includes code from the following sources:
flexible handler method <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/307618> under public domain.
"""
from wsgidav.version import __version__
import socket
@ -69,6 +70,7 @@ try:
except ImportError:
from StringIO import StringIO
_logger = util.getModuleLogger(__name__)
SERVER_ERROR = """\
<html>
@ -85,7 +87,7 @@ SERVER_ERROR = """\
class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
_SUPPORTED_METHODS = ['HEAD','GET','PUT','POST','OPTIONS','TRACE','DELETE','PROPFIND','PROPPATCH','MKCOL','COPY','MOVE','LOCK','UNLOCK']
_SUPPORTED_METHODS = ["HEAD","GET","PUT","POST","OPTIONS","TRACE","DELETE","PROPFIND","PROPPATCH","MKCOL","COPY","MOVE","LOCK","UNLOCK"]
def log_message (self, *args):
pass
@ -95,7 +97,7 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
def getApp (self):
# We want fragments to be returned as part of <path>
_protocol, _host, path, _parameters, query, _fragment = urlparse.urlparse ('http://dummyhost%s' % self.path,
_protocol, _host, path, _parameters, query, _fragment = urlparse.urlparse ("http://dummyhost%s" % self.path,
allow_fragments=False)
# Find any application we might have
for appPath, app in self.server.wsgiApplications:
@ -103,9 +105,9 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
# We found the application to use - work out the scriptName and pathInfo
pathInfo = path [len (appPath):]
if (len (pathInfo) > 0):
if (not pathInfo.startswith ('/')):
pathInfo = '/' + pathInfo
if (appPath.endswith ('/')):
if (not pathInfo.startswith ("/")):
pathInfo = "/" + pathInfo
if (appPath.endswith ("/")):
scriptName = appPath[:-1]
else:
scriptName = appPath
@ -121,40 +123,45 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
def do_method(self):
app, scriptName, pathInfo, query = self.getApp ()
if (not app):
self.send_error (404, 'Application not found.')
self.send_error (404, "Application not found.")
return
self.runWSGIApp (app, scriptName, pathInfo, query)
def __getattr__(self, name):
if len(name)>3 and name[0:3] == 'do_' and name[3:] in self._SUPPORTED_METHODS:
if len(name)>3 and name[0:3] == "do_" and name[3:] in self._SUPPORTED_METHODS:
return self.handlerFunctionClosure(name)
else:
self.send_error (501, 'Method Not Implemented.')
self.send_error (501, "Method Not Implemented.")
return
def runWSGIApp (self, application, scriptName, pathInfo, query):
logging.info ("Running application with SCRIPT_NAME %s PATH_INFO %s" % (scriptName, pathInfo))
env = {'wsgi.version': (1,0)
,'wsgi.url_scheme': 'http'
,'wsgi.input': self.rfile
,'wsgi.errors': sys.stderr
,'wsgi.multithread': 1
,'wsgi.multiprocess': 0
,'wsgi.run_once': 0
,'REQUEST_METHOD': self.command
,'SCRIPT_NAME': scriptName
,'PATH_INFO': pathInfo
,'QUERY_STRING': query
,'CONTENT_TYPE': self.headers.get ('Content-Type', '')
,'CONTENT_LENGTH': self.headers.get ('Content-Length', '')
,'REMOTE_ADDR': self.client_address[0]
,'SERVER_NAME': self.server.server_address [0]
,'SERVER_PORT': str (self.server.server_address [1])
,'SERVER_PROTOCOL': self.request_version
}
if self.command == "PUT":
pass # breakpoint
env = {"wsgi.version": (1, 0),
"wsgi.url_scheme": "http",
"wsgi.input": self.rfile,
"wsgi.errors": sys.stderr,
"wsgi.multithread": 1,
"wsgi.multiprocess": 0,
"wsgi.run_once": 0,
"REQUEST_METHOD": self.command,
"SCRIPT_NAME": scriptName,
"PATH_INFO": pathInfo,
"QUERY_STRING": query,
"CONTENT_TYPE": self.headers.get("Content-Type", ""),
"CONTENT_LENGTH": self.headers.get("Content-Length", ""),
"REMOTE_ADDR": self.client_address[0],
"SERVER_NAME": self.server.server_address[0],
"SERVER_PORT": str(self.server.server_address[1]),
"SERVER_PROTOCOL": self.request_version,
}
for httpHeader, httpValue in self.headers.items():
env ['HTTP_%s' % httpHeader.replace ('-', '_').upper()] = httpValue
if not httpHeader in ("Content-Type", "Content-Length"):
env ["HTTP_%s" % httpHeader.replace ("-", "_").upper()] = httpValue
# print env["REQUEST_METHOD"], env.get("HTTP_AUTHORIZATION")
# Setup the state
self.wsgiSentHeaders = 0
@ -162,25 +169,25 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
try:
# We have there environment, now invoke the application
util.debug("sc", "runWSGIApp application()...")
_logger.debug("runWSGIApp application()...")
result = application (env, self.wsgiStartResponse)
try:
for data in result:
if data:
self.wsgiWriteData (data)
else:
util.debug("sc", "runWSGIApp empty data")
_logger.debug("runWSGIApp empty data")
finally:
util.debug("sc", "runWSGIApp finally.")
if hasattr(result, 'close'):
_logger.debug("runWSGIApp finally.")
if hasattr(result, "close"):
result.close()
except:
util.debug("sc", "runWSGIApp caught exception...")
_logger.debug("runWSGIApp caught exception...")
errorMsg = StringIO()
traceback.print_exc(file=errorMsg)
logging.error (errorMsg.getvalue())
if not self.wsgiSentHeaders:
self.wsgiStartResponse('500 Server Error', [('Content-type', 'text/html')])
self.wsgiStartResponse("500 Server Error", [("Content-type", "text/html")])
self.wsgiWriteData(SERVER_ERROR)
if (not self.wsgiSentHeaders):
@ -189,7 +196,7 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
return
def wsgiStartResponse (self, response_status, response_headers, exc_info=None):
util.debug("sc", "wsgiStartResponse(%s, %s, %s)" % (response_status, response_headers, exc_info))
_logger.debug("wsgiStartResponse(%s, %s, %s)" % (response_status, response_headers, exc_info))
if (self.wsgiSentHeaders):
raise Exception ("Headers already sent and start_response called again!")
# Should really take a copy to avoid changes in the application....
@ -200,16 +207,16 @@ class ExtHandler (BaseHTTPServer.BaseHTTPRequestHandler):
if (not self.wsgiSentHeaders):
status, headers = self.wsgiHeaders
# Need to send header prior to data
statusCode = status [:status.find (' ')]
statusMsg = status [status.find (' ') + 1:]
util.debug("sc", "wsgiWriteData: send headers '%s', %s" % (status, headers))
statusCode = status [:status.find (" ")]
statusMsg = status [status.find (" ") + 1:]
_logger.debug("wsgiWriteData: send headers '%s', %s" % (status, headers))
self.send_response (int (statusCode), statusMsg)
for header, value in headers:
self.send_header (header, value)
self.end_headers()
self.wsgiSentHeaders = 1
# Send the data
util.debug("sc", "wsgiWriteData: '%s...', len=%s" % (data[:10], len(data)))
_logger.debug("wsgiWriteData: '%s...', len=%s" % (data[:10], len(data)))
self.wfile.write (data)
class ExtServer (SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
@ -224,16 +231,16 @@ class ExtServer (SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
def serve(conf, app):
host = conf.get('host', 'localhost')
port = int(conf.get('port', 8080))
server = ExtServer((host, port), {'': app})
if conf.get('verbose') >= 1:
host = conf.get("host", "localhost")
port = int(conf.get("port", 8080))
server = ExtServer((host, port), {"": app})
if conf.get("verbose") >= 1:
if host in ("", "0.0.0.0"):
print "WsgiDAV serving at %s, port %s (local IP is %s)..." % (host, port, socket.gethostbyname(socket.gethostname()))
print "WsgiDAV %s serving at %s, port %s (local IP is %s)..." % (__version__, host, port, socket.gethostbyname(socket.gethostname()))
else:
print "WsgiDAV serving at %s, port %s..." % (host, port)
print "WsgiDAV %s serving at %s, port %s..." % (__version__, host, port)
server.serve_forever()
if __name__ == '__main__':
if __name__ == "__main__":
raise RuntimeError("Use run_server.py")

View file

@ -40,6 +40,7 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
from optparse import OptionParser
from pprint import pprint
from inspect import isfunction
from wsgidav.wsgidav_app import DEFAULT_CONFIG
import traceback
import sys
import os
@ -55,36 +56,6 @@ __docformat__ = "reStructuredText"
# Use this config file, if no --config_file option is specified
DEFAULT_CONFIG_FILE = "wsgidav.conf"
# Use these settings, if config file does not define them (or is totally missing)
DEFAULT_CONFIG = {
"provider_mapping": {},
"user_mapping": {},
# "host": "127.0.0.1",
# "port": 80,
"propsmanager": None,
"propsfile": None,
"locksmanager": None, # None: use lock_manager.LockManager
"locksfile": None, # Used as default for
"domaincontroller": None,
# HTTP Authentication Options
"acceptbasic": True, # Allow basic authentication, True or False
"acceptdigest": True, # Allow digest authentication, True or False
"defaultdigest": True, # True (default digest) or False (default basic)
# Verbose Output
"verbose": 2, # 0 - no output (excepting application exceptions)
# 1 - show single line request summaries (for HTTP logging)
# 2 - show additional events
# 3 - show full request/response header info (HTTP Logging)
# request body and GET response bodies not shown
# Organizational Information - printed as a footer on html output
"response_trailer": None,
}
def _initCommandLineOptions():
"""Parse command line options into a dictionary."""
@ -118,14 +89,14 @@ See http://wsgidav.googlecode.com for additional information."""
description=None, #description,
add_help_option=True,
# prog="wsgidav",
epilog=epilog
# epilog=epilog # TODO: Not available on Python 2.4?
)
parser.add_option("-p", "--port",
dest="port",
type="int",
default=8080,
help='port to serve on (default: %default)')
help="port to serve on (default: %default)")
parser.add_option("-H", "--host", # '-h' conflicts with --help
dest="host",
default="localhost",
@ -146,7 +117,7 @@ See http://wsgidav.googlecode.com for additional information."""
parser.add_option("-c", "--config",
dest="config_file",
help="Configuration file (default: %default).")
help="Configuration file (default: %s in current directory)." % DEFAULT_CONFIG_FILE)
(options, args) = parser.parse_args()
@ -159,13 +130,13 @@ See http://wsgidav.googlecode.com for additional information."""
defPath = os.path.abspath(DEFAULT_CONFIG_FILE)
if os.path.exists(defPath):
if options.verbose >= 2:
print "Using default config file: %s" % defPath
print "Using default configuration file: %s" % defPath
options.config_file = defPath
else:
# If --config was specified convert to absolute path and assert it exists
options.config_file = os.path.abspath(options.config_file)
if not os.path.exists(options.config_file):
parser.error("Invalid config file specified: %s" % options.config_file)
parser.error("Could not open specified configuration file: %s" % options.config_file)
# Convert options object to dictionary
cmdLineOpts = options.__dict__.copy()
@ -199,9 +170,9 @@ def _readConfigFile(config_file, verbose):
if verbose >= 1:
traceback.print_exc()
exceptioninfo = traceback.format_exception_only(sys.exc_type, sys.exc_value) #@UndefinedVariable
exceptiontext = ''
exceptiontext = ""
for einfo in exceptioninfo:
exceptiontext += einfo + '\n'
exceptiontext += einfo + "\n"
raise RuntimeError("Failed to read configuration file: " + config_file + "\nDue to " + exceptiontext)
return conf
@ -231,11 +202,12 @@ def _initConfig():
config["port"] = cmdLineOpts.get("port")
if cmdLineOpts.get("host"):
config["host"] = cmdLineOpts.get("host")
if cmdLineOpts.get("verbose"):
if cmdLineOpts.get("verbose") is not None:
config["verbose"] = cmdLineOpts.get("verbose")
if cmdLineOpts.get("root_path"):
config["provider_mapping"]["/"] = FilesystemProvider(cmdLineOpts.get("root_path"))
root_path = os.path.abspath(cmdLineOpts.get("root_path"))
config["provider_mapping"]["/"] = FilesystemProvider(root_path)
if cmdLineOpts["verbose"] >= 3:
print "Configuration(%s):" % cmdLineOpts["config_file"]
@ -244,7 +216,7 @@ def _initConfig():
if not config["provider_mapping"]:
print >>sys.stderr, "ERROR: No DAV provider defined. Try --help option."
sys.exit(-1)
# raise RuntimeWarning("No At least one DAV provider must be specified by a --root option, or in a configuration file.")
# raise RuntimeWarning("At least one DAV provider must be specified by a --root option, or in a configuration file.")
return config
@ -258,7 +230,7 @@ def _runPaste(app, config):
try:
from paste import httpserver
if config["verbose"] >= 2:
print "Running paste.httpserver..."
print "Running WsgiDAV %s on paste.httpserver..." % __version__
# See http://pythonpaste.org/modules/httpserver.html for more options
httpserver.serve(app,
host=config["host"],
@ -282,7 +254,7 @@ def _runCherryPy(app, config):
# http://cherrypy.org/apidocs/3.0.2/cherrypy.wsgiserver-module.html
from cherrypy import wsgiserver
if config["verbose"] >= 2:
print "wsgiserver.CherryPyWSGIServer..."
print "Running WsgiDAV %s on wsgiserver.CherryPyWSGIServer..." % __version__
server = wsgiserver.CherryPyWSGIServer(
(config["host"], config["port"]),
app,
@ -303,7 +275,7 @@ def _runSimpleServer(app, config):
# http://www.python.org/doc/2.5.2/lib/module-wsgiref.html
from wsgiref.simple_server import make_server
if config["verbose"] >= 2:
print "Running wsgiref.simple_server (single threaded)..."
print "Running WsgiDAV %s on wsgiref.simple_server (single threaded)..." % __version__
httpd = make_server(config["host"], config["port"], app)
# print "Serving HTTP on port 8000..."
httpd.serve_forever()
@ -317,11 +289,11 @@ def _runSimpleServer(app, config):
def _runBuiltIn(app, config):
"""Run WsgiDAV using ext_wsgiutils_server from the WsgiDAV package."""
"""Run WsgiDAV using ext_wsgiutils_server from the wsgidav package."""
try:
import ext_wsgiutils_server
if config["verbose"] >= 2:
print "Running wsgidav.ext_wsgiutils_server..."
print "Running WsgiDAV %s on wsgidav.ext_wsgiutils_server..." % __version__
ext_wsgiutils_server.serve(config, app)
except ImportError, e:
if config["verbose"] >= 1:
@ -330,40 +302,37 @@ def _runBuiltIn(app, config):
return True
SUPPORTED_SERVERS = {"paste": _runPaste,
"cherrypy": _runCherryPy,
"wsgiref": _runSimpleServer,
"wsgidav": _runBuiltIn,
}
def run():
def run():
config = _initConfig()
# from paste import pyconfig
# config = pyconfig.Config()
# config.load(opts.config_file)
# from paste.deploy import loadapp
# app = loadapp('config:/path/to/config.ini')
# app = loadapp("config:wsgidav.conf")
app = WsgiDAVApp(config)
# from wsgidav.wsgiapp import make_app
# global_conf = {}
# app = make_app(global_conf)
app = WsgiDAVApp(config)
# Try running WsgiDAV inside the following external servers:
res = False
# if not res:
# res = _runCherryPy(app, config)
# if not res:
# res = _runPaste(app, config)
# wsgiref.simple_server is single threaded
# if not res:
# res = _runSimpleServer(app, config)
if not res:
res = _runBuiltIn(app, config)
for e in config["ext_servers"]:
fn = SUPPORTED_SERVERS.get(e)
if fn is None:
print "Invalid external server '%s'. (expected: '%s')" % (e, "', '".join(SUPPORTED_SERVERS.keys()))
elif fn(app, config):
res = True
break
if not res:
print "No supported WSGI server installed."

View file

@ -25,41 +25,19 @@ def addUser(realmName, user, password, description, roles=[]):
################################################################################
# SERVER OPTIONS
# Property Options
#propsmanager = # uncomment this line to specify your own property manager
# default: wsgidav.property_manager.PropertyManager
#propsfile = # uncomment this line to specify a storage file location
# for wsgidav.property_manager.PropertyManager
# default: 'wsgidav-props.shelve' in the current directory
#===============================================================================
# 3rd party servers
# Try to run WsgiDAV inside these WSGI servers, in that order
ext_servers = (
# "paste",
# "cherrypy",
# "wsgiref",
"wsgidav",
)
# Locks Options
#locksmanager = # uncomment this line to specify your own locks manager
# default: wsgidav.lock_manager.LockManager
#locksfile = # uncomment this line to specify a storage file location
# for wsgidav.lock_manager.LockManager
# default: 'wsgidav-locks.shelve' in current directory
# Domain Controller
#domaincontroller = # uncomment this line to specify your own domain controller
# default: wsgidav.domain_controller
# uses USERS section below
# HTTP Authentication Options
acceptbasic = True # Allow basic authentication, True or False
acceptdigest = True # Allow digest authentication, True or False
defaultdigest = True # True (default digest) or False (default basic)
# Verbose Output
#===============================================================================
# Debugging
verbose = 2 # 0 - no output (excepting application exceptions)
# 1 - show single line request summaries (HTTP logging)
@ -68,6 +46,13 @@ verbose = 2 # 0 - no output (excepting application exceptions)
# request body and GET response bodies not shown
# Enable specific module loggers
# E.g. ["lock_manager", "property_manager", "http_authenticator", ...]
enable_loggers = []
#===============================================================================
# Organizational Information - printed as a footer on html output
admin_email = "admin@example.com"
@ -78,21 +63,66 @@ Support contact: <a href='mailto:%s'>Administrator</a> at %s.
""" % (admin_email, organization)
################################################################################
# DAV Provider
#===============================================================================
# Property Manager
#
# Uncomment this lines to specify your own property manager.
# Default: wsgidav.property_manager.PropertyManager
# Also available: wsgidav.property_manager.ShelvePropertyManager
#
# Check the documentation on how to develop custom property managers.
# Note that the default PropertyManager works in-memory, and thus is NOT
# persistent.
# Example: Use in-memory property manager (this is also the default)
#from wsgidav.property_manager import PropertyManager
#propsmanager = PropertyManager()
# Example: Use PERSISTENT shelve based property manager
#from wsgidav.property_manager import ShelvePropertyManager
#propsmanager = ShelvePropertyManager("wsgidav-props.shelve")
#===============================================================================
# Lock Manager
#
# Uncomment this lines to specify your own locks manager.
# Default: wsgidav.lock_manager.LockManager
# Also available: wsgidav.lock_manager.ShelveLockManager
#
# Check the documentation on how to develop custom lock managers.
# Note that the default LockManager works in-memory, and thus is NOT persistent.
# Example: Use in-memory lock manager (this is also the default)
#from wsgidav.lock_manager import LockManager
#locksmanager = LockManager()
# Example: Use PERSISTENT shelve based lock manager
#from wsgidav.lock_manager import ShelveLockManager
#locksmanager = ShelveLockManager("wsgidav-locks.shelve")
#===============================================================================
# SHARES
#
# If you would like to publish files in the location '/v_root' through a
# WsgiDAV share 'files', so that it can be accessed by this URL:
# http://server:port/files
# insert the following line:
# addShare('files', '/v_root')
# addShare("files", "/v_root")
# or, on a Windows box:
# addShare('files', 'c:\v_root')
# addShare("files", "c:\\v_root")
#
# To access the same directory using a root level share
# http://server:port/
# insert this line:
# addShare('', 'c:\v_root')
# addShare("", "/v_root")
#
# The above examples use wsgidav.fs_dav_provider.FilesystemProvider, which is
# the default provider implementation.
@ -100,29 +130,54 @@ Support contact: <a href='mailto:%s'>Administrator</a> at %s.
# If you wish to use a custom provider, an object must be passed as second
# parameter. See the examples below.
addShare("test", r"C:\temp")
addShare("temp", "C:\\temp")
### Add a read-only file share:
#from wsgidav.fs_dav_provider import ReadOnlyFilesystemProvider
#addShare("tmp", ReadOnlyFilesystemProvider(r"C:\tmp"))
#addShare("tmp", ReadOnlyFilesystemProvider("/tmp"))
### Publish an MySQL 'world' database as share '/world-db'
#from wsgidav.addons.mysql_dav_provider import MySQLBrowserProvider
#addShare("world-db", MySQLBrowserProvider("localhost", "root", "test", "world"))
# Publish an SQL table
#from wsgidav.addons.simplemysqlabstractionlayer import SimpleMySQLResourceAbstractionLayer
#addrealm('testdb', 'database', 'testdb')
#addrealm('mysqldb', 'database', 'mysqldb')
#addAL("testdb", SimpleMySQLResourceAbstractionLayer("localhost", "", "anon", "test"))
#addAL("mysqldb", SimpleMySQLResourceAbstractionLayer("localhost", "", "anon", "mysql"))
# Publish a virtual structure
from wsgidav.addons.virtual_dav_provider import VirtualResourceProvider
addShare("virtres", VirtualResourceProvider())
#addShare("", VirtualResourceProvider())
#from wsgidav.addons.virtual_dav_provider import VirtualResourceProvider
#addShare("virtres", VirtualResourceProvider())
################################################################################
# AUTHENTICATION
#===============================================================================
# HTTP Authentication Options
acceptbasic = True # Allow basic authentication, True or False
acceptdigest = True # Allow digest authentication, True or False
defaultdigest = True # True (default digest) or False (default basic)
#domaincontroller = # Uncomment this line to specify your own domain controller
# Default: wsgidav.domain_controller, which uses the USERS
# section below
# Example: use a domain controller that allows users to authenticate against
# a Windows NT domain or a local computer.
# Note: NTDomainController requires basic authentication:
# Set acceptbasic=True, acceptdigest=False, defaultdigest=False
#from wsgidav.addons.nt_domain_controller import NTDomainController
#domaincontroller = NTDomainController(presetdomain=None, presetserver=None)
#acceptbasic = True
#acceptdigest = False
#defaultdigest = False
#===============================================================================
# USERS
#
# This section is used by for authentication by the default Domain Controller.
# This section is ONLY used by the DEFAULT Domain Controller.
#
# Users are defined per realm:
# addUser(<realm>, <user>, <password>, <description>)
@ -132,15 +187,15 @@ addShare("virtres", VirtualResourceProvider())
# If no users are specified for a realm, no authentication is required.
# Thus granting read-write access to anonymous!
#
# Note: If you wish to use Windows WebDAV support (such as Windows XP's My Network Places),
# you need to include the domain of the user as part of the username (note the DOUBLE slash),
# such as:
# addUser('v_root', 'domain\\user', 'password', 'description')
# Note: If you wish to use Windows WebDAV support (such as Windows XP's My
# Network Places), you need to include the domain of the user as part of the
# username (note the DOUBLE slash), such as:
# addUser("v_root", "domain\\user", "password", "description")
addUser('', 'tester', 'tester', '')
addUser('', 'tester2', 'tester2', '')
addUser("", "tester", "tester", "")
addUser("", "tester2", "tester2", "")
#addUser('temp', 'tester', 'tester', '')
#addUser('temp', 'tester2', 'tester2', '')
#addUser("temp", "tester", "tester", "")
#addUser("temp", "tester2", "tester2", "")
addUser('virtres', 'tester', 'tester', '')
#addUser("virtres", "tester", "tester", "")

View file

@ -0,0 +1,49 @@
# -*- coding: iso-8859-1 -*-
"""
server_sample
=============
:Author: Martin Wendt, moogle(at)wwwendt.de
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
Simple example how to a run WsgiDAV in a 3rd-party WSGI server.
See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
from tempfile import gettempdir
from wsgidav.fs_dav_provider import FilesystemProvider
from wsgidav.version import __version__
from wsgidav.wsgidav_app import DEFAULT_CONFIG, WsgiDAVApp
__docformat__ = "reStructuredText"
rootpath = gettempdir()
provider = FilesystemProvider(rootpath)
config = DEFAULT_CONFIG.copy()
config.update({
"provider_mapping": {"/": provider},
"user_mapping": {},
"verbose": 1,
"enable_loggers": [],
"propsmanager": None, # None: use property_manager.PropertyManager
"locksmanager": None, # None: use lock_manager.LockManager
"domaincontroller": None, # None: domain_controller.WsgiDAVDomainController(user_mapping)
})
app = WsgiDAVApp(config)
# For an example. use paste.httpserver
# (See http://pythonpaste.org/modules/httpserver.html for more options)
from paste import httpserver
httpserver.serve(app,
host="localhost",
port=8080,
server_version="WsgiDAV/%s" % __version__,
)
# Or use default the server that is part of the WsgiDAV package:
#from wsgidav.server import ext_wsgiutils_server
#ext_wsgiutils_server.serve(config, app)

View file

@ -4,8 +4,8 @@
util
====
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
:Author: Martin Wendt, moogle(at)wwwendt.de
:Author: Ho Chun Wei, fuzzybr80(at)gmail.com (author of original PyFileServer)
:Copyright: Lesser GNU Public License, see LICENSE file attached with package
Miscellaneous support functions for WsgiDAV.
@ -15,16 +15,19 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
"""
from urllib import quote
from lxml import etree
from lxml.etree import SubElement
from dav_error import DAVError, getHttpStatusString, HTTP_BAD_REQUEST
from pprint import pprint
from wsgidav.dav_error import HTTP_PRECONDITION_FAILED, HTTP_NOT_MODIFIED
import locale
import urllib
import logging
import re
import md5
try:
from hashlib import md5
except ImportError:
from md5 import md5
import os
import calendar
import threading
import sys
import time
import stat
@ -34,30 +37,42 @@ try:
except ImportError:
from StringIO import StringIO
__docformat__ = 'reStructuredText'
# Import XML support
useLxml = False
try:
from lxml import etree
useLxml = True
except ImportError:
try:
# Try xml module (Python 2.5 or later)
from xml.etree import ElementTree as etree
print "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/."
except ImportError:
try:
# Try elementtree (http://effbot.org/zone/element-index.htm)
from elementtree import ElementTree as etree
except ImportError:
print "ERROR: Could not import lxml, xml, nor elementtree. Consider installing lxml from http://codespeak.net/lxml/ or update to Python 2.5 or later."
raise
__docformat__ = "reStructuredText"
BASE_LOGGER_NAME = "wsgidav"
_logger = logging.getLogger(BASE_LOGGER_NAME)
#===============================================================================
# Debugging
# String handling
#===============================================================================
def traceCall(msg=None):
"""Return name of calling function."""
if __debug__:
f_code = sys._getframe(2).f_code
if msg is None:
msg = ": %s"
else: msg = ""
print "%s.%s #%s%s" % (f_code.co_filename, f_code.co_name, f_code.co_lineno, msg)
def isVerboseMode(environ):
if environ['wsgidav.verbose'] >= 1 and environ["REQUEST_METHOD"] in environ.get('wsgidav.debug_methods', []):
return True
return False
def getRfc1123Time(secs=None):
"""Return <secs> in rfc 1123 date/time format (pass secs=None for current date)."""
return time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(secs))
return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(secs))
def getLogTime(secs=None):
"""Return <secs> in log time format (pass secs=None for current date)."""
return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
def parseTimeString(timestring):
@ -101,31 +116,188 @@ def _parsegmtime(timestring):
return None
def log(msg, var=None):
out = sys.stderr
tid = threading._get_ident() #threading.currentThread()
print >>out, "<%s> %s" % (tid, msg)
if var:
pprint(var, out, indent=4)
#===============================================================================
# Logging
#===============================================================================
def initLogging(verbose=2, enable_loggers=[]):
"""Initialize base logger named 'wsgidav'.
The base logger is filtered by the *verbose* configuration option.
Log entries will have a time stamp and thread id.
:Parameters:
verbose : int
Verbosity configuration (0..3)
enable_loggers : string list
List of module logger names, that will be switched to DEBUG level.
Module loggers
~~~~~~~~~~~~~~
Module loggers (e.g 'wsgidav.lock_manager') are named loggers, that can be
independently switched to DEBUG mode.
def debug(cat, msg):
"""Print debug msessage to console (type filtered).
Except for verbosity, they will will inherit settings from the base logger.
They will suppress DEBUG level messages, unless they are enabled by passing
their name to util.initLogging().
If enabled, module loggers will print DEBUG messages, even if verbose == 2.
This is only a hack during early develpment: we can spread log calls and
use temporarily disable it by hard coding the filter condition here.
cat: 'pp' Paste Prune
'sc' "Socket closed" exception
Example initialize and use a module logger, that will generate output,
if enabled (and verbose >= 2):
.. python::
_logger = util.getModuleLogger(__name__)
[..]
_logger.debug("foo: '%s'" % s)
This logger would be enabled by passing its name to initLoggiong():
.. python::
enable_loggers = ["lock_manager",
"property_manager",
]
util.initLogging(2, enable_loggers)
Log level matrix
~~~~~~~~~~~~~~~~
======= =========== ====================== =======================
verbose Log level
------- -----------------------------------------------------------
n base logger module logger(enabled) module logger(disabled)
======= =========== ====================== =======================
0 ERROR ERROR ERROR
1 WARN WARN WARN
2 INFO DEBUG INFO
3 DEBUG DEBUG INFO
======= =========== ====================== =======================
"""
assert cat in ("pp", "sc")
tid = threading._get_ident() #threading.currentThread()
if cat in ("pp", "NOTsc"):
print >>sys.stderr, "<%s>[%s] %s" % (tid, cat, msg)
formatter = logging.Formatter("<%(thread)d> [%(asctime)s.%(msecs)d] %(name)s: %(message)s",
"%H:%M:%S")
# Define handlers
consoleHandler = logging.StreamHandler(sys.stderr)
consoleHandler.setFormatter(formatter)
consoleHandler.setLevel(logging.DEBUG)
# Add the handlers to the base logger
logger = logging.getLogger(BASE_LOGGER_NAME)
if verbose >= 3: # --debug
logger.setLevel(logging.DEBUG)
elif verbose >= 2: # --verbose
logger.setLevel(logging.INFO)
elif verbose >= 1: # standard
logger.setLevel(logging.WARN)
consoleHandler.setLevel(logging.WARN)
else: # --quiet
logger.setLevel(logging.ERROR)
consoleHandler.setLevel(logging.ERROR)
# Don't call the root's handlers after our custom handlers
logger.propagate = False
# Remove previous handlers
for hdlr in logger.handlers[:]: # Must iterate an array copy
try:
hdlr.flush()
hdlr.close()
except:
pass
logger.removeHandler(hdlr)
logger.addHandler(consoleHandler)
if verbose >= 2:
for e in enable_loggers:
if not e.startswith(BASE_LOGGER_NAME + "."):
e = BASE_LOGGER_NAME + "." + e
l = logging.getLogger(e.strip())
# if verbose >= 2:
# log("Logger(%s).setLevel(DEBUG)" % e.strip())
l.setLevel(logging.DEBUG)
def getModuleLogger(moduleName, defaultToVerbose=False):
"""Create a module logger, that can be en/disabled by configuration.
@see: unit.initLogging
"""
# _logger.debug("getModuleLogger(%s)" % moduleName)
if not moduleName.startswith(BASE_LOGGER_NAME + "."):
moduleName = BASE_LOGGER_NAME + "." + moduleName
# assert not "." in moduleName, "Only pass the module name, without leading '%s.'." % BASE_LOGGER_NAME
# logger = logging.getLogger("%s.%s" % (BASE_LOGGER_NAME, moduleName))
logger = logging.getLogger(moduleName)
if logger.level == logging.NOTSET and not defaultToVerbose:
logger.setLevel(logging.INFO) # Disable debug messages by default
return logger
def log(msg, var=None):
"""Shortcut for logging.getLogger('wsgidav').info(msg)
This message will only display, if verbose >= 2.
"""
_logger.info(msg)
if var and logging.INFO >= _logger.getEffectiveLevel():
pprint(var, sys.stderr, indent=4)
def debug(module, msg, var=None):
"""Shortcut for logging.getLogger('wsgidav.MODULE').debug(msg)
This message will only display, if the module logger was enabled and
verbose >= 2.
If module is None, the base logger is used, so the message is only displayed
if verbose >= 3.
"""
if module:
logger = logging.getLogger(BASE_LOGGER_NAME+"."+module)
# Disable debug messages for module loggers by default
if logger.level == logging.NOTSET:
logger.setLevel(logging.INFO)
else:
logger = _logger
logger.debug(msg)
if var and logging.DEBUG >= logger.getEffectiveLevel():
pprint(var, sys.stderr, indent=4)
def traceCall(msg=None):
"""Return name of calling function."""
if __debug__:
f_code = sys._getframe(2).f_code
if msg is None:
msg = ": %s"
else: msg = ""
print "%s.%s #%s%s" % (f_code.co_filename, f_code.co_name, f_code.co_lineno, msg)
def isVerboseMode(environ):
if environ["wsgidav.verbose"] >= 1 and environ["REQUEST_METHOD"] in environ.get("wsgidav.debug_methods", []):
return True
return False
#===============================================================================
# WSGI, strings and URLs
# Strings
#===============================================================================
def lstripstr(s, prefix, ignoreCase=False):
if ignoreCase:
if not s.lower().startswith(prefix.lower()):
return s
else:
if not s.startswith(prefix):
return s
return s[len(prefix):]
def saveSplit(s, sep, maxsplit):
"""Split string, always returning n-tuple (filled with None if necessary)."""
tok = s.split(sep, maxsplit)
@ -148,65 +320,149 @@ def splitNamespace(clarkName):
return ("", clarkName)
def getContentLength(environ):
"""Return CONTENT_LENGTH in a safe way (defaults to 0)."""
def toUnicode(s):
"""Convert a binary string to Unicode using UTF-8 (fallback to latin-1)."""
if not isinstance(s, str):
return s
try:
return max(0, long(environ.get('CONTENT_LENGTH', 0)))
u = s.decode("utf8")
# log("toUnicode(%r) = '%r'" % (s, u))
except:
log("toUnicode(%r) *** UTF-8 failed. Trying latin-1 " % s)
u = s.decode("latin-1")
return u
def stringRepr(s):
"""Return a string as hex dump."""
if isinstance(s, str):
res = "'%s': " % s
for b in s:
res += "%02x " % ord(b)
return res
return "%s" % s
def byteNumberString(number, thousandsSep=True, partition=False, base1024=True, appendBytes=True):
"""Convert bytes into human-readable representation."""
magsuffix = ""
bytesuffix = ""
if partition:
magnitude = 0
if base1024:
while number >= 1024:
magnitude += 1
number = number >> 10
else:
while number >= 1000:
magnitude += 1
number /= 1000.0
# TODO: use "9 KB" instead of "9K Bytes"?
# TODO use 'kibi' for base 1024?
# http://en.wikipedia.org/wiki/Kibi-#IEC_standard_prefixes
magsuffix = ["", "K", "M", "G", "T", "P"][magnitude]
if appendBytes:
if number == 1:
bytesuffix = " Byte"
else:
bytesuffix = " Bytes"
if thousandsSep and (number >= 1000 or magsuffix):
locale.setlocale(locale.LC_ALL, "")
# TODO: make precision configurable
snum = locale.format("%d", number, thousandsSep)
else:
snum = str(number)
return "%s%s%s" % (snum, magsuffix, bytesuffix)
#===============================================================================
# WSGI
#===============================================================================
def getContentLength(environ):
"""Return a positive CONTENT_LENGTH in a safe way (return 0 otherwise)."""
# TODO: http://www.wsgi.org/wsgi/WSGI_2.0
try:
return max(0, long(environ.get("CONTENT_LENGTH", 0)))
except ValueError:
return 0
def getRealm(environ):
return getUriRealm(environ["SCRIPT_NAME"] + environ["PATH_INFO"])
#===============================================================================
# URLs
#===============================================================================
def getUri(environ):
return environ["PATH_INFO"]
def getUriRealm(uri):
"""Return realm, i.e. first part of URI with a leading '/'."""
if uri.strip() in ("", "/"):
return "/"
return uri.strip("/").split("/")[0]
#def getUriRealm(uri):
# """Return realm, i.e. first part of URI with a leading '/'."""
# if uri.strip() in ("", "/"):
# return "/"
# return uri.strip("/").split("/")[0]
def getUriName(uri):
"""Return local name, i.e. last part of URI."""
"""Return local name, i.e. last segment of URI."""
return uri.strip("/").split("/")[-1]
def getUriParent(uri):
"""Return URI of parent collection."""
"""Return URI of parent collection with trailing '/', or None, if URI is top-level.
This function simply strips the last segment. It does not test, if the
target is a 'collection', or even exists.
"""
if not uri or uri.strip() == "/":
return None
return uri.rstrip("/").rsplit("/", 1)[0] + "/"
def isChildUri(parentUri, childUri):
"""Return True, if childUri is a child of parentUri.
This function accounts for the fact that '/a/b/c' and 'a/b/c/' are
children of '/a/b' (and also of '/a/b/').
Note that '/a/b/cd' is NOT a child of 'a/b/c'.
"""
return parentUri and childUri and childUri.rstrip("/").startswith(parentUri.rstrip("/")+"/")
def isEqualOrChildUri(parentUri, childUri):
"""Return True, if childUri is a child of parentUri or maps to the same resource.
Similar to <util.isChildUri>_ , but this method also returns True, if parent
equals child. ('/a/b' is considered identical with '/a/b/').
"""
return parentUri and childUri and (childUri.rstrip("/")+"/").startswith(parentUri.rstrip("/")+"/")
def makeCompleteUrl(environ, localUri=None):
"""URL reconstruction according to PEP 333.
@see http://www.python.org/dev/peps/pep-0333/#id33
"""
url = environ['wsgi.url_scheme']+'://'
url = environ["wsgi.url_scheme"]+"://"
if environ.get('HTTP_HOST'):
url += environ['HTTP_HOST']
if environ.get("HTTP_HOST"):
url += environ["HTTP_HOST"]
else:
url += environ['SERVER_NAME']
url += environ["SERVER_NAME"]
if environ['wsgi.url_scheme'] == 'https':
if environ['SERVER_PORT'] != '443':
url += ':' + environ['SERVER_PORT']
if environ["wsgi.url_scheme"] == "https":
if environ["SERVER_PORT"] != "443":
url += ":" + environ["SERVER_PORT"]
else:
if environ['SERVER_PORT'] != '80':
url += ':' + environ['SERVER_PORT']
if environ["SERVER_PORT"] != "80":
url += ":" + environ["SERVER_PORT"]
url += quote(environ.get('SCRIPT_NAME',''))
url += quote(environ.get("SCRIPT_NAME",""))
if localUri is None:
url += quote(environ.get('PATH_INFO',''))
if environ.get('QUERY_STRING'):
url += '?' + environ['QUERY_STRING']
url += quote(environ.get("PATH_INFO",""))
if environ.get("QUERY_STRING"):
url += "?" + environ["QUERY_STRING"]
else:
url += localUri # TODO: quote?
return url
@ -216,8 +472,37 @@ def makeCompleteUrl(environ, localUri=None):
# XML
#===============================================================================
def xmlToString(element, encoding="UTF-8", pretty_print=False):
"""Wrapper for etree.tostring, that takes care of unsupported pretty_print option."""
assert encoding == "UTF-8" # TODO: remove this
if useLxml:
return etree.tostring(element, encoding=encoding, pretty_print=pretty_print)
return etree.tostring(element, encoding)
def makeMultistatusEL():
"""Wrapper for etree.Element, that takes care of unsupported nsmap option."""
if useLxml:
return etree.Element("{DAV:}multistatus", nsmap={"D": "DAV:"})
return etree.Element("{DAV:}multistatus")
def makePropEL():
"""Wrapper for etree.Element, that takes care of unsupported nsmap option."""
if useLxml:
return etree.Element("{DAV:}prop", nsmap={"D": "DAV:"})
return etree.Element("{DAV:}prop")
def makeSubElement(parent, tag, nsmap=None):
"""Wrapper for etree.SubElement, that takes care of unsupported nsmap option."""
if useLxml:
return etree.SubElement(parent, tag, nsmap=nsmap)
return etree.SubElement(parent, tag)
def parseXmlBody(environ, allowEmpty=False):
"""Read request body XML into an lxml.etree.Element.
"""Read request body XML into an etree.Element.
Return None, if no request body was sent.
Raise HTTP_BAD_REQUEST, if something else went wrong.
@ -239,7 +524,8 @@ def parseXmlBody(environ, allowEmpty=False):
At least it locked, when I tried it with a request that had a missing
content-type and no body.
Current approach: if CONTENT_LENGTH is
Current approach: if CONTENT_LENGTH is
- valid and >0:
read body (exactly <CONTENT_LENGTH> bytes) and parse the result.
- 0:
@ -285,7 +571,7 @@ def parseXmlBody(environ, allowEmpty=False):
except Exception, e:
raise DAVError(HTTP_BAD_REQUEST, "Invalid XML format.", srcexception=e)
if environ['wsgidav.verbose'] >= 1 and environ["REQUEST_METHOD"] in environ.get('wsgidav.debug_methods', []):
if environ["wsgidav.verbose"] >= 1 and environ["REQUEST_METHOD"] in environ.get("wsgidav.debug_methods", []):
print "XML request for %s:\n%s" % (environ["REQUEST_METHOD"], etree.tostring(rootEL, pretty_print=True))
return rootEL
@ -301,7 +587,7 @@ def elementContentAsString(element):
return element.text or "" # Make sure, None is returned as ''
stream = StringIO()
for childnode in element:
print >>stream, etree.tostring(childnode, pretty_print=False)
print >>stream, xmlToString(childnode, pretty_print=False)
s = stream.getvalue()
stream.close()
return s
@ -309,15 +595,16 @@ def elementContentAsString(element):
def sendMultiStatusResponse(environ, start_response, multistatusEL):
# Send response
start_response('207 Multistatus', [("Content-Type", "application/xml"),
start_response("207 Multistatus", [("Content-Type", "application/xml"),
("Date", getRfc1123Time()),
])
# Hotfix for Windows XP: PPROPFIND XML response is not recognized, when
# pretty_print = True!
# pretty_print = True
# Hotfix for Windows XP
# PROPFIND XML response is not recognized, when pretty_print = True!
# (Vista and others would accept this).
# log(xmlToString(multistatusEL, pretty_print=True))
pretty_print = False
return ["<?xml version='1.0' ?>",
etree.tostring(multistatusEL, pretty_print=pretty_print) ]
return ["<?xml version='1.0' encoding='UTF-8' ?>",
xmlToString(multistatusEL, pretty_print=pretty_print) ]
def sendSimpleResponse(environ, start_response, status):
@ -334,10 +621,10 @@ def addPropertyResponse(multistatusEL, href, propList):
<prop> node depends on the value type:
- str or unicode: add element with this content
- None: add an empty element
- lxml.etree.Element: add XML element as child
- etree.Element: add XML element as child
- DAVError: add an empty element to an own <propstatus> for this status code
@param multistatusEL: lxml.etree.Element
@param multistatusEL: etree.Element
@param href: global URL of the resource, e.g. 'http://server:port/path'.
@param propList: list of 2-tuples (name, value)
"""
@ -365,26 +652,34 @@ def addPropertyResponse(multistatusEL, href, propList):
propDict.setdefault(status, []).append( (name, value) )
# <response>
responseEL = SubElement(multistatusEL, "{DAV:}response",
nsmap=nsMap)
SubElement(responseEL, "{DAV:}href").text = href
# responseEL = etree.SubElement(multistatusEL, "{DAV:}response",
# nsmap=nsMap)
responseEL = makeSubElement(multistatusEL, "{DAV:}response",
nsmap=nsMap)
# log("href value:%s" % (stringRepr(href)))
# etree.SubElement(responseEL, "{DAV:}href").text = toUnicode(href)
etree.SubElement(responseEL, "{DAV:}href").text = href
# etree.SubElement(responseEL, "{DAV:}href").text = urllib.quote(href, safe="/" + "!*'()," + "$-_|.")
# One <propstat> per status code
for status in propDict:
propstatEL = SubElement(responseEL, "{DAV:}propstat")
propstatEL = etree.SubElement(responseEL, "{DAV:}propstat")
# List of <prop>
propEL = SubElement(propstatEL, "{DAV:}prop")
propEL = etree.SubElement(propstatEL, "{DAV:}prop")
for name, value in propDict[status]:
if value is None:
SubElement(propEL, name)
etree.SubElement(propEL, name)
elif isinstance(value, etree._Element):
propEL.append(value)
else:
# value must be string or unicode
# log("%s value:%s" % (name, value))
SubElement(propEL, name).text = value
# log("%s value:%s" % (name, stringRepr(value)))
# etree.SubElement(propEL, name).text = value
etree.SubElement(propEL, name).text = toUnicode(value)
# <status>
SubElement(propstatEL, "{DAV:}status").text = "HTTP/1.1 %s" % status
etree.SubElement(propstatEL, "{DAV:}status").text = "HTTP/1.1 %s" % status
#===============================================================================
@ -396,18 +691,28 @@ def getETag(filePath):
http://www.webdav.org/specs/rfc4918.html#etag
Returns the following as entity tags::
Non-file - md5(pathname)
Win32 - md5(pathname)-lastmodifiedtime-filesize
Others - inode-lastmodifiedtime-filesize
"""
if not os.path.isfile(filePath):
return md5.new(filePath).hexdigest()
if sys.platform == 'win32':
statresults = os.stat(filePath)
return md5.new(filePath).hexdigest() + '-' + str(statresults[stat.ST_MTIME]) + '-' + str(statresults[stat.ST_SIZE])
# (At least on Vista) os.path.exists returns False, if a file name contains
# special characters, even if it is correctly UTF-8 encoded.
# So we convert to unicode. On the other hand, md5() needs a byte string.
if isinstance(filePath, unicode):
unicodeFilePath = filePath
filePath = filePath.encode("utf8")
else:
statresults = os.stat(filePath)
return str(statresults[stat.ST_INO]) + '-' + str(statresults[stat.ST_MTIME]) + '-' + str(statresults[stat.ST_SIZE])
unicodeFilePath = toUnicode(filePath)
if not os.path.isfile(unicodeFilePath):
return md5(filePath).hexdigest()
if sys.platform == "win32":
statresults = os.stat(unicodeFilePath)
return md5(filePath).hexdigest() + "-" + str(statresults[stat.ST_MTIME]) + "-" + str(statresults[stat.ST_SIZE])
else:
statresults = os.stat(unicodeFilePath)
return str(statresults[stat.ST_INO]) + "-" + str(statresults[stat.ST_MTIME]) + "-" + str(statresults[stat.ST_SIZE])
#===============================================================================
@ -420,12 +725,13 @@ reSuffixByteRangeSpecifier = re.compile("(\-([0-9]+))")
def obtainContentRanges(rangetext, filesize):
"""
returns tuple
list: content ranges as values to their parsed components in the tuple
(seek_position/abs position of first byte,
abs position of last byte,
num_of_bytes_to_read)
value: total length for Content-Length
returns tuple (list, value)
list
content ranges as values to their parsed components in the tuple
(seek_position/abs position of first byte, abs position of last byte, num_of_bytes_to_read)
value
total length for Content-Length
"""
listReturn = []
seqRanges = rangetext.split(",")
@ -436,7 +742,7 @@ def obtainContentRanges(rangetext, filesize):
if mObj:
# print mObj.group(0), mObj.group(1), mObj.group(2), mObj.group(3)
firstpos = long(mObj.group(2))
if mObj.group(3) == '':
if mObj.group(3) == "":
lastpos = filesize - 1
else:
lastpos = long(mObj.group(3))
@ -482,7 +788,7 @@ def obtainContentRanges(rangetext, filesize):
# If Headers
#===============================================================================
def evaluateHTTPConditionals(dav, path, lastmodified, entitytag, environ, isnewfile=False):
def evaluateHTTPConditionals(res, lastmodified, entitytag, environ):
"""Handle 'If-...:' headers (but not 'If:' header).
If-Match
@ -514,47 +820,53 @@ def evaluateHTTPConditionals(dav, path, lastmodified, entitytag, environ, isnewf
# status of 304 (Not Modified) unless doing so is consistent with all of the conditional header fields in
# the request.
if 'HTTP_IF_MATCH' in environ and dav.isInfoTypeSupported(path, "etag"):
if isnewfile:
raise DAVError(HTTP_PRECONDITION_FAILED)
else:
ifmatchlist = environ['HTTP_IF_MATCH'].split(",")
for ifmatchtag in ifmatchlist:
ifmatchtag = ifmatchtag.strip(" \"\t")
if ifmatchtag == entitytag or ifmatchtag == '*':
break
raise DAVError(HTTP_PRECONDITION_FAILED,
"If-Match header condition failed")
if "HTTP_IF_MATCH" in environ and res.supportEtag(): #dav.isInfoTypeSupported(path, "etag"):
ifmatchlist = environ["HTTP_IF_MATCH"].split(",")
for ifmatchtag in ifmatchlist:
ifmatchtag = ifmatchtag.strip(" \"\t")
if ifmatchtag == entitytag or ifmatchtag == "*":
break
raise DAVError(HTTP_PRECONDITION_FAILED,
"If-Match header condition failed")
# TODO: after the refactoring
ifModifiedSinceFailed = False
if "HTTP_IF_MODIFIED_SINCE" in environ and res.supportModified(): #dav.isInfoTypeSupported(path, "modified"):
ifmodtime = parseTimeString(environ["HTTP_IF_MODIFIED_SINCE"])
if ifmodtime and ifmodtime > lastmodified:
ifModifiedSinceFailed = True
# If-None-Match
# If none of the entity tags match, then the server MAY perform the requested method as if the
# If-None-Match header field did not exist, but MUST also ignore any If-Modified-Since header field
# (s) in the request. That is, if no entity tags match, then the server MUST NOT return a 304 (Not Modified)
# response.
ignoreifmodifiedsince = False
if 'HTTP_IF_NONE_MATCH' in environ and dav.isInfoTypeSupported(path, "etag"):
if isnewfile:
ignoreifmodifiedsince = True
else:
ifmatchlist = environ['HTTP_IF_NONE_MATCH'].split(",")
for ifmatchtag in ifmatchlist:
ifmatchtag = ifmatchtag.strip(" \"\t")
if ifmatchtag == entitytag or ifmatchtag == '*':
raise DAVError(HTTP_PRECONDITION_FAILED,
"If-None-Match header condition failed")
ignoreifmodifiedsince = True
ignoreIfModifiedSince = False
if "HTTP_IF_NONE_MATCH" in environ and res.supportEtag(): #dav.isInfoTypeSupported(path, "etag"):
ifmatchlist = environ["HTTP_IF_NONE_MATCH"].split(",")
for ifmatchtag in ifmatchlist:
ifmatchtag = ifmatchtag.strip(" \"\t")
if ifmatchtag == entitytag or ifmatchtag == "*":
# ETag matched. If it's a GET request and we don't have an
# conflicting If-Modified header, we return NOT_MODIFIED
if environ["REQUEST_METHOD"] in ("GET", "HEAD") and not ifModifiedSinceFailed:
raise DAVError(HTTP_NOT_MODIFIED,
"If-None-Match header failed")
raise DAVError(HTTP_PRECONDITION_FAILED,
"If-None-Match header condition failed")
ignoreIfModifiedSince = True
if not isnewfile and 'HTTP_IF_UNMODIFIED_SINCE' in environ and dav.isInfoTypeSupported(path, "modified"):
ifunmodtime = parseTimeString(environ['HTTP_IF_UNMODIFIED_SINCE'])
if "HTTP_IF_UNMODIFIED_SINCE" in environ and res.supportModified(): #dav.isInfoTypeSupported(path, "modified"):
ifunmodtime = parseTimeString(environ["HTTP_IF_UNMODIFIED_SINCE"])
if ifunmodtime and ifunmodtime <= lastmodified:
raise DAVError(HTTP_PRECONDITION_FAILED,
"If-Unmodified-Since header condition failed")
if not isnewfile and 'HTTP_IF_MODIFIED_SINCE' in environ and not ignoreifmodifiedsince and dav.isInfoTypeSupported(path, "modified"):
ifmodtime = parseTimeString(environ['HTTP_IF_MODIFIED_SINCE'])
if ifmodtime and ifmodtime > lastmodified:
raise DAVError(HTTP_NOT_MODIFIED,
"If-Modified-Since header condition failed")
if ifModifiedSinceFailed and not ignoreIfModifiedSince:
raise DAVError(HTTP_NOT_MODIFIED,
"If-Modified-Since header condition failed")
return
@ -566,7 +878,7 @@ reIfTagListContents = re.compile(r'(\S+)')
def parseIfHeaderDict(environ):
"""Parse HTTP_IF header into a dictionary and lists, and cache result.
"""Parse HTTP_IF header into a dictionary and lists, and cache the result.
@see http://www.webdav.org/specs/rfc2518.html#HEADER_If
"""
@ -574,32 +886,32 @@ def parseIfHeaderDict(environ):
return
if not "HTTP_IF" in environ:
environ['wsgidav.conditions.if'] = None
environ['wsgidav.ifLockTokenList'] = []
environ["wsgidav.conditions.if"] = None
environ["wsgidav.ifLockTokenList"] = []
return
iftext = environ["HTTP_IF"].strip()
if not iftext.startswith('<'):
iftext = '<*>' + iftext
if not iftext.startswith("<"):
iftext = "<*>" + iftext
ifDict = dict([])
ifLockList = []
resource1 = '*'
resource1 = "*"
for (tmpURLVar, URLVar, _tmpContentVar, contentVar) in reIfSeparator.findall(iftext):
if tmpURLVar != '':
if tmpURLVar != "":
resource1 = URLVar
else:
listTagContents = []
testflag = True
for listitem in reIfTagListContents.findall(contentVar):
if listitem.upper() != 'NOT':
if listitem.startswith('['):
listTagContents.append((testflag,'entity',listitem.strip('\"[]')))
if listitem.upper() != "NOT":
if listitem.startswith("["):
listTagContents.append((testflag,"entity",listitem.strip('\"[]')))
else:
listTagContents.append((testflag,'locktoken',listitem.strip('<>')))
listTagContents.append((testflag,"locktoken",listitem.strip("<>")))
ifLockList.append(listitem.strip("<>"))
testflag = listitem.upper() != 'NOT'
testflag = listitem.upper() != "NOT"
if resource1 in ifDict:
listTag = ifDict[resource1]
@ -608,60 +920,34 @@ def parseIfHeaderDict(environ):
ifDict[resource1] = listTag
listTag.append(listTagContents)
environ['wsgidav.conditions.if'] = ifDict
environ['wsgidav.ifLockTokenList'] = ifLockList
environ["wsgidav.conditions.if"] = ifDict
environ["wsgidav.ifLockTokenList"] = ifLockList
debug("if", "parseIfHeaderDict", ifDict)
return
#def _lookForLockTokenInSubDict(locktoken, listTest):
# for listTestConds in listTest:
# for (testflag, checkstyle, checkvalue) in listTestConds:
# if checkstyle == 'locktoken' and testflag:
# if locktoken == checkvalue:
# return True
# return False
#def testForLockTokenInIfHeaderDict(dictIf, locktoken, fullurl, headurl):
# if '*' in dictIf:
# if _lookForLockTokenInSubDict(locktoken, dictIf['*']):
# return True
#
# if fullurl in dictIf:
# if _lookForLockTokenInSubDict(locktoken, dictIf[fullurl]):
# return True
#
# if headurl in dictIf:
# if _lookForLockTokenInSubDict(locktoken, dictIf[headurl]):
# return True
def testIfHeaderDict(dav, path, dictIf, fullurl, locktokenlist, entitytag):
log("testIfHeaderDict(%s, %s, %s, %s)" % (path, fullurl, locktokenlist, entitytag),
dictIf)
def testIfHeaderDict(res, dictIf, fullurl, locktokenlist, entitytag):
debug("if", "testIfHeaderDict(%s, %s, %s)" % (fullurl, locktokenlist, entitytag),
dictIf)
if fullurl in dictIf:
listTest = dictIf[fullurl]
elif '*' in dictIf:
listTest = dictIf['*']
elif "*" in dictIf:
listTest = dictIf["*"]
else:
return True
supportEntityTag = dav.isInfoTypeSupported(path, "etag")
# supportEntityTag = dav.isInfoTypeSupported(path, "etag")
supportEntityTag = res.supportEtag()
for listTestConds in listTest:
matchfailed = False
for (testflag, checkstyle, checkvalue) in listTestConds:
if checkstyle == 'entity' and supportEntityTag:
if checkstyle == "entity" and supportEntityTag:
testresult = entitytag == checkvalue
elif checkstyle == 'entity':
elif checkstyle == "entity":
testresult = testflag
elif checkstyle == 'locktoken':
elif checkstyle == "locktoken":
testresult = checkvalue in locktokenlist
else: # unknown
testresult = True
@ -671,14 +957,41 @@ def testIfHeaderDict(dav, path, dictIf, fullurl, locktokenlist, entitytag):
break
if not matchfailed:
return True
debug("if", " -> FAILED")
return False
testIfHeaderDict.__test__ = False # Tell nose to ignore this function
#===============================================================================
# TEST
#===============================================================================
if __name__ == "__main__":
#n = etree.XML("<a><b/></a><c/>")
n = etree.XML("abc")
print etree.tostring(n)
def testLogging():
enable_loggers = ["test",
]
initLogging(0, enable_loggers)
pass
_baseLogger = logging.getLogger(BASE_LOGGER_NAME)
_enabledLogger = getModuleLogger("test")
_disabledLogger = getModuleLogger("test2")
_baseLogger.debug("_baseLogger.debug")
_baseLogger.info("_baseLogger.info")
_baseLogger.warning("_baseLogger.warning")
_baseLogger.error("_baseLogger.error")
print >>sys.stderr, ""
_enabledLogger.debug("_enabledLogger.debug")
_enabledLogger.info("_enabledLogger.info")
_enabledLogger.warning("_enabledLogger.warning")
_enabledLogger.error("_enabledLogger.error")
print >>sys.stderr, ""
_disabledLogger.debug("_disabledLogger.debug")
_disabledLogger.info("_disabledLogger.info")
_disabledLogger.warning("_disabledLogger.warning")
_disabledLogger.error("_disabledLogger.error")
if __name__ == "__main__":
testLogging()
pass

View file

@ -1,4 +1,7 @@
"""
Current WsgiDAV version number.
http://peak.telecommunity.com/DevCenter/setuptools#specifying-your-project-s-version
http://peak.telecommunity.com/DevCenter/setuptools#tagging-and-daily-build-or-snapshot-releases
"""
__version__ = "0.4.0.pre"
__version__ = "0.4.0b1"

View file

@ -12,6 +12,46 @@ wsgidav_app
WSGI container, that handles the HTTP requests. This object is passed to the
WSGI server and represents our WsgiDAV application to the outside.
Configuration
-------------
provider_mapping
Type: dictionary, default: {}
{shareName: DAVProvider,
}
user_mapping
Type: dictionary, default: {}
host
Type: str, default: 'localhost'
port
Type: int, default: 8080
ext_servers
Type: string list
enable_loggers
List
propsmanager
Default: None (use property_manager.PropertyManager)
locksmanager
Default: None (use lock_manager.LockManager)
domaincontroller
Default: None (use domain_controller.WsgiDAVDomainController(user_mapping))
verbose
Type: int, default: 2
0 no output (excepting application exceptions)
1 - show single line request summaries (for HTTP logging)
2 - show additional events
3 - show full request/response header info (HTTP Logging) request body and GET response bodies not shown
# HTTP Authentication Options
"acceptbasic": True, # Allow basic authentication, True or False
"acceptdigest": True, # Allow digest authentication, True or False
"defaultdigest": True, # True (default digest) or False (default basic)
# Organizational Information - printed as a footer on html output
"response_trailer": None,
See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
.. _DEVELOPERS.txt: http://wiki.wsgidav-dev.googlecode.com/hg/DEVELOPERS.html
@ -19,10 +59,11 @@ See DEVELOPERS.txt_ for more information about the WsgiDAV architecture.
from fs_dav_provider import FilesystemProvider
from wsgidav.dir_browser import WsgiDavDirBrowser
from wsgidav.dav_provider import DAVProvider
import time
import sys
import threading
import urllib
import util
import os
from error_printer import ErrorPrinter
from debug_filter import WsgiDavDebugFilter
from http_authenticator import HTTPAuthenticator
@ -34,7 +75,46 @@ from lock_manager import LockManager
__docformat__ = "reStructuredText"
_defaultOpts = {}
# Use these settings, if config file does not define them (or is totally missing)
DEFAULT_CONFIG = {
"mount_path": None, # Application root, e.g. <mount_path>/<share_name>/<res_path>
"provider_mapping": {},
"host": "localhost",
"port": 8080,
"ext_servers": [
# "paste",
# "cherrypy",
# "wsgiref",
"wsgidav",
],
"enable_loggers": [
],
"propsmanager": None, # None: use property_manager.PropertyManager
"locksmanager": None, # None: use lock_manager.LockManager
# HTTP Authentication Options
"user_mapping": {}, # dictionary of dictionaries
"domaincontroller": None, # None: domain_controller.WsgiDAVDomainController(user_mapping)
"acceptbasic": True, # Allow basic authentication, True or False
"acceptdigest": True, # Allow digest authentication, True or False
"defaultdigest": True, # True (default digest) or False (default basic)
# Verbose Output
"verbose": 2, # 0 - no output (excepting application exceptions)
# 1 - show single line request summaries (for HTTP logging)
# 2 - show additional events
# 3 - show full request/response header info (HTTP Logging)
# request body and GET response bodies not shown
# Organizational Information - printed as a footer on html output
"response_trailer": None,
}
def _checkConfig(config):
mandatoryFields = ["provider_mapping",
@ -44,37 +124,49 @@ def _checkConfig(config):
raise ValueError("Invalid configuration: missing required field '%s'" % field)
class WsgiDAVApp(object):
def __init__(self, config):
self.config = config
util.initLogging(config["verbose"], config.get("enable_loggers", []))
util.log("Default encoding: %s (file system: %s)" % (sys.getdefaultencoding(), sys.getfilesystemencoding()))
# Evaluate configuration and set defaults
_checkConfig(config)
provider_mapping = self.config["provider_mapping"]
response_trailer = config.get("response_trailer", "")
self._verbose = config.get("verbose", 2)
locksmanager = config.get("locksmanager")
if not locksmanager:
locksfile = config.get("locksfile") or "wsgidav-locks.shelve"
locksfile = os.path.abspath(locksfile)
locksmanager = LockManager(locksfile)
locksManager = config.get("locksmanager")
if not locksManager:
locksManager = LockManager()
propsmanager = config.get("propsmanager")
if not propsmanager:
propsfile = config.get("propsfile") or "wsgidav-props.shelve"
propsfile = os.path.abspath(propsfile)
propsmanager = PropertyManager(propsfile)
propsManager = config.get("propsmanager")
if not propsManager:
propsManager = PropertyManager()
mount_path = config.get("mount_path")
user_mapping = self.config.get("user_mapping", {})
domaincontrollerobj = config.get("domaincontroller") or WsgiDAVDomainController(user_mapping)
domainController = config.get("domaincontroller") or WsgiDAVDomainController(user_mapping)
isDefaultDC = isinstance(domainController, WsgiDAVDomainController)
# authentication fields
authacceptbasic = config.get("acceptbasic", False)
authacceptbasic = config.get("acceptbasic", True)
authacceptdigest = config.get("acceptdigest", True)
authdefaultdigest = config.get("defaultdigest", True)
# Check configuration for NTDomainController
# We don't use 'isinstance', because include would fail on non-windows boxes.
wdcName = "NTDomainController"
if domainController.__class__.__name__ == wdcName:
if authacceptdigest or authdefaultdigest or not authacceptbasic:
print >>sys.stderr, "WARNING: %s requires basic authentication.\n\tSet acceptbasic=True, acceptdigest=False, defaultdigest=False" % wdcName
# Instantiate DAV resource provider objects for every share
self.providerMap = {}
for (share, provider) in provider_mapping.items():
@ -89,30 +181,39 @@ class WsgiDAVApp(object):
assert isinstance(provider, DAVProvider)
provider.setSharePath(share)
if mount_path:
provider.setMountPath(mount_path)
# TODO: someday we may want to configure different lock/prop managers per provider
provider.setLockManager(locksmanager)
provider.setPropManager(propsmanager)
provider.setLockManager(locksManager)
provider.setPropManager(propsManager)
self.providerMap[share] = provider
if self._verbose >= 2:
print "Using lock manager: %s" % locksManager
print "Using property manager: %s" % propsManager
print "Using domain controller: %s" % domainController
print "Registered DAV providers:"
for k, v in self.providerMap.items():
for share, provider in self.providerMap.items():
hint = ""
if not user_mapping.get(share):
hint = "Anonymous!"
print " Share '%s': %s%s" % (k, v, hint)
if self._verbose >= 1:
if isDefaultDC and not user_mapping.get(share):
hint = " Anonymous!"
print " Share '%s': %s%s" % (share, provider, hint)
# If the default DC is used, emit a warning for anonymous realms
if isDefaultDC and self._verbose >= 1:
for share in self.providerMap:
if not user_mapping.get(share):
# TODO: we should only warn here, if --no-auth is not given
print "WARNING: share '%s' will allow anonymous access." % share
# Define WSGI application stack
application = RequestResolver()
application = WsgiDavDirBrowser(application)
application = HTTPAuthenticator(application,
domaincontrollerobj,
domainController,
authacceptbasic,
authacceptdigest,
authdefaultdigest)
@ -127,13 +228,8 @@ class WsgiDAVApp(object):
def __call__(self, environ, start_response):
print >> environ["wsgi.errors"], "%s SCRIPT_NAME:'%s', PATH_INFO:'%s', %s\n %s" % (
environ.get("REQUEST_METHOD"),
environ.get("SCRIPT_NAME"),
environ.get("PATH_INFO"),
environ.get("REMOTE_USER"),
environ.get("HTTP_AUTHORIZATION"),
)
util.log("SCRIPT_NAME='%s', PATH_INFO='%s'" % (environ.get("SCRIPT_NAME"), environ.get("PATH_INFO")))
# We unquote PATH_INFO here, although this should already be done by
# the server.
path = urllib.unquote(environ["PATH_INFO"])
@ -157,29 +253,27 @@ class WsgiDAVApp(object):
share = r
break
elif path.upper() == r.upper() or path.upper().startswith(r.upper()+"/"):
# print path, ":->" , r
share = r
break
provider = self.providerMap.get(share)
# Note: we call the next app, even if provider is None, because OPTIONS
# must still be handled
# must still be handled.
# All other requests will result in '404 Not Found'
environ["wsgidav.provider"] = provider
# TODO: test with multi-level realms: 'aa/bb'
# TODO: test security: url contains '..'
# TODO: SCRIPT_NAME could already contain <approot>
# See @@
# Transform SCRIPT_NAME and PATH_INFO
# (Since path and share are unquoted, this also fixes quoted values.)
# (Since path and share are unquoted, this also fixes quoted values.)
if share == "/" or not share:
environ["SCRIPT_NAME"] = ""
environ["PATH_INFO"] = path
else:
environ["SCRIPT_NAME"] = share
environ["SCRIPT_NAME"] += share
environ["PATH_INFO"] = path[len(share):]
# util.log("--> SCRIPT_NAME='%s', PATH_INFO='%s'" % (environ.get("SCRIPT_NAME"), environ.get("PATH_INFO")))
# See http://mail.python.org/pipermail/web-sig/2007-January/002475.html
# for some clarification about SCRIPT_NAME/PATH_INFO format
@ -189,20 +283,49 @@ class WsgiDAVApp(object):
assert environ["SCRIPT_NAME"] in ("", "/") or not environ["SCRIPT_NAME"].endswith("/")
# PATH_INFO starts with '/'
assert environ["PATH_INFO"] == "" or environ["PATH_INFO"].startswith("/")
# Log HTTP request
if self._verbose >= 1:
threadInfo = ""
userInfo = ""
if self._verbose >= 2:
threadInfo = "<%s>" % threading._get_ident() #.currentThread())
if not environ.get("HTTP_AUTHORIZATION"):
start_time = time.time()
def _start_response_wrapper(status, response_headers, exc_info=None):
# Log request
if self._verbose >= 1:
threadInfo = ""
userInfo = environ.get("http_authenticator.username")
if not userInfo:
userInfo = "(anonymous)"
if self._verbose >= 1:
threadInfo = "<%s> " % threading._get_ident()
extra = []
if environ.get("CONTENT_LENGTH", "") != "":
extra.append("length=%s" % environ.get("CONTENT_LENGTH"))
if "HTTP_DEPTH" in environ:
extra.append("depth=%s" % environ.get("HTTP_DEPTH"))
if "HTTP_OVERWRITE" in environ:
extra.append("overwrite=%s" % environ.get("HTTP_OVERWRITE"))
if "HTTP_DESTINATION" in environ:
extra.append('dest="%s"' % environ.get("HTTP_DESTINATION"))
if self._verbose >= 2 and "HTTP_USER_AGENT" in environ:
extra.append('agent="%s"' % environ.get("HTTP_USER_AGENT"))
if self._verbose >= 1:
extra.append('elap=%.3fsec' % (time.time() - start_time))
extra = ", ".join(extra)
print >> environ["wsgi.errors"], "[", util.getRfc1123Time(),"] from ", environ.get("REMOTE_ADDR","unknown"), " ", threadInfo, environ.get("REQUEST_METHOD","unknown"), " ", environ.get("PATH_INFO","unknown"), " ", environ.get("HTTP_DESTINATION", ""), userInfo
# This is the CherryPy format:
# 127.0.0.1 - - [08/Jul/2009:17:25:23] "GET /loginPrompt?redirect=/renderActionList%3Frelation%3Dpersonal%26key%3D%26filter%3DprivateSchedule&reason=0 HTTP/1.1" 200 1944 "http://127.0.0.1:8002/command?id=CMD_Schedule" "Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1) Gecko/20090624 Firefox/3.5"
print >>sys.stderr, '%s - %s - [%s] "%s" %s -> %s' % (
threadInfo + environ.get("REMOTE_ADDR",""),
userInfo,
util.getLogTime(),
environ.get("REQUEST_METHOD") + " " + environ.get("PATH_INFO", ""),
extra,
status,
# response Content-Length
# referer
)
return start_response(status, response_headers, exc_info)
# Call next middleware
for v in self._application(environ, start_response):
# for v in self._application(environ, start_response):
for v in self._application(environ, _start_response_wrapper):
util.debug("sc", "WsgiDAVApp: yield start")
yield v
util.debug("sc", "WsgiDAVApp: yield end")