# Copyright (C) 1998-2018 by the Free Software Foundation, Inc. # # This program 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 2 # of the License, or (at your option) any later version. # # This program 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 this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. """Provide a password-interface wrapper around private archives.""" import os import sys import cgi import mimetypes from Mailman import mm_cfg from Mailman import Utils from Mailman import MailList from Mailman import Errors from Mailman import i18n from Mailman.htmlformat import * from Mailman.Logging.Syslog import syslog # Set up i18n. Until we know which list is being requested, we use the # server's default. _ = i18n._ i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) SLASH = '/' 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(mm_cfg.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() syslog('mischief', 'Private archive hostile path: %s', path) return # BAW: This needs to be converted to the Site module abstraction true_filename = os.path.join( mm_cfg.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)) # Send this with a 404 status. print 'Status: 404 Not Found' print doc.Format() syslog('error', 'private: No such list "%s": %s\n', listname, e) return i18n.set_language(mlist.preferred_language) doc.set_language(mlist.preferred_language) cgidata = cgi.FieldStorage() try: username = cgidata.getfirst('username', '').strip() except TypeError: # Someone crafted a POST with a bad Content-Type:. doc.AddItem(Header(2, _("Error"))) doc.AddItem(Bold(_('Invalid options to CGI script.'))) # Send this with a 400 status. print 'Status: 400 Bad Request' print doc.Format() return password = cgidata.getfirst('password', '') is_auth = 0 realname = mlist.real_name message = '' if not mlist.WebAuthenticate((mm_cfg.AuthUser, mm_cfg.AuthListModerator, mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin), password, username): if cgidata.has_key('submit'): # This is a re-authorization attempt message = Bold(FontSize('+1', _('Authorization failed.'))).Format() remote = os.environ.get('HTTP_FORWARDED_FOR', os.environ.get('HTTP_X_FORWARDED_FOR', os.environ.get('REMOTE_ADDR', 'unidentified origin'))) syslog('security', 'Authorization failed (private): user=%s: list=%s: remote=%s', username, listname, remote) # give an HTTP 401 for authentication failure print 'Status: 401 Unauthorized' # Are we processing a password reminder from the login screen? if cgidata.has_key('login-remind'): if username: message = Bold(FontSize('+1', _("""If you are a list member, your password has been emailed to you."""))).Format() else: message = Bold(FontSize('+1', _('Please enter your email address'))).Format() if mlist.isMember(username): mlist.MailUserPassword(username) elif username: # Not a member if mlist.private_roster == 0: # Public rosters safeuser = Utils.websafe(username) message = Bold(FontSize('+1', _('No such member: %(safeuser)s.'))).Format() else: syslog('mischief', 'Reminder attempt of non-member w/ private rosters: %s', username) # 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', absolute=1) if mboxfile: action += '.mbox' 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 'Status: 404 Not Found' print doc.Format() syslog('error', 'Private archive file not found: %s', true_filename) else: print 'Content-type: %s\n' % ctype sys.stdout.write(f.read()) f.close()