# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
#
# This file is part of GNU Mailman.
#
# GNU Mailman is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see .
"""Provide a password-interface wrapper around private archives."""
import os
import sys
import cgi
import logging
import mimetypes
from Mailman import Errors
from Mailman import MailList
from Mailman import Utils
from Mailman import i18n
from Mailman.configuration import config
from Mailman.htmlformat import *
# Set up i18n. Until we know which list is being requested, we use the
# server's default.
_ = i18n._
i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
SLASH = '/'
log = logging.getLogger('mailman.error')
mlog = logging.getLogger('mailman.mischief')
def true_path(path):
"Ensure that the path is safe by removing .."
# Workaround for path traverse vulnerability. Unsuccessful attempts will
# be logged in logs/error.
parts = [x for x in path.split(SLASH) if x not in ('.', '..')]
return SLASH.join(parts)[1:]
def guess_type(url, strict):
if hasattr(mimetypes, 'common_types'):
return mimetypes.guess_type(url, strict)
return mimetypes.guess_type(url)
def main():
doc = Document()
doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
parts = Utils.GetPathPieces()
if not parts:
doc.SetTitle(_("Private Archive Error"))
doc.AddItem(Header(3, _("You must specify a list.")))
print doc.Format()
return
path = os.environ.get('PATH_INFO')
tpath = true_path(path)
if tpath <> path[1:]:
msg = _('Private archive - "./" and "../" not allowed in URL.')
doc.SetTitle(msg)
doc.AddItem(Header(2, msg))
print doc.Format()
mlog.error('Private archive hostile path: %s', path)
return
# BAW: This needs to be converted to the Site module abstraction
true_filename = os.path.join(
config.PRIVATE_ARCHIVE_FILE_DIR, tpath)
listname = parts[0].lower()
mboxfile = ''
if len(parts) > 1:
mboxfile = parts[1]
# See if it's the list's mbox file is being requested
if listname.endswith('.mbox') and mboxfile.endswith('.mbox') and \
listname[:-5] == mboxfile[:-5]:
listname = listname[:-5]
else:
mboxfile = ''
# If it's a directory, we have to append index.html in this script. We
# must also check for a gzipped file, because the text archives are
# usually stored in compressed form.
if os.path.isdir(true_filename):
true_filename = true_filename + '/index.html'
if not os.path.exists(true_filename) and \
os.path.exists(true_filename + '.gz'):
true_filename = true_filename + '.gz'
try:
mlist = MailList.MailList(listname, lock=0)
except Errors.MMListError, e:
# Avoid cross-site scripting attacks
safelistname = Utils.websafe(listname)
msg = _('No such list %(safelistname)s')
doc.SetTitle(_("Private Archive Error - %(msg)s"))
doc.AddItem(Header(2, msg))
print doc.Format()
log.error('No such list "%s": %s\n', listname, e)
return
i18n.set_language(mlist.preferred_language)
doc.set_language(mlist.preferred_language)
cgidata = cgi.FieldStorage()
username = cgidata.getvalue('username', '')
password = cgidata.getvalue('password', '')
is_auth = 0
realname = mlist.real_name
message = ''
if not mlist.WebAuthenticate((config.AuthUser,
config.AuthListModerator,
config.AuthListAdmin,
config.AuthSiteAdmin),
password, username):
if cgidata.has_key('submit'):
# This is a re-authorization attempt
message = Bold(FontSize('+1', _('Authorization failed.'))).Format()
# Output the password form
charset = Utils.GetCharSet(mlist.preferred_language)
print 'Content-type: text/html; charset=' + charset + '\n\n'
# Put the original full path in the authorization form, but avoid
# trailing slash if we're not adding parts. We add it below.
action = mlist.GetScriptURL('private')
if parts[1:]:
action = os.path.join(action, SLASH.join(parts[1:]))
# If we added '/index.html' to true_filename, add a slash to the URL.
# We need this because we no longer add the trailing slash in the
# private.html template. It's always OK to test parts[-1] since we've
# already verified parts[0] is listname. The basic rule is if the
# post URL (action) is a directory, it must be slash terminated, but
# not if it's a file. Otherwise, relative links in the target archive
# page don't work.
if true_filename.endswith('/index.html') and parts[-1] <> 'index.html':
action += SLASH
# Escape web input parameter to avoid cross-site scripting.
print Utils.maketext(
'private.html',
{'action' : Utils.websafe(action),
'realname': mlist.real_name,
'message' : message,
}, mlist=mlist)
return
lang = mlist.getMemberLanguage(username)
i18n.set_language(lang)
doc.set_language(lang)
# Authorization confirmed... output the desired file
try:
ctype, enc = guess_type(path, strict=0)
if ctype is None:
ctype = 'text/html'
if mboxfile:
f = open(os.path.join(mlist.archive_dir() + '.mbox',
mlist.internal_name() + '.mbox'))
ctype = 'text/plain'
elif true_filename.endswith('.gz'):
import gzip
f = gzip.open(true_filename, 'r')
else:
f = open(true_filename, 'r')
except IOError:
msg = _('Private archive file not found')
doc.SetTitle(msg)
doc.AddItem(Header(2, msg))
print doc.Format()
log.error('Private archive file not found: %s', true_filename)
else:
print 'Content-type: %s\n' % ctype
sys.stdout.write(f.read())
f.close()