def drop_privileges(): ''' Drop root privileges and run on behalf of the specified unprivileged users. ''' passwd = getpwnam() utils_posix.chuser(passwd)
def __main(): ''' Neubot auto-updater process ''' # Process command line options logopt = syslog.LOG_PID daemonize = True try: options, arguments = getopt.getopt(sys.argv[1:], 'adv') except getopt.error: sys.exit('Usage: neubot/updater/unix.py [-adv]') if arguments: sys.exit('Usage: neubot/updater/unix.py [-adv]') check_for_updates = 0 # By default we don't check for updates for tpl in options: if tpl[0] == '-a': check_for_updates = 1 elif tpl[0] == '-d': daemonize = False elif tpl[0] == '-v': logopt |= syslog.LOG_PERROR|syslog.LOG_NDELAY # We must be run as root if os.getuid() != 0 and os.geteuid() != 0: sys.exit('FATAL: You must be root.') # Open the system logger syslog.openlog('neubot(updater)', logopt, syslog.LOG_DAEMON) # Clear root user environment utils_posix.chuser(utils_posix.getpwnam('root')) # Daemonize if daemonize: utils_posix.daemonize('/var/run/neubot.pid') # # TODO We should install a signal handler that kills # properly the child process when requested to exit # gracefully. # firstrun = True pid = -1 signal.signal(signal.SIGUSR1, __sigusr1_handler) # # Loop forever, catch and just log all exceptions. # Spend many time sleeping and wake up just once every # few seconds to make sure everything is fine. # while True: if firstrun: firstrun = False else: time.sleep(15) # Read configuration files CONFIG.update(utils_rc.parse_safe('/etc/neubot/updater')) CONFIG.update(utils_rc.parse_safe('/etc/neubot/users')) try: # If needed start the agent if pid == -1: syslog.syslog(syslog.LOG_INFO, 'Starting the agent') pid = __start_neubot_agent() # Check for updates now = time.time() updates_check_in = 1800 - (now - STATE['lastcheck']) if updates_check_in <= 0: STATE['lastcheck'] = now if check_for_updates: nversion = _download_and_verify_update() if nversion: if pid > 0: __stop_neubot_agent(pid) pid = -1 __install_new_version(nversion) __switch_to_new_version() raise RuntimeError('Internal error') # # We have not found an update, while here make # sure that we keep clean our base directory, # remove old files and directories, the tarball # of this version, etc. # else: __clear_base_directory() else: syslog.syslog(syslog.LOG_DEBUG, 'Auto-updates are disabled') elif check_for_updates: syslog.syslog(syslog.LOG_DEBUG, 'Auto-updates check in %d sec' % updates_check_in) # Monitor the agent rpid, status = __waitpid(pid, 0) if rpid == pid: pid = -1 # Signaled? if os.WIFSIGNALED(status): raise RuntimeError('Agent terminated by signal') # For robustness if not os.WIFEXITED(status): raise RuntimeError('Internal error in __waitpid()') syslog.syslog(syslog.LOG_WARNING, 'Child exited with status %d' % os.WEXITSTATUS(status)) except: try: why = asyncore.compact_traceback() syslog.syslog(syslog.LOG_ERR, 'In main loop: %s' % str(why)) except: pass
def __download(address, rpath, tofile=False, https=False, maxbytes=67108864): ''' Fork an unprivileged child that will connect to @address and download from @rpath, using https: if @https is True and http: otherwise. If @tofile is False the output is limited to 8192 bytes and returned as a string. Otherwise, if @tofile is True, the return value is the path to the file that contains the response body. ''' syslog.syslog(syslog.LOG_INFO, '__download: address=%s rpath=%s tofile=%d ' 'https=%d maxbytes=%d' % (address, rpath, tofile, https, maxbytes)) # Create communication pipe fdin, fdout = os.pipe() flags = fcntl.fcntl(fdin, fcntl.F_GETFL) flags |= os.O_NONBLOCK fcntl.fcntl(fdin, fcntl.F_SETFL, flags) if not tofile: lfdesc, lpath = -1, None else: # Build output file name basename = os.path.basename(rpath) lpath = os.sep.join([BASEDIR, basename]) # # If the output file exists and is a regular file # unlink it because it might be an old possibly failed # download attempt. # if os.path.exists(lpath): if not os.path.isfile(lpath): raise RuntimeError('%s: not a file' % lpath) os.unlink(lpath) # Open the output file (384 == 0600) lfdesc = os.open(lpath, os.O_RDWR|os.O_CREAT, 384) # Fork off a new process pid = os.fork() if pid > 0: # Close unneeded descriptors if lfdesc >= 0: os.close(lfdesc) os.close(fdout) # Wait for child process to complete status = __waitpid(pid)[1] # Read child process response try: response = os.read(fdin, 8192) except OSError: response = '' # Close communication pipe os.close(fdin) # Terminated by signal? if os.WIFSIGNALED(status): syslog.syslog(syslog.LOG_ERR, 'Child terminated by signal %d' % os.WTERMSIG(status)) return None # For robustness if not os.WIFEXITED(status): raise RuntimeError('Internal error in __waitpid()') # Failure? if os.WEXITSTATUS(status) != 0: error = __printable_only(response.replace('ERROR ', '', 1)) syslog.syslog(syslog.LOG_ERR, 'Child error: %s' % error) return None # Is output a file? if tofile: syslog.syslog(syslog.LOG_ERR, 'Response saved to: %s' % lpath) return lpath # # Output inline # NOTE The caller is expected to validate the result # using regular expression. Here we use __printable_only # for safety. # result = response.replace('OK ', '', 1) syslog.syslog(syslog.LOG_ERR, 'Response is: %s' % __printable_only(result)) return result else: # # The child code is surrounded by this giant try..except # because what is interesting for us is the child process # exit status (plus eventually the reason). # try: # Lookup unprivileged user info passwd = utils_posix.getpwnam(CONFIG['update_user']) # Become unprivileged as soon as possible utils_posix.chuser(passwd) if os.getuid() == 0 or os.geteuid() == 0: raise RuntimeError('Has not dropped privileges') # Close all unneeded file descriptors for tmpdesc in range(64): if tmpdesc == lfdesc or tmpdesc == fdout: continue try: os.close(tmpdesc) except OSError: pass except: pass # Ensure stdio point to something for _ in range(3): os.open('/dev/null', os.O_RDWR) # Send HTTP request if https: connection = __lib_http.HTTPSConnection(address) else: connection = __lib_http.HTTPConnection(address) headers = {'User-Agent': utils_version.HTTP_HEADER} connection.request("GET", rpath, None, headers) # Recv HTTP response response = connection.getresponse() if response.status != 200: raise RuntimeError('HTTP response: %d' % response.status) # Need to write response body to file? if tofile: assert(lfdesc >= 0) total = 0 while True: # Read a piece of response body data = response.read(262144) if not data: break # Enforce maximum response size total += len(data) if total > maxbytes: raise RuntimeError('Response is too big') # Copy to output descriptor os.write(lfdesc, data) # Close I/O channels os.close(lfdesc) connection.close() # Notify parent os.write(fdout, 'OK\n') else: vector = [] total = 0 while True: data = response.read(262144) if not data: break vector.append(data) total += len(data) if total > 8192: raise RuntimeError('Response is too big') connection.close() os.write(fdout, 'OK %s\n' % ''.join(vector)) except: try: why = asyncore.compact_traceback() os.write(fdout, 'ERROR %s\n' % str(why)) except: pass __exit(1) else: __exit(0)