def pend_new(self, op, *content, **kws): """Create a new entry in the pending database, returning cookie for it. """ assert op in _ALLKEYS, 'op: %s' % op lifetime = kws.get('lifetime', mm_cfg.PENDING_REQUEST_LIFE) # We try the main loop several times. If we get a lock error somewhere # (for instance because someone broke the lock) we simply try again. assert self.Locked() # Load the database db = self.__load() # Calculate a unique cookie. Algorithm vetted by the Timbot. time() # has high resolution on Linux, clock() on Windows. random gives us # about 45 bits in Python 2.2, 53 bits on Python 2.3. The time and # clock values basically help obscure the random number generator, as # does the hash calculation. The integral parts of the time values # are discarded because they're the most predictable bits. while True: now = time.time() x = random.random() + now % 1.0 + time.clock() % 1.0 cookie = sha_new(repr(x)).hexdigest() # We'll never get a duplicate, but we'll be anal about checking # anyway. if not db.has_key(cookie): break # Store the content, plus the time in the future when this entry will # be evicted from the database, due to staleness. db[cookie] = (op,) + content evictions = db.setdefault('evictions', {}) evictions[cookie] = now + lifetime self.__save(db) return cookie
def pend_new(self, op, *content, **kws): """Create a new entry in the pending database, returning cookie for it. """ assert op in _ALLKEYS, 'op: %s' % op lifetime = kws.get('lifetime', mm_cfg.PENDING_REQUEST_LIFE) # We try the main loop several times. If we get a lock error somewhere # (for instance because someone broke the lock) we simply try again. assert self.Locked() # Load the database db = self.__load() # Calculate a unique cookie. Algorithm vetted by the Timbot. time() # has high resolution on Linux, clock() on Windows. random gives us # about 45 bits in Python 2.2, 53 bits on Python 2.3. The time and # clock values basically help obscure the random number generator, as # does the hash calculation. The integral parts of the time values # are discarded because they're the most predictable bits. while True: now = time.time() x = random.random() + now % 1.0 + time.clock() % 1.0 cookie = sha_new(repr(x)).hexdigest() # We'll never get a duplicate, but we'll be anal about checking # anyway. if cookie not in db: break # Store the content, plus the time in the future when this entry will # be evicted from the database, due to staleness. db[cookie] = (op, ) + content evictions = db.setdefault('evictions', {}) evictions[cookie] = now + lifetime self.__save(db) return cookie
def __checkone(self, c, authcontext, user): # Do the guts of the cookie check, for one authcontext/user # combination. try: key, secret = self.AuthContextInfo(authcontext, user) except Errors.NotAMemberError: return False if not c.has_key(key) or not isinstance(secret, StringType): return False # Undo the encoding we performed in MakeCookie() above. BAW: I # believe this is safe from exploit because marshal can't be forced to # load recursive data structures, and it can't be forced to execute # any unexpected code. The worst that can happen is that either the # client will have provided us bogus data, in which case we'll get one # of the caught exceptions, or marshal format will have changed, in # which case, the cookie decoding will fail. In either case, we'll # simply request reauthorization, resulting in a new cookie being # returned to the client. try: data = marshal.loads(binascii.unhexlify(c[key])) issued, received_mac = data except (EOFError, ValueError, TypeError, KeyError): return False # Make sure the issued timestamp makes sense now = time.time() if now < issued: return False # Calculate what the mac ought to be based on the cookie's timestamp # and the shared secret. mac = sha_new(secret + `issued`).hexdigest() if mac <> received_mac: return False # Authenticated! return True
def MakeCookie(self, authcontext, user=None): key, secret = self.AuthContextInfo(authcontext, user) if key is None or secret is None or not isinstance(secret, StringType): raise ValueError # Timestamp issued = int(time.time()) # Get a digest of the secret, plus other information. mac = sha_new(secret + `issued`).hexdigest() # Create the cookie object. c = Cookie.SimpleCookie() c[key] = binascii.hexlify(marshal.dumps((issued, mac))) # The path to all Mailman stuff, minus the scheme and host, # i.e. usually the string `/mailman' parsed = urlparse(self.web_page_url) path = parsed[2] is_cpanel = os.environ.get('CPANEL'); if is_cpanel: path = '/3rdparty' + path cp_security_token = os.environ.get('cp_security_token') if cp_security_token: path = cp_security_token + path c[key]['path'] = path # Make sure to set the 'secure' flag on the cookie if mailman is # accessed by an https url. if parsed[0] == 'https': c[key]['secure'] = True # We use session cookies, so don't set `expires' or `max-age' keys. # Set the RFC 2109 required header. c[key]['version'] = 1 return c
def calculate_attachments_dir(mlist, msg, msgdata): # Calculate the directory that attachments for this message will go # under. To avoid inode limitations, the scheme will be: # archives/private/<listname>/attachments/YYYYMMDD/<msgid-hash>/<files> # Start by calculating the date-based and msgid-hash components. fmt = "%Y%m%d" datestr = msg.get("Date") if datestr: now = parsedate(datestr) else: now = time.gmtime(msgdata.get("received_time", time.time())) datedir = safe_strftime(fmt, now) if not datedir: datestr = msgdata.get("X-List-Received-Date") if datestr: datedir = safe_strftime(fmt, datestr) if not datedir: # What next? Unixfrom, I guess. parts = msg.get_unixfrom().split() try: month = { "Jan": 1, "Feb": 2, "Mar": 3, "Apr": 4, "May": 5, "Jun": 6, "Jul": 7, "Aug": 8, "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12, }.get(parts[3], 0) day = int(parts[4]) year = int(parts[6]) except (IndexError, ValueError): # Best we can do I think month = day = year = 0 datedir = "%04d%02d%02d" % (year, month, day) assert datedir if mm_cfg.SCRUBBER_ADD_PAYLOAD_HASH_FILENAME: return os.path.join("attachments", datedir) else: # As for the msgid hash, we'll base this part on the Message-ID: so that # all attachments for the same message end up in the same directory (we'll # uniquify the filenames in that directory as needed). We use the first 2 # and last 2 bytes of the SHA1 hash of the message id as the basis of the # directory name. Clashes here don't really matter too much, and that # still gives us a 32-bit space to work with. msgid = msg["message-id"] if msgid is None: msgid = msg["Message-ID"] = Utils.unique_message_id(mlist) # We assume that the message id actually /is/ unique! digest = sha_new(msgid).hexdigest() return os.path.join("attachments", datedir, digest[:4] + digest[-4:])
def _seed(): try: fp = open('/dev/random') d = fp.read(40) fp.close() except EnvironmentError, e: if e.errno <> errno.ENOENT: raise from Mailman.Utils import sha_new d = sha_new(`os.getpid()`+`time.time()`).hexdigest()
def calculate_attachments_dir(mlist, msg, msgdata): # Calculate the directory that attachments for this message will go # under. To avoid inode limitations, the scheme will be: # archives/private/<listname>/attachments/YYYYMMDD/<msgid-hash>/<files> # Start by calculating the date-based and msgid-hash components. fmt = '%Y%m%d' datestr = msg.get('Date') if datestr: now = parsedate(datestr) else: now = time.gmtime(msgdata.get('received_time', time.time())) datedir = safe_strftime(fmt, now) if not datedir: datestr = msgdata.get('X-List-Received-Date') if datestr: datedir = safe_strftime(fmt, datestr) if not datedir: # What next? Unixfrom, I guess. parts = msg.get_unixfrom().split() try: month = { 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12, }.get(parts[3], 0) day = int(parts[4]) year = int(parts[6]) except (IndexError, ValueError): # Best we can do I think month = day = year = 0 datedir = '%04d%02d%02d' % (year, month, day) assert datedir # As for the msgid hash, we'll base this part on the Message-ID: so that # all attachments for the same message end up in the same directory (we'll # uniquify the filenames in that directory as needed). We use the first 2 # and last 2 bytes of the SHA1 hash of the message id as the basis of the # directory name. Clashes here don't really matter too much, and that # still gives us a 32-bit space to work with. msgid = msg['message-id'] if msgid is None: msgid = msg['Message-ID'] = Utils.unique_message_id(mlist) # We assume that the message id actually /is/ unique! digest = sha_new(msgid).hexdigest() # hash disabled to handle file duplicate over mutiple email. #return os.path.join('attachments', datedir, digest[:4] + digest[-4:]) return os.path.join('attachments', datedir)
def _seed(): try: fp = open('/dev/random') d = fp.read(40) fp.close() except EnvironmentError as e: if e.errno != errno.ENOENT: raise from Mailman.Utils import sha_new d = sha_new(repr(os.getpid()) + repr(time.time())).hexdigest() random.seed(d)
def csrf_token(mlist, contexts, user=None): """ create token by mailman cookie generation algorithm """ for context in contexts: key, secret = mlist.AuthContextInfo(context, user) if key: break else: return None # not authenticated issued = int(time.time()) mac = sha_new(secret + `issued`).hexdigest() keymac = '%s:%s' % (key, mac) token = binascii.hexlify(marshal.dumps((issued, keymac))) return token
def csrf_token(mlist, contexts, user=None): """ create token by mailman cookie generation algorithm """ for context in contexts: key, secret = mlist.AuthContextInfo(context, user) if key: break else: return None # not authenticated issued = int(time.time()) mac = sha_new(secret + repr(issued)).hexdigest() keymac = '%s:%s' % (key, mac) token = binascii.hexlify(marshal.dumps((issued, keymac))) return token
def enqueue(self, _msg, _metadata={}, **_kws): # Calculate the SHA hexdigest of the message to get a unique base # filename. We're also going to use the digest as a hash into the set # of parallel qrunner processes. data = _metadata.copy() data.update(_kws) listname = data.get('listname', '--nolist--') # Get some data for the input to the sha hash now = time.time() if SAVE_MSGS_AS_PICKLES and not data.get('_plaintext'): protocol = 1 msgsave = cPickle.dumps(_msg, protocol) else: protocol = 0 msgsave = cPickle.dumps(str(_msg), protocol) hashfood = msgsave + listname + `now` # Encode the current time into the file name for FIFO sorting in # files(). The file name consists of two parts separated by a `+': # the received time for this message (i.e. when it first showed up on # this system) and the sha hex digest. #rcvtime = data.setdefault('received_time', now) rcvtime = data.setdefault('received_time', now) filebase = `rcvtime` + '+' + sha_new(hashfood).hexdigest() filename = os.path.join(self.__whichq, filebase + '.pck') tmpfile = filename + '.tmp' # Always add the metadata schema version number data['version'] = mm_cfg.QFILE_SCHEMA_VERSION # Filter out volatile entries for k in data.keys(): if k.startswith('_'): del data[k] # We have to tell the dequeue() method whether to parse the message # object or not. data['_parsemsg'] = (protocol == 0) # Write to the pickle file the message object and metadata. omask = os.umask(007) # -rw-rw---- try: fp = open(tmpfile, 'w') try: fp.write(msgsave) cPickle.dump(data, fp, protocol) fp.flush() os.fsync(fp.fileno()) finally: fp.close() finally: os.umask(omask) os.rename(tmpfile, filename) return filebase
def request_create (self): """Creates a list (name taken from the CGI form value called lc_name). Returns None if the list was created successfully. Returns a string containing error message if list could not be created for whatever reason.""" self._ml = MailList.MailList() err = self.errcheck(action='create') if err: return err # We've got all the data we need, so go ahead and try to create the # list See admin.py for why we need to set up the signal handler. try: signal.signal(signal.SIGTERM, self.sigterm_handler) pwhex = sha_new(self.pw).hexdigest() # Guarantee that all newly created files have the proper permission. # proper group ownership should be assured by the autoconf script # enforcing that all directories have the group sticky bit set oldmask = os.umask(002) try: try: self.ml.Create(self.ln, self.owner[0], pwhex, self.langs, self.eh, urlhost=self.hn) finally: os.umask(oldmask) except Errors.EmailAddressError, e: if e.args: s = Utils.websafe(e.args[0]) else: s = Utils.websafe(owner) return 'Bad owner email address: %s' % s except Errors.MMListAlreadyExistsError: return 'List already exists: %s' % self.ln except Errors.BadListNameError, e: if e.args: s = Utils.websafe(e.args[0]) else: s = Utils.websafe(listname) return 'Illegal list name: %s' % self.ln
def MakeCookie(self, authcontext, user=None): key, secret = self.AuthContextInfo(authcontext, user) if key is None or secret is None or not isinstance(secret, StringType): raise ValueError # Timestamp issued = int(time.time()) # Get a digest of the secret, plus other information. mac = sha_new(secret + `issued`).hexdigest() # Create the cookie object. c = Cookie.SimpleCookie() c[key] = binascii.hexlify(marshal.dumps((issued, mac))) # The path to all Mailman stuff, minus the scheme and host, # i.e. usually the string `/mailman' path = urlparse(self.web_page_url)[2] c[key]['path'] = path # We use session cookies, so don't set `expires' or `max-age' keys. # Set the RFC 2109 required header. c[key]['version'] = 1 return c
def calculate_attachments_dir(mlist, msg, msgdata): # Calculate the directory that attachments for this message will go # under. To avoid inode limitations, the scheme will be: # archives/private/<listname>/attachments/YYYYMMDD/<msgid-hash>/<files> # Start by calculating the date-based and msgid-hash components. fmt = '%Y%m%d' datestr = msg.get('Date') if datestr: now = parsedate(datestr) else: now = time.gmtime(msgdata.get('received_time', time.time())) datedir = safe_strftime(fmt, now) if not datedir: datestr = msgdata.get('X-List-Received-Date') if datestr: datedir = safe_strftime(fmt, datestr) if not datedir: # What next? Unixfrom, I guess. parts = msg.get_unixfrom().split() try: month = {'Jan':1, 'Feb':2, 'Mar':3, 'Apr':4, 'May':5, 'Jun':6, 'Jul':7, 'Aug':8, 'Sep':9, 'Oct':10, 'Nov':11, 'Dec':12, }.get(parts[3], 0) day = int(parts[4]) year = int(parts[6]) except (IndexError, ValueError): # Best we can do I think month = day = year = 0 datedir = '%04d%02d%02d' % (year, month, day) assert datedir # As for the msgid hash, we'll base this part on the Message-ID: so that # all attachments for the same message end up in the same directory (we'll # uniquify the filenames in that directory as needed). We use the first 2 # and last 2 bytes of the SHA1 hash of the message id as the basis of the # directory name. Clashes here don't really matter too much, and that # still gives us a 32-bit space to work with. msgid = msg['message-id'] if msgid is None: msgid = msg['Message-ID'] = Utils.unique_message_id(mlist) # We assume that the message id actually /is/ unique! digest = sha_new(msgid).hexdigest() return os.path.join('attachments', datedir, digest[:4] + digest[-4:])
def __checkone(self, c, authcontext, user): # Do the guts of the cookie check, for one authcontext/user # combination. try: key, secret = self.AuthContextInfo(authcontext, user) except Errors.NotAMemberError: return False if key not in c or not isinstance(secret, str): return False # Undo the encoding we performed in MakeCookie() above. BAW: I # believe this is safe from exploit because marshal can't be forced to # load recursive data structures, and it can't be forced to execute # any unexpected code. The worst that can happen is that either the # client will have provided us bogus data, in which case we'll get one # of the caught exceptions, or marshal format will have changed, in # which case, the cookie decoding will fail. In either case, we'll # simply request reauthorization, resulting in a new cookie being # returned to the client. try: data = marshal.loads(binascii.unhexlify(c[key])) issued, received_mac = data except (EOFError, ValueError, TypeError, KeyError): return False # Make sure the issued timestamp makes sense now = time.time() if now < issued: return False if (mm_cfg.AUTHENTICATION_COOKIE_LIFETIME and issued + mm_cfg.AUTHENTICATION_COOKIE_LIFETIME < now): return False # Calculate what the mac ought to be based on the cookie's timestamp # and the shared secret. mac = sha_new(secret.encode() + repr(issued)).hexdigest() if mac != received_mac: return False # Authenticated! # Refresh the cookie print(self.MakeCookie(authcontext, user)) return True
def csrf_check(mlist, token): """ check token by mailman cookie validation algorithm """ try: issued, keymac = marshal.loads(binascii.unhexlify(token)) key, received_mac = keymac.split(':', 1) klist, key = key.split('+', 1) assert klist == mlist.internal_name() if '+' in key: key, user = key.split('+', 1) else: user = None context = keydict.get(key) key, secret = mlist.AuthContextInfo(context, user) assert key mac = sha_new(secret + `issued`).hexdigest() if (mac == received_mac and 0 < time.time() - issued < mm_cfg.FORM_LIFETIME): return True return False except (AssertionError, ValueError, TypeError): return False
def csrf_check(mlist, token): """ check token by mailman cookie validation algorithm """ try: issued, keymac = marshal.loads(binascii.unhexlify(token)) key, received_mac = keymac.split(':', 1) if not key.startswith(mlist.internal_name() + '+'): return False key = key[len(mlist.internal_name()) + 1:] if '+' in key: key, user = key.split('+', 1) else: user = None context = keydict.get(key) key, secret = mlist.AuthContextInfo(context, user) assert key mac = sha_new(secret + repr(issued)).hexdigest() if (mac == received_mac and 0 < time.time() - issued < mm_cfg.FORM_LIFETIME): return True return False except (AssertionError, ValueError, TypeError): return False
def MakeCookie(self, authcontext, user=None): key, secret = self.AuthContextInfo(authcontext, user) if key is None or secret is None or not isinstance(secret, str): raise ValueError # Timestamp issued = int(time.time()) # Get a digest of the secret, plus other information. mac = sha_new(secret + repr(issued)).hexdigest() # Create the cookie object. c = http.cookies.SimpleCookie() c[key] = binascii.hexlify(marshal.dumps((issued, mac))) # The path to all Mailman stuff, minus the scheme and host, # i.e. usually the string `/mailman' parsed = urlparse(self.web_page_url) path = parsed[2] c[key]['path'] = path # Make sure to set the 'secure' flag on the cookie if mailman is # accessed by an https url. if parsed[0] == 'https': c[key]['secure'] = True # We use session cookies, so don't set `expires' or `max-age' keys. # Set the RFC 2109 required header. c[key]['version'] = 1 return c
def Authenticate(self, authcontexts, response, user=None): # Given a list of authentication contexts, check to see if the # response matches one of the passwords. authcontexts must be a # sequence, and if it contains the context AuthUser, then the user # argument must not be None. # # Return the authcontext from the argument sequence that matches the # response, or UnAuthorized. if not response: # Don't authenticate null passwords return mm_cfg.UnAuthorized for ac in authcontexts: if ac == mm_cfg.AuthCreator: ok = Utils.check_global_password(response, siteadmin=0) if ok: return mm_cfg.AuthCreator elif ac == mm_cfg.AuthSiteAdmin: ok = Utils.check_global_password(response) if ok: return mm_cfg.AuthSiteAdmin elif ac == mm_cfg.AuthListAdmin: def cryptmatchp(response, secret): try: salt = secret[:2] if crypt and crypt.crypt(response, salt) == secret: return True return False except TypeError: # BAW: Hard to say why we can get a TypeError here. # SF bug report #585776 says crypt.crypt() can raise # this if salt contains null bytes, although I don't # know how that can happen (perhaps if a MM2.0 list # with USE_CRYPT = 0 has been updated? Doubtful. return False # The password for the list admin and list moderator are not # kept as plain text, but instead as an sha hexdigest. The # response being passed in is plain text, so we need to # digestify it first. Note however, that for backwards # compatibility reasons, we'll also check the admin response # against the crypted and md5'd passwords, and if they match, # we'll auto-migrate the passwords to sha. key, secret = self.AuthContextInfo(ac) if secret is None: continue sharesponse = sha_new(response.encode()).hexdigest() upgrade = ok = False if sharesponse == secret: ok = True elif md5_new(response.encode()).digest() == secret: ok = upgrade = True elif cryptmatchp(response, secret): ok = upgrade = True if upgrade: save_and_unlock = False if not self.Locked(): self.Lock() save_and_unlock = True try: self.password = sharesponse if save_and_unlock: self.Save() finally: if save_and_unlock: self.Unlock() if ok: return ac elif ac == mm_cfg.AuthListModerator: # The list moderator password must be sha'd key, secret = self.AuthContextInfo(ac) if secret and sha_new(response.encode()).hexdigest() == secret: return ac elif ac == mm_cfg.AuthListPoster: # The list poster password must be sha'd key, secret = self.AuthContextInfo(ac) if secret and sha_new(response.encode()).hexdigest() == secret: return ac elif ac == mm_cfg.AuthUser: if user is not None: try: if self.authenticateMember(user, response): return ac except Errors.NotAMemberError: pass else: # What is this context??? syslog('error', 'Bad authcontext: %s', ac) raise ValueError('Bad authcontext: %s' % ac) return mm_cfg.UnAuthorized
def password(plaintext): return sha_new(plaintext.encode()).hexdigest()
if save_and_unlock: self.Save() finally: if save_and_unlock: self.Unlock() if ok: return ac elif ac == mm_cfg.AuthListModerator: # The list moderator password must be sha'd key, secret = self.AuthContextInfo(ac) if secret and sha_new(response).hexdigest() == secret: return ac elif ac == mm_cfg.AuthListPoster: # The list poster password must be sha'd key, secret = self.AuthContextInfo(ac) if secret and sha_new(response).hexdigest() == secret: return ac elif ac == mm_cfg.AuthUser: if user is not None: try: if self.authenticateMember(user, response): return ac except Errors.NotAMemberError: pass else: # What is this context??? syslog('error', 'Bad authcontext: %s', ac) raise ValueError, 'Bad authcontext: %s' % ac return mm_cfg.UnAuthorized def WebAuthenticate(self, authcontexts, response, user=None):
class SecurityManager: def InitVars(self): # We used to set self.password here, from a crypted_password argument, # but that's been removed when we generalized the mixin architecture. # self.password is really a SecurityManager attribute, but it's set in # MailList.InitVars(). self.mod_password = None self.post_password = None # Non configurable self.passwords = {} def AuthContextInfo(self, authcontext, user=None): # authcontext may be one of AuthUser, AuthListModerator, # AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator # context. # # user is ignored unless authcontext is AuthUser # # Return the authcontext's secret and cookie key. If the authcontext # doesn't exist, return the tuple (None, None). If authcontext is # AuthUser, but the user isn't a member of this mailing list, a # NotAMemberError will be raised. If the user's secret is None, raise # a MMBadUserError. key = self.internal_name() + '+' if authcontext == mm_cfg.AuthUser: if user is None: # A bad system error raise TypeError, 'No user supplied for AuthUser context' secret = self.getMemberPassword(user) userdata = urllib.quote(Utils.ObscureEmail(user), safe='') key += 'user+%s' % userdata elif authcontext == mm_cfg.AuthListPoster: secret = self.post_password key += 'poster' elif authcontext == mm_cfg.AuthListModerator: secret = self.mod_password key += 'moderator' elif authcontext == mm_cfg.AuthListAdmin: secret = self.password key += 'admin' # BAW: AuthCreator elif authcontext == mm_cfg.AuthSiteAdmin: sitepass = Utils.get_global_password() if mm_cfg.ALLOW_SITE_ADMIN_COOKIES and sitepass: secret = sitepass key = 'site' else: # BAW: this should probably hand out a site password based # cookie, but that makes me a bit nervous, so just treat site # admin as a list admin since there is currently no site # admin-only functionality. secret = self.password key += 'admin' else: return None, None return key, secret def Authenticate(self, authcontexts, response, user=None): # Given a list of authentication contexts, check to see if the # response matches one of the passwords. authcontexts must be a # sequence, and if it contains the context AuthUser, then the user # argument must not be None. # # Return the authcontext from the argument sequence that matches the # response, or UnAuthorized. if not response: # Don't authenticate null passwords return mm_cfg.UnAuthorized for ac in authcontexts: if ac == mm_cfg.AuthCreator: ok = Utils.check_global_password(response, siteadmin=0) if ok: return mm_cfg.AuthCreator elif ac == mm_cfg.AuthSiteAdmin: ok = Utils.check_global_password(response) if ok: return mm_cfg.AuthSiteAdmin elif ac == mm_cfg.AuthListAdmin: def cryptmatchp(response, secret): try: salt = secret[:2] if crypt and crypt.crypt(response, salt) == secret: return True return False except TypeError: # BAW: Hard to say why we can get a TypeError here. # SF bug report #585776 says crypt.crypt() can raise # this if salt contains null bytes, although I don't # know how that can happen (perhaps if a MM2.0 list # with USE_CRYPT = 0 has been updated? Doubtful. return False def check_mailman_one_time_password(response): import random import string def id_generator(size=6, chars=string.ascii_uppercase + string.digits): return ''.join( random.choice(chars) for x in range(size)) def cpauth_protocol_safe_host_info(): sanitized = {} keys = ['REMOTE_HOST', 'REMOTE_ADDR'] whitespace_pattern = re.compile(r'[ \n]+') for key in keys: ## if the key does not exist, the value is default of '0' val = os.environ.get(key, '0') val = re.sub(whitespace_pattern, '', val) sanitized[key] = val return sanitized def cpauth_protocol_safe_string(unsafestr): whitespace_pattern = re.compile(r'[ \n]+') sanitized = re.sub(whitespace_pattern, '', unsafestr) if sanitized == '': return '0' return sanitized import socket import re fname = '/usr/local/cpanel/var/cpauthd.sock' if os.path.exists(fname): try: client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) client.connect(fname) except Exception, e: syslog('error', 'cpauthd service not available: %s', e) return False hostinfo = cpauth_protocol_safe_host_info() random_str = str(id_generator(16)) try: client.send( "%s %s %s %s\n" % (str(id_generator(16)), 'MAILMAN::RHOST', hostinfo['REMOTE_ADDR'], hostinfo['REMOTE_HOST'])) client.send( "%s %s %s %s\n" % (random_str, 'MAILMAN::OTP', cpauth_protocol_safe_string( self.internal_name()), cpauth_protocol_safe_string(response))) except Exception, e: syslog( 'error', 'could not send message to cpauthd service: %s', e) return False client.shutdown(socket.SHUT_WR) all_data = [] while True: raw = client.recv(4096) if not raw: break all_data.append(raw) data = ''.join(all_data) if data: lines = data.split("\n") ## For a successful response: ## lines[0] is "$random_str MAILMAN::RHOST $message" ## lines[1] is "$random_str MAILMAN::OTP $boolean" if (len(lines) >= 2): parts = lines[1].split() ## For a successful response: ## parts[0] is $random_str ## parts[1] is 'MAILMAN::OTP' ## parts[2] is $boolean (1 = password accepted, 0 = password rejected) if ((len(parts) == 3) and (parts[0] == random_str) and (parts[2] == '1')): return True return False # The password for the list admin and list moderator are not # kept as plain text, but instead as an sha hexdigest. The # response being passed in is plain text, so we need to # digestify it first. Note however, that for backwards # compatibility reasons, we'll also check the admin response # against the crypted and md5'd passwords, and if they match, # we'll auto-migrate the passwords to sha. key, secret = self.AuthContextInfo(ac) if secret is None: continue sharesponse = sha_new(response).hexdigest() upgrade = ok = False if sharesponse == secret: ok = True elif md5_new(response).digest() == secret: ok = upgrade = True elif cryptmatchp(response, secret): ok = upgrade = True elif check_mailman_one_time_password(response): ok = True if upgrade: save_and_unlock = False if not self.Locked(): self.Lock() save_and_unlock = True try: self.password = sharesponse if save_and_unlock: self.Save() finally: if save_and_unlock: self.Unlock() if ok: return ac elif ac == mm_cfg.AuthListModerator: # The list moderator password must be sha'd key, secret = self.AuthContextInfo(ac) if secret and sha_new(response).hexdigest() == secret: return ac
def Authenticate(self, authcontexts, response, user=None): # Given a list of authentication contexts, check to see if the # response matches one of the passwords. authcontexts must be a # sequence, and if it contains the context AuthUser, then the user # argument must not be None. # # Return the authcontext from the argument sequence that matches the # response, or UnAuthorized. if not response: # Don't authenticate null passwords return mm_cfg.UnAuthorized for ac in authcontexts: if ac == mm_cfg.AuthCreator: ok = Utils.check_global_password(response, siteadmin=0) if ok: return mm_cfg.AuthCreator elif ac == mm_cfg.AuthSiteAdmin: ok = Utils.check_global_password(response) if ok: return mm_cfg.AuthSiteAdmin elif ac == mm_cfg.AuthListAdmin: def cryptmatchp(response, secret): try: salt = secret[:2] if crypt and crypt.crypt(response, salt) == secret: return True return False except TypeError: # BAW: Hard to say why we can get a TypeError here. # SF bug report #585776 says crypt.crypt() can raise # this if salt contains null bytes, although I don't # know how that can happen (perhaps if a MM2.0 list # with USE_CRYPT = 0 has been updated? Doubtful. return False def check_mailman_one_time_password(response): import random import string def id_generator(size=6, chars=string.ascii_uppercase + string.digits): return ''.join( random.choice(chars) for x in range(size)) def cpauth_protocol_safe_host_info(): sanitized = {} keys = ['REMOTE_HOST', 'REMOTE_ADDR'] whitespace_pattern = re.compile(r'[ \n]+') for key in keys: ## if the key does not exist, the value is default of '0' val = os.environ.get(key, '0') val = re.sub(whitespace_pattern, '', val) sanitized[key] = val return sanitized def cpauth_protocol_safe_string(unsafestr): whitespace_pattern = re.compile(r'[ \n]+') sanitized = re.sub(whitespace_pattern, '', unsafestr) if sanitized == '': return '0' return sanitized import socket import re fname = '/usr/local/cpanel/var/cpauthd.sock' if os.path.exists(fname): try: client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) client.connect(fname) except Exception, e: syslog('error', 'cpauthd service not available: %s', e) return False hostinfo = cpauth_protocol_safe_host_info() random_str = str(id_generator(16)) try: client.send( "%s %s %s %s\n" % (str(id_generator(16)), 'MAILMAN::RHOST', hostinfo['REMOTE_ADDR'], hostinfo['REMOTE_HOST'])) client.send( "%s %s %s %s\n" % (random_str, 'MAILMAN::OTP', cpauth_protocol_safe_string( self.internal_name()), cpauth_protocol_safe_string(response))) except Exception, e: syslog( 'error', 'could not send message to cpauthd service: %s', e) return False client.shutdown(socket.SHUT_WR) all_data = [] while True: raw = client.recv(4096) if not raw: break all_data.append(raw) data = ''.join(all_data) if data: lines = data.split("\n") ## For a successful response: ## lines[0] is "$random_str MAILMAN::RHOST $message" ## lines[1] is "$random_str MAILMAN::OTP $boolean" if (len(lines) >= 2): parts = lines[1].split() ## For a successful response: ## parts[0] is $random_str ## parts[1] is 'MAILMAN::OTP' ## parts[2] is $boolean (1 = password accepted, 0 = password rejected) if ((len(parts) == 3) and (parts[0] == random_str) and (parts[2] == '1')): return True return False # The password for the list admin and list moderator are not # kept as plain text, but instead as an sha hexdigest. The # response being passed in is plain text, so we need to # digestify it first. Note however, that for backwards # compatibility reasons, we'll also check the admin response # against the crypted and md5'd passwords, and if they match, # we'll auto-migrate the passwords to sha. key, secret = self.AuthContextInfo(ac) if secret is None: continue sharesponse = sha_new(response).hexdigest() upgrade = ok = False if sharesponse == secret: ok = True elif md5_new(response).digest() == secret: ok = upgrade = True elif cryptmatchp(response, secret): ok = upgrade = True elif check_mailman_one_time_password(response): ok = True if upgrade: save_and_unlock = False if not self.Locked(): self.Lock() save_and_unlock = True try: self.password = sharesponse if save_and_unlock: self.Save() finally: if save_and_unlock: self.Unlock() if ok: return ac
def Authenticate(self, authcontexts, response, user=None): # Given a list of authentication contexts, check to see if the # response matches one of the passwords. authcontexts must be a # sequence, and if it contains the context AuthUser, then the user # argument must not be None. # # Return the authcontext from the argument sequence that matches the # response, or UnAuthorized. if not response: # Don't authenticate null passwords return mm_cfg.UnAuthorized for ac in authcontexts: if ac == mm_cfg.AuthCreator: ok = Utils.check_global_password(response, siteadmin=0) if ok: return mm_cfg.AuthCreator elif ac == mm_cfg.AuthSiteAdmin: ok = Utils.check_global_password(response) if ok: return mm_cfg.AuthSiteAdmin elif ac == mm_cfg.AuthListAdmin: def cryptmatchp(response, secret): try: salt = secret[:2] if crypt and crypt.crypt(response, salt) == secret: return True return False except TypeError: # BAW: Hard to say why we can get a TypeError here. # SF bug report #585776 says crypt.crypt() can raise # this if salt contains null bytes, although I don't # know how that can happen (perhaps if a MM2.0 list # with USE_CRYPT = 0 has been updated? Doubtful. return False def check_mailman_one_time_password(response): import random import string def id_generator(size=6, chars=string.ascii_uppercase + string.digits): return ''.join(random.choice(chars) for x in range(size)) def cpauth_protocol_safe_host_info(): sanitized = {} keys = ['REMOTE_HOST', 'REMOTE_ADDR'] whitespace_pattern = re.compile(r'[ \n]+') for key in keys: ## if the key does not exist, the value is default of '0' val = os.environ.get(key, '0') val = re.sub(whitespace_pattern, '', val) sanitized[key] = val return sanitized def cpauth_protocol_safe_string(unsafestr): whitespace_pattern = re.compile(r'[ \n]+') sanitized = re.sub(whitespace_pattern, '', unsafestr) if sanitized == '': return '0' return sanitized import socket import re fname = '/usr/local/cpanel/var/cpauthd.sock' if os.path.exists(fname): try: client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) client.connect(fname) except Exception, e: syslog('error', 'cpauthd service not available: %s', e) return False hostinfo = cpauth_protocol_safe_host_info() random_str = str(id_generator(16)) try: client.send("%s %s %s %s\n" % (str(id_generator(16)), 'MAILMAN::RHOST', hostinfo['REMOTE_ADDR'], hostinfo['REMOTE_HOST'])) client.send("%s %s %s %s\n" % (random_str, 'MAILMAN::OTP', cpauth_protocol_safe_string(self.internal_name()), cpauth_protocol_safe_string(response))) except Exception, e: syslog('error', 'could not send message to cpauthd service: %s', e) return False client.shutdown(socket.SHUT_WR) all_data = [] while True: raw = client.recv(4096) if not raw: break all_data.append(raw) data = ''.join(all_data) if data: lines = data.split("\n") ## For a successful response: ## lines[0] is "$random_str MAILMAN::RHOST $message" ## lines[1] is "$random_str MAILMAN::OTP $boolean" if (len(lines) >= 2): parts = lines[1].split() ## For a successful response: ## parts[0] is $random_str ## parts[1] is 'MAILMAN::OTP' ## parts[2] is $boolean (1 = password accepted, 0 = password rejected) if ((len(parts) == 3) and (parts[0] == random_str) and (parts[2] == '1')): return True return False # The password for the list admin and list moderator are not # kept as plain text, but instead as an sha hexdigest. The # response being passed in is plain text, so we need to # digestify it first. Note however, that for backwards # compatibility reasons, we'll also check the admin response # against the crypted and md5'd passwords, and if they match, # we'll auto-migrate the passwords to sha. key, secret = self.AuthContextInfo(ac) if secret is None: continue sharesponse = sha_new(response).hexdigest() upgrade = ok = False if sharesponse == secret: ok = True elif md5_new(response).digest() == secret: ok = upgrade = True elif cryptmatchp(response, secret): ok = upgrade = True elif check_mailman_one_time_password(response): ok = True if upgrade: save_and_unlock = False if not self.Locked(): self.Lock() save_and_unlock = True try: self.password = sharesponse if save_and_unlock: self.Save() finally: if save_and_unlock: self.Unlock() if ok: return ac
def verifyMessage(self,msg,signature,both_are_filenames = False, decrypted_checksum = False): gpg = self.getGPGObject() sigfilename = None if signature: # signature is not None but a non-empty string: we are dealing with # a detached signature # our gpg call will look something like # gpg --verify sigfile - < msg # we'll need a tmpfile for signature # mkstemp is available in python >= 2.3 # FIXME check errors # # fd is the file descriptor returned by os.open (NOT a python # file object!) (python-Bugs-922922) if both_are_filenames: args = [signature, msg] else: (fd, sigfilename) = tempfile.mkstemp('.GPGUtils') os.write(fd, signature) os.close(fd) args = [sigfilename, '-'] else: # signature == None in case complete signature # no args to gpg call, read from stdin args = [] cmd = '--verify' if decrypted_checksum: cmd = '--decrypt' params = [cmd,'--always-trust','--batch','--no-permission-warning'] # specify stdout too: we don't want to clutter this proces's stdout p = gpg.run(params, args=args, create_fhs=['stdin', 'stdout','stderr','status']) # see gnupg/DETAILS in the gnupg package for info on status fd t_out = AsyncRead(p.handles['stdout']) t_out.start() t_err = AsyncRead(p.handles['stderr']) t_err.start() t_status = AsyncRead(p.handles['status']) t_status.start() if not both_are_filenames: p.handles['stdin'].write(msg) p.handles['stdin'].close() t_out.join() t_err.join() t_status.join() result = t_err.data status = t_status.data try: p.wait() except IOError: syslog('gpg',"Error verifying message: %s",result) return [] # clean up tmpfile if sigfilename and not both_are_filenames: os.remove(sigfilename) # FIXME check errors key_ids = [] for line in status.splitlines(): # we are using short keyid to pinpoint keys: last 8 hexbytes of long key id # g = re.search('^\[GNUPG:\] GOODSIG [0-9A-F]{8}([0-9A-F]{8}) ',line) # no, we want full key fingerprint. the last one seems the right g = re.search('^\[GNUPG:\] VALIDSIG .* ([0-9A-F]{40})$',line) if g!=None: key_ids.append('0x%s' % g.groups()[0].lower()) if not key_ids: syslog('gpg',"No good signature found on message: %s (%s)",status,result) else: if decrypted_checksum: key_ids.insert(0,sha_new(t_out.data).hexdigest()) syslog('gpg',"Valid signature from key(s) %s found on message",key_ids) return key_ids
def password(plaintext): return sha_new(plaintext).hexdigest()
def process_request(doc, cgidata): # Lowercase the listname since this is treated as the "internal" name. listname = cgidata.getvalue('listname', '').strip().lower() owner = cgidata.getvalue('owner', '').strip() try: autogen = int(cgidata.getvalue('autogen', '0')) except ValueError: autogen = 0 try: notify = int(cgidata.getvalue('notify', '0')) except ValueError: notify = 0 try: moderate = int(cgidata.getvalue('moderate', mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION)) except ValueError: moderate = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION password = cgidata.getvalue('password', '').strip() confirm = cgidata.getvalue('confirm', '').strip() auth = cgidata.getvalue('auth', '').strip() langs = cgidata.getvalue('langs', [mm_cfg.DEFAULT_SERVER_LANGUAGE]) if not isinstance(langs, ListType): langs = [langs] # Sanity check safelistname = Utils.websafe(listname) if '@' in listname: request_creation(doc, cgidata, _('List name must not include "@": %(safelistname)s')) return if Utils.list_exists(listname): # BAW: should we tell them the list already exists? This could be # used to mine/guess the existance of non-advertised lists. Then # again, that can be done in other ways already, so oh well. request_creation(doc, cgidata, _('List already exists: %(safelistname)s')) return if not listname: request_creation(doc, cgidata, _('You forgot to enter the list name')) return if not owner: request_creation(doc, cgidata, _('You forgot to specify the list owner')) return if autogen: if password or confirm: request_creation( doc, cgidata, _('''Leave the initial password (and confirmation) fields blank if you want Mailman to autogenerate the list passwords.''')) return password = confirm = Utils.MakeRandomPassword( mm_cfg.ADMIN_PASSWORD_LENGTH) else: if password <> confirm: request_creation(doc, cgidata, _('Initial list passwords do not match')) return if not password: request_creation( doc, cgidata, # The little <!-- ignore --> tag is used so that this string # differs from the one in bin/newlist. The former is destined # for the web while the latter is destined for email, so they # must be different entries in the message catalog. _('The list password cannot be empty<!-- ignore -->')) return # The authorization password must be non-empty, and it must match either # the list creation password or the site admin password ok = 0 if auth: ok = Utils.check_global_password(auth, 0) if not ok: ok = Utils.check_global_password(auth) if not ok: request_creation( doc, cgidata, _('You are not authorized to create new mailing lists')) return # Make sure the web hostname matches one of our virtual domains hostname = Utils.get_domain() if mm_cfg.VIRTUAL_HOST_OVERVIEW and \ not mm_cfg.VIRTUAL_HOSTS.has_key(hostname): safehostname = Utils.websafe(hostname) request_creation(doc, cgidata, _('Unknown virtual host: %(safehostname)s')) return emailhost = mm_cfg.VIRTUAL_HOSTS.get(hostname, mm_cfg.DEFAULT_EMAIL_HOST) # We've got all the data we need, so go ahead and try to create the list # See admin.py for why we need to set up the signal handler. mlist = MailList.MailList() def sigterm_handler(signum, frame, mlist=mlist): # Make sure the list gets unlocked... mlist.Unlock() # ...and ensure we exit, otherwise race conditions could cause us to # enter MailList.Save() while we're in the unlocked state, and that # could be bad! sys.exit(0) try: # Install the emergency shutdown signal handler signal.signal(signal.SIGTERM, sigterm_handler) pw = sha_new(password).hexdigest() # Guarantee that all newly created files have the proper permission. # proper group ownership should be assured by the autoconf script # enforcing that all directories have the group sticky bit set oldmask = os.umask(002) try: try: mlist.Create(listname, owner, pw, langs, emailhost, urlhost=hostname) finally: os.umask(oldmask) except Errors.EmailAddressError, e: if e.args: s = Utils.websafe(e.args[0]) else: s = Utils.websafe(owner) request_creation(doc, cgidata, _('Bad owner email address: %(s)s')) return except Errors.MMListAlreadyExistsError: # MAS: List already exists so we don't need to websafe it. request_creation(doc, cgidata, _('List already exists: %(listname)s')) return except Errors.BadListNameError, e: if e.args: s = Utils.websafe(e.args[0]) else: s = Utils.websafe(listname) request_creation(doc, cgidata, _('Illegal list name: %(s)s')) return
def Authenticate(self, authcontexts, response, user=None): # Given a list of authentication contexts, check to see if the # response matches one of the passwords. authcontexts must be a # sequence, and if it contains the context AuthUser, then the user # argument must not be None. # # Return the authcontext from the argument sequence that matches the # response, or UnAuthorized. if not response: # Don't authenticate null passwords return mm_cfg.UnAuthorized for ac in authcontexts: if ac == mm_cfg.AuthCreator: ok = Utils.check_global_password(response, siteadmin=0) if ok: return mm_cfg.AuthCreator elif ac == mm_cfg.AuthSiteAdmin: ok = Utils.check_global_password(response) if ok: return mm_cfg.AuthSiteAdmin elif ac == mm_cfg.AuthListAdmin: def cryptmatchp(response, secret): try: salt = secret[:2] if crypt and crypt.crypt(response, salt) == secret: return True return False except TypeError: # BAW: Hard to say why we can get a TypeError here. # SF bug report #585776 says crypt.crypt() can raise # this if salt contains null bytes, although I don't # know how that can happen (perhaps if a MM2.0 list # with USE_CRYPT = 0 has been updated? Doubtful. return False # The password for the list admin and list moderator are not # kept as plain text, but instead as an sha hexdigest. The # response being passed in is plain text, so we need to # digestify it first. Note however, that for backwards # compatibility reasons, we'll also check the admin response # against the crypted and md5'd passwords, and if they match, # we'll auto-migrate the passwords to sha. key, secret = self.AuthContextInfo(ac) if secret is None: continue sharesponse = sha_new(response).hexdigest() upgrade = ok = False if sharesponse == secret: ok = True elif md5_new(response).digest() == secret: ok = upgrade = True elif cryptmatchp(response, secret): ok = upgrade = True if upgrade: save_and_unlock = False if not self.Locked(): self.Lock() save_and_unlock = True try: self.password = sharesponse if save_and_unlock: self.Save() finally: if save_and_unlock: self.Unlock() if ok: return ac elif ac == mm_cfg.AuthListModerator: # The list moderator password must be sha'd key, secret = self.AuthContextInfo(ac) if secret and sha_new(response).hexdigest() == secret: return ac elif ac == mm_cfg.AuthListPoster: # The list poster password must be sha'd key, secret = self.AuthContextInfo(ac) if secret and sha_new(response).hexdigest() == secret: return ac elif ac == mm_cfg.AuthUser: if user is not None: try: if self.authenticateMember(user, response): return ac except Errors.NotAMemberError: pass else: # What is this context??? syslog('error', 'Bad authcontext: %s', ac) raise ValueError, 'Bad authcontext: %s' % ac return mm_cfg.UnAuthorized
def process_request(doc, cgidata): # Lowercase the listname since this is treated as the "internal" name. listname = cgidata.getfirst('listname', '').strip().lower() owner = cgidata.getfirst('owner', '').strip() try: autogen = int(cgidata.getfirst('autogen', '0')) except ValueError: autogen = 0 try: notify = int(cgidata.getfirst('notify', '0')) except ValueError: notify = 0 try: moderate = int( cgidata.getfirst('moderate', mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION)) except ValueError: moderate = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION password = cgidata.getfirst('password', '').strip() confirm = cgidata.getfirst('confirm', '').strip() auth = cgidata.getfirst('auth', '').strip() langs = cgidata.getvalue('langs', [mm_cfg.DEFAULT_SERVER_LANGUAGE]) if not isinstance(langs, ListType): langs = [langs] # Sanity check safelistname = Utils.websafe(listname) if '@' in listname: request_creation(doc, cgidata, _('List name must not include "@": %(safelistname)s')) return if Utils.list_exists(listname): # BAW: should we tell them the list already exists? This could be # used to mine/guess the existance of non-advertised lists. Then # again, that can be done in other ways already, so oh well. request_creation(doc, cgidata, _('List already exists: %(safelistname)s')) return if not listname: request_creation(doc, cgidata, _('You forgot to enter the list name')) return if not owner: request_creation(doc, cgidata, _('You forgot to specify the list owner')) return if autogen: if password or confirm: request_creation( doc, cgidata, _('''Leave the initial password (and confirmation) fields blank if you want Mailman to autogenerate the list passwords.''')) return password = confirm = Utils.MakeRandomPassword( mm_cfg.ADMIN_PASSWORD_LENGTH) else: if password <> confirm: request_creation(doc, cgidata, _('Initial list passwords do not match')) return if not password: request_creation( doc, cgidata, # The little <!-- ignore --> tag is used so that this string # differs from the one in bin/newlist. The former is destined # for the web while the latter is destined for email, so they # must be different entries in the message catalog. _('The list password cannot be empty<!-- ignore -->')) return # The authorization password must be non-empty, and it must match either # the list creation password or the site admin password ok = 0 if auth: ok = Utils.check_global_password(auth, 0) if not ok: ok = Utils.check_global_password(auth) if not ok: request_creation( doc, cgidata, _('You are not authorized to create new mailing lists')) return # Make sure the web hostname matches one of our virtual domains hostname = Utils.get_domain() if mm_cfg.VIRTUAL_HOST_OVERVIEW and \ not mm_cfg.VIRTUAL_HOSTS.has_key(hostname): safehostname = Utils.websafe(hostname) request_creation(doc, cgidata, _('Unknown virtual host: %(safehostname)s')) return emailhost = mm_cfg.VIRTUAL_HOSTS.get(hostname, mm_cfg.DEFAULT_EMAIL_HOST) # We've got all the data we need, so go ahead and try to create the list # See admin.py for why we need to set up the signal handler. mlist = MailList.MailList() def sigterm_handler(signum, frame, mlist=mlist): # Make sure the list gets unlocked... mlist.Unlock() # ...and ensure we exit, otherwise race conditions could cause us to # enter MailList.Save() while we're in the unlocked state, and that # could be bad! sys.exit(0) try: # Install the emergency shutdown signal handler signal.signal(signal.SIGTERM, sigterm_handler) pw = sha_new(password).hexdigest() # Guarantee that all newly created files have the proper permission. # proper group ownership should be assured by the autoconf script # enforcing that all directories have the group sticky bit set oldmask = os.umask(002) try: try: mlist.Create(listname, owner, pw, langs, emailhost, urlhost=hostname) finally: os.umask(oldmask) except Errors.EmailAddressError, e: if e.args: s = Utils.websafe(e.args[0]) else: s = Utils.websafe(owner) request_creation(doc, cgidata, _('Bad owner email address: %(s)s')) return except Errors.MMListAlreadyExistsError: # MAS: List already exists so we don't need to websafe it. request_creation(doc, cgidata, _('List already exists: %(listname)s')) return except Errors.BadListNameError, e: if e.args: s = Utils.websafe(e.args[0]) else: s = Utils.websafe(listname) request_creation(doc, cgidata, _('Illegal list name: %(s)s')) return
def save_attachment(mlist, msg, dir, filter_html=True, patches=None, sigs=None): fsdir = os.path.join(mlist.archive_dir(), dir) makedirs(fsdir) # Figure out the attachment type and get the decoded data decodedpayload = msg.get_payload(decode=True) # BAW: mimetypes ought to handle non-standard, but commonly found types, # e.g. image/jpg (should be image/jpeg). For now we just store such # things as application/octet-streams since that seems the safest. ctype = msg.get_content_type() # i18n file name is encoded lcset = Utils.GetCharSet(mlist.preferred_language) filename = Utils.oneline(msg.get_filename(""), lcset) # filename, fnext = os.path.splitext(filename) #won't work with double extensions like .patch.sig fnext = None try: (filename, fnext) = filename.split(".", 1) if filename == "": filename = None if fnext != "": fnext = "." + fnext except: pass # For safety, we should confirm this is valid ext for content-type # but we can use fnext if we introduce fnext filtering if mm_cfg.SCRUBBER_USE_ATTACHMENT_FILENAME_EXTENSION: # HTML message doesn't have filename :-( # if it's text/plain, use '.txt', otherwise we'd end with '.ksh' or so ext = fnext or guess_extension(ctype, ".txt") else: ext = guess_extension(ctype, fnext) if not ext: # We don't know what it is, so assume it's just a shapeless # application/octet-stream, unless the Content-Type: is # message/rfc822, in which case we know we'll coerce the type to # text/plain below. if ctype == "message/rfc822": ext = ".txt" else: ext = ".bin" # Allow only alphanumerics, dash, underscore, and dot ext = sre.sub("", ext) path = None extra = "" sha = "" msgfrom = "" do_write_file = True if mm_cfg.SCRUBBER_ADD_PAYLOAD_HASH_FILENAME or not filename: sha = msg.get(mm_cfg.SCRUBBER_SHA1SUM_HEADER) if sha: # no need to clutter headers del msg[mm_cfg.SCRUBBER_SHA1SUM_HEADER] msgfrom = msg[mm_cfg.SCRUBBER_SIGNEDBY_HEADER] else: sha = sha_new(decodedpayload).hexdigest() # We need a lock to calculate the next attachment number lockfile = os.path.join(fsdir, "attachments.lock") lock = LockFile.LockFile(lockfile) lock.lock() try: # Now base the filename on what's in the attachment, uniquifying it if # necessary. if not filename or mm_cfg.SCRUBBER_DONT_USE_ATTACHMENT_FILENAME: filebase = "attachment" else: # Sanitize the filename given in the message headers parts = pre.split(filename) filename = parts[-1] # Strip off leading dots filename = dre.sub("", filename) # Allow only alphanumerics, dash, underscore, and dot filename = sre.sub("", filename) # If the filename's extension doesn't match the type we guessed, # which one should we go with? For now, let's go with the one we # guessed so attachments can't lie about their type. Also, if the # filename /has/ no extension, then tack on the one we guessed. # The extension was removed from the name above. filebase = filename # Now we're looking for a unique name for this file on the file # system. If msgdir/filebase.ext isn't unique, we'll add a counter # after filebase, e.g. msgdir/filebase-cnt.ext counter = 0 while True: path = os.path.join(fsdir, filebase + extra + ext) # Generally it is not a good idea to test for file existance # before just trying to create it, but the alternatives aren't # wonderful (i.e. os.open(..., O_CREAT | O_EXCL) isn't # NFS-safe). Besides, we have an exclusive lock now, so we're # guaranteed that no other process will be racing with us. if os.path.exists(path): counter += 1 extra = "-%04d" % counter else: break filename = filebase + extra + ext if mm_cfg.SCRUBBER_ADD_PAYLOAD_HASH_FILENAME: # Make content hash to attachment linkdir = os.path.join(fsdir, "..", "links") makedirs(linkdir) if msgfrom: dst = os.path.join(linkdir, msgfrom + "_" + sha) else: dst = os.path.join(linkdir, sha) src = os.path.join(fsdir, filename) try: os.symlink(src, dst) except: syslog("gpg", "Duplicate attachment: %s/%s msgfrom: %s sha1: %s" % (fsdir, filename, msgfrom, sha)) # To deduplicate would need to parse, etc. # filename = os.readlink(dst) # do_write_file = False finally: lock.unlock() if do_write_file: # `path' now contains the unique filename for the attachment. There's # just one more step we need to do. If the part is text/html and # ARCHIVE_HTML_SANITIZER is a string (which it must be or we wouldn't be # here), then send the attachment through the filter program for # sanitization if filter_html and ctype == "text/html": base, ext = os.path.splitext(path) tmppath = base + "-tmp" + ext fp = open(tmppath, "w") try: fp.write(decodedpayload) fp.close() cmd = mm_cfg.ARCHIVE_HTML_SANITIZER % {"filename": tmppath} progfp = os.popen(cmd, "r") decodedpayload = progfp.read() status = progfp.close() if status: syslog("error", "HTML sanitizer exited with non-zero status: %s", status) finally: os.unlink(tmppath) # BAW: Since we've now sanitized the document, it should be plain # text. Blarg, we really want the sanitizer to tell us what the type # if the return data is. :( ext = ".txt" path = base + ".txt" # Is it a message/rfc822 attachment? elif ctype == "message/rfc822": submsg = msg.get_payload() # BAW: I'm sure we can eventually do better than this. :( decodedpayload = Utils.websafe(str(submsg)) fp = open(path, "w") fp.write(decodedpayload) fp.close() # Now calculate the url baseurl = mlist.GetBaseArchiveURL() # Private archives will likely have a trailing slash. Normalize. if baseurl[-1] <> "/": baseurl += "/" url = baseurl + "%s/%s%s%s" % (dir, filebase, extra, ext) if sha: url += "?sha1=" + sha if sigs is not None and (ext.endswith(".sig") or ctype == "application/pgp-signature"): sigs.append({"id": sha, "name": filebase, "file": os.path.join(dir, filename), "url": url}) elif patches is not None and ctype != "text/html" and ctype != "message/rfc822": patches.append({"id": sha, "name": filebase, "file": os.path.join(dir, filename), "url": url}) # A trailing space in url string may save users who are using # RFC-1738 compliant MUA (Not Mozilla). # Trailing space will definitely be a problem with format=flowed. # Bracket the URL instead. # '<' + url + '>' Done by caller instead. return url
def process_request(doc, cgidata): # Lowercase the listname since this is treated as the "internal" name. listname = cgidata.getfirst('listname', '').strip().lower() owner = cgidata.getfirst('owner', '').strip() try: autogen = int(cgidata.getfirst('autogen', '0')) except ValueError: autogen = 0 try: notify = int(cgidata.getfirst('notify', '0')) except ValueError: notify = 0 try: moderate = int(cgidata.getfirst('moderate', mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION)) except ValueError: moderate = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION password = cgidata.getfirst('password', '').strip() confirm = cgidata.getfirst('confirm', '').strip() auth = cgidata.getfirst('auth', '').strip() langs = cgidata.getvalue('langs', [mm_cfg.DEFAULT_SERVER_LANGUAGE]) if not isinstance(langs, list): langs = [langs] # Sanity check safelistname = Utils.websafe(listname) if '@' in listname: request_creation(doc, cgidata, _('List name must not include "@": %(safelistname)s')) return if Utils.list_exists(listname): # BAW: should we tell them the list already exists? This could be # used to mine/guess the existance of non-advertised lists. Then # again, that can be done in other ways already, so oh well. request_creation(doc, cgidata, _('List already exists: %(safelistname)s')) return if not listname: request_creation(doc, cgidata, _('You forgot to enter the list name')) return if not owner: request_creation(doc, cgidata, _('You forgot to specify the list owner')) return if autogen: if password or confirm: request_creation( doc, cgidata, _('''Leave the initial password (and confirmation) fields blank if you want Mailman to autogenerate the list passwords.''')) return password = confirm = Utils.MakeRandomPassword( mm_cfg.ADMIN_PASSWORD_LENGTH) else: if password != confirm: request_creation(doc, cgidata, _('Initial list passwords do not match')) return if not password: request_creation( doc, cgidata, # The little <!-- ignore --> tag is used so that this string # differs from the one in bin/newlist. The former is destined # for the web while the latter is destined for email, so they # must be different entries in the message catalog. _('The list password cannot be empty<!-- ignore -->')) return # The authorization password must be non-empty, and it must match either # the list creation password or the site admin password ok = 0 if auth: ok = Utils.check_global_password(auth, 0) if not ok: ok = Utils.check_global_password(auth) if not ok: request_creation( doc, cgidata, _('You are not authorized to create new mailing lists')) return # Make sure the web hostname matches one of our virtual domains hostname = Utils.get_domain() if mm_cfg.VIRTUAL_HOST_OVERVIEW and \ hostname not in mm_cfg.VIRTUAL_HOSTS: safehostname = Utils.websafe(hostname) request_creation(doc, cgidata, _('Unknown virtual host: %(safehostname)s')) return emailhost = mm_cfg.VIRTUAL_HOSTS.get(hostname, mm_cfg.DEFAULT_EMAIL_HOST) # We've got all the data we need, so go ahead and try to create the list # See admin.py for why we need to set up the signal handler. mlist = MailList.MailList() def sigterm_handler(signum, frame, mlist=mlist): # Make sure the list gets unlocked... mlist.Unlock() # ...and ensure we exit, otherwise race conditions could cause us to # enter MailList.Save() while we're in the unlocked state, and that # could be bad! sys.exit(0) try: # Install the emergency shutdown signal handler signal.signal(signal.SIGTERM, sigterm_handler) pw = sha_new(password.encode()).hexdigest() # Guarantee that all newly created files have the proper permission. # proper group ownership should be assured by the autoconf script # enforcing that all directories have the group sticky bit set oldmask = os.umask(0o02) try: try: mlist.Create(listname, owner, pw, langs, emailhost, urlhost=hostname) finally: os.umask(oldmask) except Errors.EmailAddressError as e: if e.args: s = Utils.websafe(e.args[0]) else: s = Utils.websafe(owner) request_creation(doc, cgidata, _('Bad owner email address: %(s)s')) return except Errors.MMListAlreadyExistsError: # MAS: List already exists so we don't need to websafe it. request_creation(doc, cgidata, _('List already exists: %(listname)s')) return except Errors.BadListNameError as e: if e.args: s = Utils.websafe(e.args[0]) else: s = Utils.websafe(listname) request_creation(doc, cgidata, _('Illegal list name: %(s)s')) return except Errors.MMListError: request_creation( doc, cgidata, _('''Some unknown error occurred while creating the list. Please contact the site administrator for assistance.''')) return # Initialize the host_name and web_page_url attributes, based on # virtual hosting settings and the request environment variables. mlist.default_member_moderation = moderate mlist.web_page_url = mm_cfg.DEFAULT_URL_PATTERN % hostname mlist.host_name = emailhost mlist.Save() finally: # Now be sure to unlock the list. It's okay if we get a signal here # because essentially, the signal handler will do the same thing. And # unlocking is unconditional, so it's not an error if we unlock while # we're already unlocked. mlist.Unlock() # Now do the MTA-specific list creation tasks if mm_cfg.MTA: modname = 'Mailman.MTA.' + mm_cfg.MTA __import__(modname) sys.modules[modname].create(mlist, cgi=1) # And send the notice to the list owner. if notify: siteowner = Utils.get_site_email(mlist.host_name, 'owner') text = Utils.maketext( 'newlist.txt', {'listname' : listname, 'password' : password, 'admin_url' : mlist.GetScriptURL('admin', absolute=1), 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), 'requestaddr' : mlist.GetRequestEmail(), 'siteowner' : siteowner, }, mlist=mlist) msg = Message.UserNotification( owner, siteowner, _('Your new mailing list: %(listname)s'), text, mlist.preferred_language) msg.send(mlist) # Success! listinfo_url = mlist.GetScriptURL('listinfo', absolute=1) admin_url = mlist.GetScriptURL('admin', absolute=1) create_url = Utils.ScriptURL('create') title = _('Mailing list creation results') doc.SetTitle(title) table = Table(border=0, width='100%') table.AddRow([Center(Bold(FontAttr(title, size='+1')))]) table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=mm_cfg.WEB_HEADER_COLOR) table.AddRow([_('''You have successfully created the mailing list <b>%(listname)s</b> and notification has been sent to the list owner <b>%(owner)s</b>. You can now:''')]) ullist = UnorderedList() ullist.AddItem(Link(listinfo_url, _("Visit the list's info page"))) ullist.AddItem(Link(admin_url, _("Visit the list's admin page"))) ullist.AddItem(Link(create_url, _('Create another list'))) table.AddRow([ullist]) doc.AddItem(table)