1#
2#Copyright (C) 2003, 2004, 2005, 2006, 2007, 2009, 2010, 2011, 2018 Olivier Sessink
3#All rights reserved.
4#
5#Redistribution and use in source and binary forms, with or without
6#modification, are permitted provided that the following conditions
7#are met:
8#  * Redistributions of source code must retain the above copyright
9#    notice, this list of conditions and the following disclaimer.
10#  * Redistributions in binary form must reproduce the above
11#    copyright notice, this list of conditions and the following
12#    disclaimer in the documentation and/or other materials provided
13#    with the distribution.
14#  * The names of its contributors may not be used to endorse or
15#    promote products derived from this software without specific
16#    prior written permission.
17#
18#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19#"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20#LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21#FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22#COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23#INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25#LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27#LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28#ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29#POSSIBILITY OF SUCH DAMAGE.
30#
31
32from __future__ import print_function
33
34import os.path
35import string
36#import os
37import sys
38import stat
39import shutil
40import glob
41import subprocess
42
43
44dir_mode = 493								#octal 755, "rwxr-xr-x"
45
46statcache = {}
47
48def cachedlstat(path):
49	ret = statcache.get(path, None)
50	if ret is None:
51		statcache[path] = ret = os.lstat(path)
52	return ret
53
54#def nextpathup(path):
55#	#if (path[-1:] == '/'):
56#	#	path = path[:-1]
57#	try:
58#		#print('path='+path)
59#		indx = string.rindex(path,'/')
60#		if (indx > 0):
61#			return path[:indx]
62#	except ValueError:
63#		pass
64#	return None
65
66def path_is_safe(path, failquiet=0):
67	try:
68		statbuf = cachedlstat(path)
69	except OSError:
70		if (failquiet == 0):
71			sys.stderr.write('ERROR: cannot lstat() '+path+'\n')
72		return -1
73	if (sys.platform[-3:] == 'bsd'):
74		# on freebsd root is in group wheel
75		if (statbuf[stat.ST_UID] != 0 or statbuf[stat.ST_GID] != grp.getgrnam('wheel').gr_gid):
76			sys.stderr.write('ERROR: '+path+' is not owned by root:wheel!\n')
77			return -3
78	else:
79		if (statbuf[stat.ST_UID] != 0 or statbuf[stat.ST_GID] != 0):
80			sys.stderr.write('ERROR: '+path+' is not owned by root:root!\n')
81			return -3
82	if ((statbuf[stat.ST_MODE] & stat.S_IWOTH or statbuf[stat.ST_MODE] & stat.S_IWGRP)and not stat.S_ISLNK(statbuf[stat.ST_MODE])):
83		sys.stderr.write('ERROR: '+path+' is writable by group or others!')
84		return -4
85	if (not stat.S_ISDIR(statbuf[stat.ST_MODE])):
86		if (stat.S_ISLNK(statbuf[stat.ST_MODE])):
87			# Fedora has moved /sbin /lib and /bin into /usr
88			target = os.readlink(path)
89			print(str(target) + str(path))
90			if (target[:4] != 'usr/'):
91				sys.stderr.write('ERROR: '+path+' is a symlink, please point to the real directory\n')
92				return -2
93		else:
94			sys.stderr.write('ERROR: '+path+' is not a directory!\n')
95			return -2
96	return 1
97
98def chroot_is_safe(path, failquiet=0):
99	"""tests if path is a safe jail, not writable, no writable /etc/ and /lib, return 1 if all is OK"""
100	path = os.path.abspath(path)
101	retval = path_is_safe(path,failquiet)
102	if (retval < -1):
103		return retval
104	for subd in 'etc','usr','var','bin','dev','proc','sbin','sys':
105		retval = path_is_safe(path+'/'+subd,1)
106		if (retval < -1):
107			return retval
108	npath = os.path.dirname(path)
109	while (npath != '/'):
110		#print(npath)
111		retval = path_is_safe(npath,0)
112		if (retval != 1):
113#			print('testing path='+npath+'returned '+str(retval))
114			return retval
115		npath = os.path.dirname(npath)
116	return 1
117
118def test_suid_sgid(path):
119	"""returns 1 if the file is setuid or setgid, returns 0 if it is not"""
120	statbuf = cachedlstat(path)
121	if (statbuf[stat.ST_MODE] & (stat.S_ISUID | stat.S_ISGID)):
122		return 1
123	return 0
124
125def gen_library_cache(jail):
126	if (sys.platform[:5] == 'linux'):
127		create_parent_path(jail,'/etc', 0, copy_permissions=0, allow_suid=0, copy_ownership=0)
128		os.system('ldconfig -r '+jail)
129
130
131def lddlist_libraries_linux(executable):
132	"""returns a list of libraries that the executable depends on """
133	retval = []
134	p = subprocess.Popen('ldd '+executable, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
135	line = p.stdout.readline()
136	#pd = os.popen3('ldd '+executable)
137	#line = pd[1].readline()
138	if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
139		line = line.decode('utf-8', 'replace')
140	while (len(line)>0):
141		subl = line.split()
142		#print('parse line',subl,'with len',len(subl))
143		if (len(subl)>0):
144			if (subl[0] == 'statically' and subl[1] == 'linked'):
145				return retval
146			elif (subl[0] == 'not' and subl[2] == 'dynamic' and subl[3] == 'executable'):
147				return retval
148			elif (subl[0] == 'linux-gate.so.1' or subl[0] == 'linux-vdso.so.1'):
149				pass
150			elif (len(subl)==4 and subl[2] == 'not' and subl[3] == 'found'):
151				pass
152			elif (len(subl)>=3):
153				if (os.path.exists(subl[2])):
154					retval += [subl[2]]
155				else:
156					print('ldd returns non existing library '+subl[2]+' for '+executable)
157			# on gentoo amd64 the last entry of ldd looks like '/lib64/ld-linux-x86-64.so.2 (0x0000002a95556000)'
158			elif (len(subl)>=1 and subl[0][0] == '/'):
159				if (os.path.exists(subl[0])):
160					retval += [subl[0]]
161				else:
162					print('ldd returns non existing library '+subl[0])
163			else:
164				print('WARNING: failed to parse ldd output '+line[:-1])
165		else:
166			print('WARNING: failed to parse ldd output '+line[:-1])
167		#line = pd[1].readline()
168		line = p.stdout.readline()
169		if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
170			line = line.decode('utf-8', 'replace')
171	return retval
172
173def lddlist_libraries_openbsd(executable):
174	"""returns a list of libraries that the executable depends on """
175	retval = []
176	mode = 3 # openbsd 4 has new ldd output
177	p = subprocess.Popen('ldd '+executable, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
178	line = p.stdout.readline()
179#	pd = os.popen3('ldd '+executable)
180#	line = pd[1].readline()
181	if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
182		line = line.decode('utf-8', 'replace')
183	while (len(line)>0):
184		subl = line.split()
185		if (len(subl)>0):
186			if (subl[0] == executable+':'):
187				pass
188			elif (subl[0] == 'Start'):
189				if (len(subl)==7 and subl[6] == 'Name'):
190					mode = 4
191				pass
192			elif (len(subl)>=5):
193				if (mode == 3):
194					if (os.path.exists(subl[4])):
195						retval += [subl[4]]
196					else:
197						print('ldd returns non existing library '+subl[4])
198				elif (mode == 4):
199					if (os.path.exists(subl[6])):
200						retval += [subl[6]]
201					else:
202						print('ldd returns non existing library '+subl[6])
203				else:
204					print('unknown mode, please report this bug in jk_lib.py')
205			else:
206				print('WARNING: failed to parse ldd output '+line[:-1])
207		else:
208			print('WARNING: failed to parse ldd output '+line[:-1])
209		line = p.stdout.readline()
210		#line = pd[1].readline()
211		if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
212			line = line.decode('utf-8', 'replace')
213	return retval
214
215def lddlist_libraries_freebsd(executable):
216	"""returns a list of libraries that the executable depends on """
217	retval = []
218	p = subprocess.Popen('ldd '+executable, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
219	line = p.stdout.readline()
220	#pd = os.popen3('ldd '+executable)
221	#line = pd[1].readline()
222	if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
223		line = line.decode('utf-8', 'replace')
224	while (len(line)>0):
225		subl = line.split()
226		if (len(subl)>0):
227			if (len(subl)==1 and subl[0][:len(executable)+1] == executable+':'):
228				pass
229			elif (len(subl)>=6 and subl[2] == 'not' and subl[4] == 'dynamic'):
230				return retval
231			elif (len(subl)>=4):
232				if (os.path.exists(subl[2])):
233					retval += [subl[2]]
234				else:
235					print('ldd returns non existing library '+subl[2])
236			else:
237				print('WARNING: failed to parse ldd output "'+line[:-1]+'"')
238		elif (line[:len(executable)+1] == executable+':'):
239			pass
240		else:
241			print('WARNING: failed to parse ldd output "'+line[:-1]+'"')
242		line = p.stdout.readline()
243		#line = pd[1].readline()
244		if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
245			line = line.decode('utf-8', 'replace')
246	return retval
247
248def lddlist_libraries(executable):
249	if (sys.platform[:5] == 'linux'):
250		return lddlist_libraries_linux(executable)
251	elif (sys.platform[:7] == 'openbsd'):
252		return lddlist_libraries_openbsd(executable)
253	elif (sys.platform[:7] == 'freebsd'):
254		return lddlist_libraries_freebsd(executable)
255	elif (sys.platform[:5] == 'sunos'):
256		retval = lddlist_libraries_linux(executable)
257		retval += ['/lib/ld.so.1']
258		return retval
259	else:
260		retval = lddlist_libraries_linux(executable)
261		retval += ['/usr/libexec/ld.so','/usr/libexec/ld-elf.so.1','/libexec/ld-elf.so.1']
262		return retval
263
264def resolve_realpath(path, chroot='', include_file=0):
265	if (path=='/'):
266		return '/'
267	spath = split_path(path)
268	basename = ''
269	if (not include_file):
270		basename = spath[-1]
271		spath = spath[:-1]
272	ret = '/'
273	doscounter=0# a symlink loop may otherwise hang this script
274	#print('path' + path + 'spath' + spath)
275	for entry in spath:
276		ret = os.path.join(ret,entry)
277		#print('lstat' + ret)
278		sb = cachedlstat(ret)
279		if (stat.S_ISLNK(sb.st_mode)):
280			doscounter+=1
281			realpath = os.readlink(ret)
282			if (realpath[0]=='/'):
283				ret = os.path.normpath(chroot+realpath)
284			else:
285				tmp = os.path.normpath(os.path.join(os.path.dirname(ret),realpath))
286				if (len(chroot)>0 and tmp[:len(chroot)]!=chroot):
287					sys.stderr.write('ERROR: symlink '+tmp+' points outside jail, ABORT\n')
288					raise Exception("Symlink points outside jail")
289				ret = tmp
290	return os.path.join(ret,basename)
291
292# os.path.realpath() seems to do the same:
293# NO: it cannot handle symlink resolving WITH a chroot
294def OLDresolve_realpath(path, chroot='', include_file=0):
295	"""will return the same path that contains not a single symlink directory element"""
296	chrootlen=len(chroot)
297	if (include_file):
298		donepath = ''
299		todopath = path
300	else:
301		donepath = os.path.basename(path)
302		todopath = os.path.dirname(path)
303	doscounter=0 # a symlink loop may otherwise hang this script
304	while (todopath != '/' and doscounter != 100):
305		#print('todopath=' + todopath + 'donepath=' + donepath)
306		sb = cachedlstat(todopath)
307		if (stat.S_ISLNK(sb.st_mode)):
308			doscounter += 1
309			realpath = os.readlink(todopath)
310			if (realpath[0]=='/'):
311				todopath = chroot+realpath
312				if (todopath[-1:]=='/'):
313					todopath = todopath[:-1]
314			else:
315				tmp = os.path.normpath(os.path.join(os.path.dirname(todopath),realpath))
316				if (chrootlen>0 and tmp[:chrootlen]!=chroot):
317					sys.stderr.write('ERROR: symlink '+tmp+' points outside jail, ABORT\n')
318					raise Exception("Symlink points outside jail")
319				todopath=tmp
320		else:
321			donepath = os.path.basename(todopath)+'/'+donepath
322			todopath = os.path.dirname(todopath)
323		sb=None
324	return '/'+donepath
325
326def copy_time_and_permissions(src, dst, be_verbose=0, allow_suid=0, copy_ownership=0):
327	# the caller should catch any exceptions!
328# similar to shutil.copymode(src, dst) but we do not copy any SUID bits
329	sbuf = os.stat(src)
330#	in python 2.1 the return value is a tuple, not an object, st_mode is field 0
331#	mode = stat.S_IMODE(sbuf.st_mode)
332	mode = stat.S_IMODE(sbuf[stat.ST_MODE])
333	if (not allow_suid):
334		if (mode & (stat.S_ISUID | stat.S_ISGID)):
335			print('removing setuid and setgid permissions from '+dst)
336#			print('setuid!!! mode='+str(mode))
337			mode = (mode & ~stat.S_ISUID) & ~stat.S_ISGID
338#			print('setuid!!! after mode='+str(mode))
339#		print('mode='+str(mode))
340	os.utime(dst, (sbuf[stat.ST_ATIME], sbuf[stat.ST_MTIME]))
341	if (copy_ownership):
342		os.chown(dst, sbuf[stat.ST_UID], sbuf[stat.ST_GID])
343	#WARNING: chmod must be AFTER chown to preserve setuid/setgid bits
344	os.chmod(dst, mode)
345
346
347def OLDreturn_existing_base_directory(path):
348	"""This function tests if a directory exists, if not tries the parent etc. etc. until it finds a directory that exists"""
349	tmp = path
350	while (not os.path.exists(tmp) and not (tmp == '/' or tmp=='')):
351		tmp = os.path.dirname(tmp)
352	return tmp
353
354def OLD_create_parent_path(chroot, path, be_verbose=0, copy_permissions=1, allow_suid=0, copy_ownership=0):
355	"""creates the directory and all its parents id needed. copy_ownership can only be used if copy permissions is also used"""
356	directory = path
357	if (directory[-1:] == '/'):
358		directory = directory[:-1]
359	# TODO, this function cannot yet handle if /lib in the jail is a symlink to something else
360	try:
361		chrootdirectory = resolve_realpath(chroot+directory,chroot)
362		if (os.path.exists(chrootdirectory)):
363			return
364	except OSError:
365		pass
366	tmp = return_existing_base_directory(chroot+directory)
367	oldindx = len(tmp)-len(chroot)
368	# find the first slash after the existing directories
369	indx = directory.find('/',oldindx+1)
370	if (indx == -1 ):
371		indx=len(directory)
372	while (indx != -1):
373		# avoid the /bla//bla pitfall
374		if (oldindx +1 == indx):
375			oldindx = indx
376		else:
377			try:
378				sb = cachedlstat(directory[:indx])
379			except OSError as e:
380				_, strerror = e.args
381				sys.stderr.write('ERROR: failed to lstat('+directory[:indx]+'):'+strerror+'\n')
382				break
383			if (stat.S_ISLNK(sb.st_mode)):
384				# create the link, create the target, and then continue
385				realfile = os.readlink(directory[:indx])
386				chrootname = resolve_realpath(chroot+directory[:indx],chroot)
387				if (be_verbose):
388					print('Creating symlink '+chrootname+' to '+realfile)
389				try:
390					os.symlink(realfile, chrootname)
391				except OSError as e:
392					errno, _ = e.args
393					if (errno == 17): # file exists
394						pass
395					else:
396						sys.stderr.write('ERROR: failed to create symlink '+chroot+directory[:indx]+'\n');
397				if (realfile[0]=='/'):
398					create_parent_path(chroot, realfile, be_verbose, copy_permissions, allow_suid, copy_ownership)
399				else:
400					indx2 = string.rfind(directory[:indx],'/')
401#					print('try' + directory[:indx2+1]+realfile)
402					create_parent_path(chroot, directory[:indx2+1]+realfile, be_verbose, copy_permissions, allow_suid, copy_ownership)
403			elif (stat.S_ISDIR(sb.st_mode)):
404				chrootname = resolve_realpath(chroot+directory[:indx],chroot)
405				if (be_verbose):
406					print('Creating directory '+chrootname)
407				os.mkdir(chrootname, dir_mode)
408				if (copy_permissions):
409					try:
410						copy_time_and_permissions(directory[:indx], chrootname, be_verbose, allow_suid, copy_ownership)
411					except OSError as e:
412						_, strerror = e.args
413						sys.stderr.write('ERROR: failed to copy time/permissions/owner from '+directory[:indx]+' to '+chrootname+': '+strerror+'\n')
414			oldindx = indx
415		if (indx==len(directory)):
416			indx=-1
417		else:
418			indx = directory.find('/',oldindx+1)
419			if (indx==-1):
420				indx=len(directory)
421
422def fix_double_slashes(instring):
423	outstring=''
424	slash=0
425	for i in instring:
426		if (slash==0 or i!= '/'):
427			outstring += i
428		if (i == '/'):
429			slash=1
430		else:
431			slash=0
432	return outstring
433
434def split_path(path):
435	ret = []
436	next=fix_double_slashes(os.path.normpath(path))
437	while (next != '/'):
438		ret.insert(0,os.path.basename(next))
439		next=os.path.dirname(next)
440	return ret
441
442def join_path(spath):
443	if (len(spath)==0):
444		return '/'
445	ret = ''
446	for entry in spath:
447		ret += '/'+entry
448	return ret
449
450def create_parent_path(chroot,path,be_verbose=0, copy_permissions=1, allow_suid=0, copy_ownership=0):
451	#print('create_parent_path, path' + path + 'in chroot' + chroot)
452	# the first part of the function checks the already existing paths in the jail
453	# and follows any symlinks relative to the jail
454	spath = split_path(path)
455	existpath = chroot
456	i=0
457	while (i<len(spath)):
458		tmp1 = os.path.join(existpath,spath[i])
459		if not os.path.exists(tmp1):
460			#print('tmp1 does not exist' + tmp1)
461			break
462		tmp = resolve_realpath(tmp1,chroot,1)
463		if not os.path.exists(tmp):
464			#print('tmp does not exist' + tmp)
465			break
466		#print('tmp exists:' + tmp)
467		existpath = tmp
468		i+=1
469	#print('existpath result:' + existpath + 'i=' + i)
470	# the second part of the function creates the missing parts in the jail
471	# according to the original directory names, including any symlinks
472	while (i<len(spath)):
473		origpath = join_path(spath[0:i+1])
474		jailpath = os.path.join(existpath,spath[i])
475		#print('origpath' + origpath + 'jailpath' + jailpath)
476		try:
477			sb = cachedlstat(origpath)
478		except OSError as e:
479			_, strerror = e.args
480			sys.stderr.write('ERROR: failed to lstat('+origpath+'):'+strerror+'\n')
481			return None
482		if (stat.S_ISDIR(sb.st_mode)):
483			if (be_verbose):
484				print('Create directory '+jailpath)
485			os.mkdir(jailpath, dir_mode)
486			if (copy_permissions):
487				try:
488					copy_time_and_permissions(origpath, jailpath, be_verbose, allow_suid, copy_ownership)
489				except OSError as e:
490					_, strerror = e.args
491					sys.stderr.write('ERROR: failed to copy time/permissions/owner from '+directory[:indx]+' to '+chrootname+': '+strerror+'\n')
492		elif (stat.S_ISLNK(sb.st_mode)):
493			realfile = os.readlink(origpath)
494			if (be_verbose):
495				print('Creating symlink '+jailpath+' to '+realfile)
496			os.symlink(realfile,jailpath)
497			if (realfile[0]=='/'):
498				jailpath = create_parent_path(chroot, realfile,be_verbose, copy_permissions, allow_suid, copy_ownership)
499			else:
500				tmp = os.path.normpath(os.path.join(os.path.dirname(jailpath),realfile))
501				#print('symlink resolves to ' + tmp)
502				if (len(chroot)>0 and tmp[:len(chroot)]!=chroot):
503					sys.stderr.write('ERROR: symlink '+tmp+' points outside jail, ABORT\n')
504					raise Exception("Symlink points outside jail")
505				realfile = tmp[len(chroot):]
506				jailpath = create_parent_path(chroot, realfile,be_verbose, copy_permissions, allow_suid, copy_ownership)
507		existpath = jailpath
508		#print('new value for existpath is' + existpath)
509		i+=1
510	return existpath
511
512def copy_dir_with_permissions_and_owner(srcdir,dstdir,be_verbose=0):
513	# used to **move** home directories into the jail
514	#create directory dstdir
515	try:
516		if (be_verbose):
517			print('Creating directory'+dstdir)
518		os.mkdir(dstdir)
519		copy_time_and_permissions(srcdir, dstdir, be_verbose, allow_suid=0, copy_ownership=1)
520	except (IOError, OSError) as e:
521		_, strerror = e.args
522		sys.stderr.write('ERROR: copying directory and permissions '+srcdir+' to '+dstdir+': '+strerror+'\n')
523		return 0
524	for root, dirs, files in os.walk(srcdir):
525		for name in files:
526			if (be_verbose):
527				print('Copying '+root+'/'+name+' to '+dstdir+'/'+name)
528			try:
529				shutil.copyfile(root+'/'+name,dstdir+'/'+name)
530				copy_time_and_permissions(root+'/'+name, dstdir+'/'+name, be_verbose, allow_suid=0, copy_ownership=1)
531			except (IOError,OSError) as e:
532				_, strerror = e.args
533				sys.stderr.write('ERROR: copying file and permissions '+root+'/'+name+' to '+dstdir+'/'+name+': '+strerror+'\n')
534				return 0
535		for name in dirs:
536			move_dir_with_permissions_and_owner(root+'/'+name,dstdir+'/'+name,be_verbose)
537	return 1
538
539def move_dir_with_permissions_and_owner(srcdir,dstdir,be_verbose=0):
540	retval = copy_dir_with_permissions_and_owner(srcdir,dstdir,be_verbose)
541	if (retval == 1):
542		# remove the source directory
543		if (be_verbose==1):
544			print('Removing original home directory '+srcdir)
545		try:
546			shutil.rmtree(srcdir)
547		except (OSError, IOError) as e:
548			_, strerror = e.args
549			sys.stderr.write('ERROR: failed to remove '+srcdir+': '+strerror+'\n')
550	else:
551		print('Not everything was copied to '+dstdir+', keeping the old directory '+srcdir)
552
553def copy_with_permissions(src, dst, be_verbose=0, try_hardlink=1, allow_suid=0, retain_owner=0):
554	"""copies/links the file and the permissions (and possibly ownership and setuid/setgid bits"""
555	do_normal_copy = 1
556	if (try_hardlink==1):
557		try:
558			os.link(src,dst)
559			do_normal_copy = 0
560		except:
561			print('Linking '+src+' to '+dst+' failed, will revert to copying')
562			pass
563	if (do_normal_copy == 1):
564		try:
565			shutil.copyfile(src,dst)
566			copy_time_and_permissions(src, dst, be_verbose, allow_suid=allow_suid, copy_ownership=retain_owner)
567		except (IOError, OSError) as e:
568			_, strerror = e.args
569			sys.stderr.write('ERROR: copying file and permissions '+src+' to '+dst+': '+strerror+'\n')
570
571def copy_device(chroot, path, be_verbose=1, retain_owner=0):
572	# perhaps the calling function should make sure the basedir exists
573	create_parent_path(chroot,os.path.dirname(path), be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=0)
574	chrootpath = resolve_realpath(chroot+path,chroot)
575	if (os.path.exists(chrootpath)):
576		print('Device '+chrootpath+' does exist already')
577		return
578	sb = os.stat(path)
579	try:
580		if (sys.platform[:5] == 'linux'):
581			major = sb.st_rdev / 256 #major = st_rdev divided by 256 (8bit reserved for the minor number)
582			minor = sb.st_rdev % 256 #minor = remainder of st_rdev divided by 256
583		elif (sys.platform == 'sunos5'):
584			if (sys.maxint == 2147483647):
585				major = sb.st_rdev / 262144 #major = st_rdev divided by 256 (18 bits reserved for the minor number)
586				minor = sb.st_rdev % 262144 #minor = remainder of st_rdev divided by 256
587			else:
588				#64 bit solaris has 32 bit minor/32bit major
589				major = sb.st_rdev / 2147483647
590				minor =  sb.st_rdev % 2147483647
591		else:
592			major = sb.st_rdev / 256 #major = st_rdev divided by 256
593			minor = sb.st_rdev % 256 #minor = remainder of st_rdev divided by 256
594		if (stat.S_ISCHR(sb.st_mode)):
595			mode = 'c'
596		elif (stat.S_ISBLK(sb.st_mode)):
597			mode = 'b'
598		else:
599			print('WARNING, '+path+' is not a character or block device')
600			return 1
601		if (be_verbose==1):
602			print('Creating device '+chroot+path)
603		ret = os.spawnlp(os.P_WAIT, 'mknod','mknod', chrootpath, str(mode), str(major), str(minor))
604		copy_time_and_permissions(path, chrootpath, allow_suid=0, copy_ownership=retain_owner)
605	except:
606		print('Failed to create device '+chrootpath+', this is a know problem with python 2.1')
607		print('use "ls -l '+path+'" to find out the mode, major and minor for the device')
608		print('use "mknod '+chrootpath+' mode major minor" to create the device')
609		print('use chmod and chown to set the permissions as found by ls -l')
610
611def copy_dir_recursive(chroot,dir,force_overwrite=0, be_verbose=0, check_libs=1, try_hardlink=1, allow_suid=0, retain_owner=0, handledfiles=[]):
612	"""copies a directory and the permissions recursively, possibly with ownership and setuid/setgid bits"""
613	files2 = ()
614	for entry in os.listdir(dir):
615		tmp = os.path.join(dir, entry)
616		try:
617			sbuf = cachedlstat(tmp)
618			if (stat.S_ISDIR(sbuf.st_mode)):
619				create_parent_path(chroot, tmp, be_verbose=be_verbose, copy_permissions=1, allow_suid=allow_suid, copy_ownership=retain_owner)
620				handledfiles = copy_dir_recursive(chroot,tmp,force_overwrite, be_verbose, check_libs, try_hardlink, allow_suid, retain_owner, handledfiles)
621			else:
622				files2 += os.path.join(dir, entry),
623		except OSError as e:
624			sys.stderr.write('ERROR: failed to investigate source file '+tmp+': '+e.strerror+'\n')
625	handledfiles = copy_binaries_and_libs(chroot,files2,force_overwrite, be_verbose, check_libs, try_hardlink, allow_suid, retain_owner, handledfiles)
626	return handledfiles
627#	for root, dirs, files in os.walk(dir):
628#		files2 = ()
629#		for name in files:
630#			files2 += os.path.join(root, name),
631#		handledfiles = copy_binaries_and_libs(chroot,files2,force_overwrite, be_verbose, check_libs, try_hardlink, retain_owner, handledfiles)
632#		for name in dirs:
633#			tmp = os.path.join(root, name)
634#			create_parent_path(chroot, tmp, be_verbose=be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=retain_owner)
635#			handledfiles = copy_dir_recursive(chroot,os.path.join(root, name),force_overwrite, be_verbose, check_libs, try_hardlink, retain_owner, handledfiles)
636#	return handledfiles
637
638
639# there is a very tricky situation for this function:
640# suppose /srv/jail/opt/bin is a symlink to /usr/bin
641# try to lstat(/srv/jail/opt/bin/foo) and you get the result for /usr/bin/foo
642# so use resolve_realpath to find you want lstat(/srv/jail/usr/bin/foo)
643#
644def copy_binaries_and_libs(chroot, binarieslist, force_overwrite=0, be_verbose=0, check_libs=1, try_hardlink=1, allow_suid=0, retain_owner=0, try_glob_matching=0, handledfiles=[]):
645	"""copies a list of executables and their libraries to the chroot"""
646	if (chroot[-1] == '/'):
647		chroot = chroot[:-1]
648	for file in binarieslist:
649		if (file in handledfiles):
650			continue
651
652		try:
653			sb = cachedlstat(file)
654		except OSError as e:
655			if (e.errno == 2):
656				if (try_glob_matching == 1):
657					ret = glob.glob(file)
658					if (len(ret)>0):
659						handledfiles = copy_binaries_and_libs(chroot, ret, force_overwrite, be_verbose, check_libs, try_hardlink=try_hardlink, retain_owner=retain_owner, try_glob_matching=0, handledfiles=handledfiles)
660					elif (be_verbose):
661						print('Source file(s) '+file+' do not exist')
662				elif (be_verbose):
663					print('Source file '+file+' does not exist')
664			else:
665				sys.stderr.write('ERROR: failed to investigate source file '+file+': '+e.strerror+'\n')
666			continue
667		# source file exists, resolve the chroot realfile
668		create_parent_path(chroot,os.path.dirname(file), be_verbose, copy_permissions=1, allow_suid=allow_suid, copy_ownership=retain_owner)
669		chrootrfile = resolve_realpath(os.path.normpath(chroot+'/'+file),chroot)
670
671		try:
672			chrootsb = cachedlstat(chrootrfile)
673			chrootfile_exists = 1
674		except OSError as e:
675			if (e.errno == 2):
676				chrootfile_exists = 0
677			else:
678				sys.stderr.write('ERROR: failed to investigate destination file '+chroot+file+': '+e.strerror+'\n')
679
680		if ((force_overwrite == 0) and chrootfile_exists and not stat.S_ISDIR(chrootsb.st_mode)):
681			if (be_verbose):
682				print(''+chrootrfile+' already exists, will not touch it')
683		else:
684			if (chrootfile_exists):
685				if (force_overwrite):
686					if (stat.S_ISREG(chrootsb.st_mode)):
687						if (be_verbose):
688							print('Destination file '+chrootrfile+' exists, will delete to force update')
689						try:
690							os.unlink(chrootrfile)
691						except OSError as e:
692							sys.stderr.write('ERROR: failed to delete '+chrootrfile+': '+e.strerror+'\ncannot force update '+chrootrfile+'\n')
693							# BUG: perhaps we can fix the permissions so we can really delete the file?
694							# but what permissions cause this error?
695					elif (stat.S_ISDIR(chrootsb.st_mode)):
696						print('Destination dir '+chrootrfile+' exists')
697				else:
698					if (stat.S_ISDIR(chrootsb.st_mode)):
699						pass
700						# for a directory we also should inspect all the contents, so we do not
701						# skip to the next item of the loop
702					else:
703						if (be_verbose):
704							print('Destination file '+chrootrfile+' exists')
705						continue
706			create_parent_path(chroot,os.path.dirname(file), be_verbose, copy_permissions=1, allow_suid=allow_suid, copy_ownership=retain_owner)
707			if (stat.S_ISLNK(sb.st_mode)):
708				realfile = os.readlink(file)
709				if (chrootfile_exists):
710					if (be_verbose):
711						print('Destination symlink '+chrootrfile+' exists, will delete to force update')
712					try:
713						os.unlink(chrootrfile)
714					except OSError as e:
715						sys.stderr.write('ERROR: failed to delete '+chrootrfile+': '+e.strerror+'\ncannot force update '+chrootrfile+'\n')
716				try:
717					print('Creating symlink '+chrootrfile+' to '+realfile)
718					os.symlink(realfile, chrootrfile)
719				except OSError:
720					# if the file exists already, check if it is correct
721
722					pass
723				handledfiles.append(file)
724				if (realfile[0] != '/'):
725					realfile = os.path.normpath(os.path.join(os.path.dirname(file),realfile))
726				handledfiles = copy_binaries_and_libs(chroot, [realfile], force_overwrite, be_verbose, check_libs, try_hardlink, allow_suid, retain_owner, handledfiles)
727			elif (stat.S_ISDIR(sb.st_mode)):
728				handledfiles = copy_dir_recursive(chroot,file,force_overwrite, be_verbose, check_libs, try_hardlink, allow_suid, retain_owner, handledfiles)
729			elif (stat.S_ISREG(sb.st_mode)):
730				if (try_hardlink):
731					print('Trying to link '+file+' to '+chrootrfile)
732				else:
733					print('Copying '+file+' to '+chrootrfile)
734				copy_with_permissions(file,chrootrfile,be_verbose, try_hardlink, allow_suid, retain_owner)
735				handledfiles.append(file)
736			elif (stat.S_ISCHR(sb.st_mode) or stat.S_ISBLK(sb.st_mode)):
737				copy_device(chroot, file, be_verbose, retain_owner)
738			else:
739				sys.stderr.write('Failed to find how to copy '+file+' into a chroot jail, please report to the Jailkit developers\n')
740#	in python 2.1 the return value is a tuple, not an object, st_mode is field 0
741#	mode = stat.S_IMODE(sbuf.st_mode)
742			mode = stat.S_IMODE(sb[stat.ST_MODE])
743			if (check_libs and (file.find('lib') != -1 or file.find('.so') != -1 or (mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)))):
744				libs = lddlist_libraries(file)
745				handledfiles = copy_binaries_and_libs(chroot, libs, force_overwrite, be_verbose, 0, try_hardlink, handledfiles=handledfiles)
746	return handledfiles
747
748def config_get_option_as_list(cfgparser, sectionname, optionname):
749	"""retrieves a comma separated option from the configparser and splits it into a list, returning an empty list if it does not exist"""
750	retval = []
751	if (cfgparser.has_option(sectionname,optionname)):
752		inputstr = cfgparser.get(sectionname,optionname)
753		for tmp in inputstr.split(','):
754			retval += [tmp.strip()]
755	return retval
756
757def clean_exit(exitno,message,usagefunc,type='ERROR'):
758	print('')
759	print(type+': '+message)
760	usagefunc()
761	sys.exit(exitno)
762
763def test_numitem_exist(item,num,filename):
764	try:
765		fd = open(filename,'r')
766	except:
767		#print(''+filename+' does not exist')
768		return 0
769	line = fd.readline()
770	if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
771		line = line.decode('utf-8', 'replace')
772	while (len(line)>0):
773		pwstruct = line.split(':')
774		#print('len pwstruct='+str(len(pwstruct))+' while looking for '+item)
775		if (len(pwstruct) > num and pwstruct[num] == item):
776			fd.close()
777			return 1
778		line = fd.readline()
779		if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
780			line = line.decode('utf-8', 'replace')
781	return 0
782
783def test_user_exist(user, passwdfile):
784	return test_numitem_exist(user,0,passwdfile)
785
786def test_group_exist(group, groupfile):
787	return test_numitem_exist(group,0,groupfile)
788
789def init_passwd_and_group(chroot,users,groups,be_verbose=0):
790	if (chroot[-1] == '/'):
791		chroot = chroot[:-1]
792	create_parent_path(chroot,'/etc/', be_verbose, copy_permissions=0, allow_suid=0, copy_ownership=0)
793	if (sys.platform[4:7] == 'bsd'):
794		open(chroot+'/etc/passwd','a').close()
795		open(chroot+'/etc/spwd.db','a').close()
796		open(chroot+'/etc/pwd.db','a').close()
797		open(chroot+'/etc/master.passwd','a').close()
798	else:
799		if (not os.path.isfile(chroot+'/etc/passwd')):
800			fd2 = open(chroot+'/etc/passwd','w')
801		else:
802	# the chroot passwds file exists, check if any of the users exist already
803			fd2 = open(chroot+'/etc/passwd','r+')
804			line = fd2.readline()
805			if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
806				line = line.decode('utf-8', 'replace')
807			while (len(line)>0):
808				pwstruct = line.split(':')
809				if (len(pwstruct) >=3):
810					if ((pwstruct[0] in users) or (pwstruct[2] in users)):
811						if (be_verbose):
812							print('user '+pwstruct[0]+' exists in '+chroot+'/etc/passwd')
813						try:
814							users.remove(pwstruct[0])
815						except ValueError:
816							pass
817						try:
818							users.remove(pwstruct[2])
819						except ValueError:
820							pass
821				line = fd2.readline()
822				if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
823					line = line.decode('utf-8', 'replace')
824			fd2.seek(0,2)
825		if (len(users) > 0):
826			fd = open('/etc/passwd','r')
827			line = fd.readline()
828			if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
829				line = line.decode('utf-8', 'replace')
830			while (len(line)>0):
831				pwstruct = line.split(':')
832				if (len(pwstruct) >=3):
833					if ((pwstruct[0] in users) or (pwstruct[2] in users)):
834						fd2.write(line)
835						if (be_verbose):
836							print('writing user '+pwstruct[0]+' to '+chroot+'/etc/passwd')
837						if (not pwstruct[3] in groups):
838							groups += [pwstruct[3]]
839				line = fd.readline()
840				if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
841					line = line.decode('utf-8', 'replace')
842			fd.close()
843		fd2.close()
844	# do the same sequence for the group files
845	if (not os.path.isfile(chroot+'/etc/group')):
846		fd2 = open(chroot+'/etc/group','w')
847	else:
848		fd2 = open(chroot+'/etc/group','r+')
849		line = fd2.readline()
850		if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
851			line = line.decode('utf-8', 'replace')
852		while (len(line)>0):
853			groupstruct = line.split(':')
854			if (len(groupstruct) >=2):
855				if ((groupstruct[0] in groups) or (groupstruct[2] in groups)):
856					if (be_verbose):
857						print('group '+groupstruct[0]+' exists in '+chroot+'/etc/group')
858					try:
859						groups.remove(groupstruct[0])
860					except ValueError:
861						pass
862					try:
863						groups.remove(groupstruct[2])
864					except ValueError:
865						pass
866			line = fd2.readline()
867			if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
868				line = line.decode('utf-8', 'replace')
869		fd2.seek(0,2)
870	if (len(groups) > 0):
871		fd = open('/etc/group','r')
872		line = fd.readline()
873		if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
874			line = line.decode('utf-8', 'replace')
875		while (len(line)>0):
876			groupstruct = line.split(':')
877			if (len(groupstruct) >=2):
878				if ((groupstruct[0] in groups) or (groupstruct[2] in groups)):
879					fd2.write(line)
880					if (be_verbose):
881						print('writing group '+groupstruct[0]+' to '+chroot+'/etc/group')
882			line = fd.readline()
883			if sys.version_info > (3, 0) and isinstance(line, bytes):                                               #Python 3
884				line = line.decode('utf-8', 'replace')
885		fd.close()
886	fd2.close()
887
888def find_file_in_path(filename):
889	search_path = os.getenv('PATH')
890	paths = search_path.split(':')
891	for path in paths:
892		joined = os.path.join(path, filename)
893		if os.path.exists(joined):
894			return os.path.abspath(joined)
895	return None
896
897def find_files_in_path(paths):
898	paths2 = []
899	for tmp in paths:
900		if (tmp[0] == '/'):
901			paths2.append(tmp)
902		else:
903			tmp2 = find_file_in_path(tmp)
904			if (tmp2):
905				paths2.append(tmp2)
906	return paths2
907