... |
... |
@@ -36,8 +36,23 @@ DEFAULT_KEY_MODE = 0o400
|
36 |
36 |
DEFAULT_SENSITIVE_PATTERN = '/privkey.pem$'
|
37 |
37 |
TIME_PROPS = ('st_atime', 'st_ctime', 'st_mtime')
|
38 |
38 |
|
39 |
|
-class CombinedFS(Operations):
|
40 |
|
- def __init__(self, conf):
|
|
39 |
+def read_mode_setting(obj, key, default):
|
|
40 |
+ try:
|
|
41 |
+ return int(obj[key], 8)
|
|
42 |
+ except (KeyError, ValueError):
|
|
43 |
+ return default
|
|
44 |
+
|
|
45 |
+class CombinedFSConfiguration(object):
|
|
46 |
+ def __init__(self, conf_path):
|
|
47 |
+ self.read_conf(conf_path)
|
|
48 |
+
|
|
49 |
+ def read_conf(self, conf_path):
|
|
50 |
+ with open(conf_path) as conf_file:
|
|
51 |
+ conf = yaml.load(conf_file.read())
|
|
52 |
+ self.apply_conf(conf)
|
|
53 |
+ self.path = conf_path
|
|
54 |
+
|
|
55 |
+ def apply_conf(self, conf):
|
41 |
56 |
self.root = conf.get('letsencrypt_live', DEFAULT_ROOT)
|
42 |
57 |
self.filter = conf.get('cert_filter', DEFAULT_CERT_FILTER)
|
43 |
58 |
self.whitelist = conf.get('cert_whitelist', DEFAULT_WHITELIST)
|
... |
... |
@@ -46,27 +61,16 @@ class CombinedFS(Operations):
|
46 |
61 |
self.files = conf.get('files', {})
|
47 |
62 |
self.uid = int(conf.get('uid', DEFAULT_UID))
|
48 |
63 |
self.gid = int(conf.get('gid', DEFAULT_GID))
|
49 |
|
- self.dir_mode = self.read_mode_setting(conf, 'dir_mode', DEFAULT_DIR_MODE)
|
50 |
|
- self.reg_mode = self.read_mode_setting(conf, 'reg_mode', DEFAULT_REG_MODE)
|
51 |
|
- self.key_mode = self.read_mode_setting(conf, 'key_mode', DEFAULT_KEY_MODE)
|
|
64 |
+ self.dir_mode = read_mode_setting(conf, 'dir_mode', DEFAULT_DIR_MODE)
|
|
65 |
+ self.reg_mode = read_mode_setting(conf, 'reg_mode', DEFAULT_REG_MODE)
|
|
66 |
+ self.key_mode = read_mode_setting(conf, 'key_mode', DEFAULT_KEY_MODE)
|
52 |
67 |
self.sensitive_pattern = conf.get('sensitive_pattern', DEFAULT_SENSITIVE_PATTERN)
|
53 |
|
- # File descriptor management:
|
54 |
|
- self.filedesc_lock = threading.Lock()
|
55 |
|
- self.filedesc_index = 0
|
56 |
|
- self.filedesc = {}
|
57 |
68 |
# Compile regexes:
|
58 |
69 |
self.sensitive_pattern_re = re.compile(self.sensitive_pattern)
|
59 |
70 |
if self.filter:
|
60 |
71 |
self.pattern_re = re.compile(self.pattern)
|
61 |
72 |
|
62 |
73 |
# Helpers:
|
63 |
|
-
|
64 |
|
- def read_mode_setting(self, obj, key, default):
|
65 |
|
- try:
|
66 |
|
- return int(obj[key], 8)
|
67 |
|
- except (KeyError, ValueError):
|
68 |
|
- return default
|
69 |
|
-
|
70 |
74 |
def filter_cert(self, cert):
|
71 |
75 |
if not self.filter:
|
72 |
76 |
return True
|
... |
... |
@@ -117,11 +121,6 @@ class CombinedFS(Operations):
|
117 |
121 |
raise FuseOSError(errno.ENOENT)
|
118 |
122 |
return cert, filename, file_spec
|
119 |
123 |
|
120 |
|
- def attributes(self, full_path):
|
121 |
|
- st = os.lstat(full_path)
|
122 |
|
- return dict((key, getattr(st, key)) for key in ('st_atime', 'st_ctime',
|
123 |
|
- 'st_gid', 'st_mode', 'st_mtime', 'st_nlink', 'st_size', 'st_uid'))
|
124 |
|
-
|
125 |
124 |
def expand_path(self, cert, path):
|
126 |
125 |
expanded_path = path.replace('${cert}', cert)
|
127 |
126 |
if not expanded_path.startswith('/'):
|
... |
... |
@@ -141,6 +140,24 @@ class CombinedFS(Operations):
|
141 |
140 |
def is_sensitive_file(self, filepath):
|
142 |
141 |
return self.sensitive_pattern_re.search(filepath)
|
143 |
142 |
|
|
143 |
+class CombinedFS(Operations):
|
|
144 |
+ def __init__(self, conf_path):
|
|
145 |
+ # Configuration:
|
|
146 |
+ self.configuration = CombinedFSConfiguration(conf_path)
|
|
147 |
+ # File descriptor management:
|
|
148 |
+ self.filedesc_lock = threading.Lock()
|
|
149 |
+ self.filedesc_index = 0
|
|
150 |
+ self.filedesc = {}
|
|
151 |
+
|
|
152 |
+ # Helpers:
|
|
153 |
+ def attributes(self, full_path):
|
|
154 |
+ st = os.lstat(full_path)
|
|
155 |
+ return dict((key, getattr(st, key)) for key in ('st_atime', 'st_ctime',
|
|
156 |
+ 'st_gid', 'st_mode', 'st_mtime', 'st_nlink', 'st_size', 'st_uid'))
|
|
157 |
+
|
|
158 |
+ def get_conf(self):
|
|
159 |
+ return self.configuration
|
|
160 |
+
|
144 |
161 |
def iterate_paths(self, func, paths):
|
145 |
162 |
for filepath in paths:
|
146 |
163 |
try:
|
... |
... |
@@ -177,28 +194,29 @@ class CombinedFS(Operations):
|
177 |
194 |
raise FuseOSError(errno.ENOTSUP)
|
178 |
195 |
|
179 |
196 |
def getattr(self, path, fh=None):
|
180 |
|
- cert, filename, file_spec = self.analyse_path(path)
|
|
197 |
+ conf = self.get_conf()
|
|
198 |
+ cert, filename, file_spec = conf.analyse_path(path)
|
181 |
199 |
if filename is None: # Directory
|
182 |
|
- full_path = os.path.join(self.root, path.lstrip('/'))
|
|
200 |
+ full_path = os.path.join(conf.root, path.lstrip('/'))
|
183 |
201 |
dir_attrs = self.attributes(full_path)
|
184 |
|
- dir_attrs['st_uid'] = self.uid
|
185 |
|
- dir_attrs['st_gid'] = self.gid
|
186 |
|
- dir_attrs['st_mode'] = stat.S_IFDIR | self.dir_mode
|
|
202 |
+ dir_attrs['st_uid'] = conf.uid
|
|
203 |
+ dir_attrs['st_gid'] = conf.gid
|
|
204 |
+ dir_attrs['st_mode'] = stat.S_IFDIR | conf.dir_mode
|
187 |
205 |
return dir_attrs
|
188 |
206 |
attrs = {
|
189 |
207 |
'st_nlink': 1,
|
190 |
|
- 'st_uid': file_spec.get('uid', self.uid),
|
191 |
|
- 'st_gid': file_spec.get('gid', self.gid),
|
|
208 |
+ 'st_uid': file_spec.get('uid', conf.uid),
|
|
209 |
+ 'st_gid': file_spec.get('gid', conf.gid),
|
192 |
210 |
'st_size': 0,
|
193 |
211 |
}
|
194 |
|
- def_mode = self.reg_mode
|
195 |
|
- paths = self.get_paths(cert, file_spec)
|
|
212 |
+ def_mode = conf.reg_mode
|
|
213 |
+ paths = conf.get_paths(cert, file_spec)
|
196 |
214 |
if not paths:
|
197 |
215 |
# Virtual empty file:
|
198 |
|
- root_stats = os.stat(self.root)
|
|
216 |
+ root_stats = os.stat(conf.root)
|
199 |
217 |
for prop in TIME_PROPS:
|
200 |
218 |
attrs[prop] = getattr(root_stats, prop)
|
201 |
|
- attrs['st_mode'] = stat.S_IFREG | self.read_mode_setting(file_spec, 'mode', def_mode)
|
|
219 |
+ attrs['st_mode'] = stat.S_IFREG | read_mode_setting(file_spec, 'mode', def_mode)
|
202 |
220 |
return attrs
|
203 |
221 |
stats = {}
|
204 |
222 |
def stat_file(path):
|
... |
... |
@@ -213,13 +231,14 @@ class CombinedFS(Operations):
|
213 |
231 |
# Add up sizes:
|
214 |
232 |
attrs['st_size'] += stat_obj.st_size
|
215 |
233 |
# Lower permissions if necessary:
|
216 |
|
- if self.is_sensitive_file(filepath):
|
217 |
|
- def_mode = self.key_mode
|
218 |
|
- attrs['st_mode'] = stat.S_IFREG | self.read_mode_setting(file_spec, 'mode', def_mode)
|
|
234 |
+ if conf.is_sensitive_file(filepath):
|
|
235 |
+ def_mode = conf.key_mode
|
|
236 |
+ attrs['st_mode'] = stat.S_IFREG | read_mode_setting(file_spec, 'mode', def_mode)
|
219 |
237 |
return attrs
|
220 |
238 |
|
221 |
239 |
def readdir(self, path, fh):
|
222 |
|
- cert, filename, _ = self.analyse_path(path)
|
|
240 |
+ conf = self.get_conf()
|
|
241 |
+ cert, filename, _ = conf.analyse_path(path)
|
223 |
242 |
# Deal only with directories:
|
224 |
243 |
if filename:
|
225 |
244 |
raise FuseOSError(errno.ENOTDIR)
|
... |
... |
@@ -231,24 +250,26 @@ class CombinedFS(Operations):
|
231 |
250 |
yield '..', dir_attrs, 0
|
232 |
251 |
if not cert:
|
233 |
252 |
# Top-level directory
|
234 |
|
- flat_mode = self.separator != '/'
|
235 |
|
- for cert in (d for d in os.listdir(self.root) if self.filter_cert(d)):
|
|
253 |
+ flat_mode = conf.separator != '/'
|
|
254 |
+ for cert in (d for d in os.listdir(conf.root) if conf.filter_cert(d)):
|
236 |
255 |
if flat_mode:
|
237 |
|
- for filename in self.files:
|
238 |
|
- yield cert + self.separator + filename, reg_attrs, 0
|
|
256 |
+ for filename in conf.files:
|
|
257 |
+ yield cert + conf.separator + filename, reg_attrs, 0
|
239 |
258 |
else:
|
240 |
259 |
yield cert, dir_attrs, 0
|
241 |
260 |
else:
|
242 |
261 |
# Second-level directory
|
243 |
|
- for filename in self.files:
|
|
262 |
+ for filename in conf.files:
|
244 |
263 |
yield filename, reg_attrs, 0
|
245 |
264 |
|
246 |
265 |
def open(self, path, flags):
|
247 |
|
- cert, filename, file_spec = self.analyse_path(path)
|
|
266 |
+ conf = self.get_conf()
|
|
267 |
+ cert, filename, file_spec = conf.analyse_path(path)
|
248 |
268 |
if not cert or not filename:
|
249 |
269 |
raise FuseOSError(errno.ENOENT)
|
250 |
270 |
# Being a read-only filesystem spares us the need to check most flags.
|
251 |
271 |
new_fd = {
|
|
272 |
+ 'conf': conf,
|
252 |
273 |
'cert': cert,
|
253 |
274 |
'filename': filename,
|
254 |
275 |
'file_spec': file_spec,
|
... |
... |
@@ -261,11 +282,13 @@ class CombinedFS(Operations):
|
261 |
282 |
|
262 |
283 |
def read(self, path, length, offset, fh):
|
263 |
284 |
filedesc = self.filedesc.get(fh)
|
|
285 |
+ # Use the same configuration as open() when it created the file descriptor:
|
|
286 |
+ conf = filedesc['conf']
|
264 |
287 |
if filedesc is None:
|
265 |
288 |
raise FuseOSError(errno.EBADF)
|
266 |
289 |
data = filedesc.get('data')
|
267 |
290 |
if data is None:
|
268 |
|
- paths = self.get_paths(filedesc['cert'], filedesc['file_spec'])
|
|
291 |
+ paths = conf.get_paths(filedesc['cert'], filedesc['file_spec'])
|
269 |
292 |
data = {'data': bytes() }
|
270 |
293 |
def concatenate(path):
|
271 |
294 |
data['data'] += open(path, 'rb').read()
|
... |
... |
@@ -282,7 +305,7 @@ class CombinedFS(Operations):
|
282 |
305 |
# pure Python.
|
283 |
306 |
|
284 |
307 |
def statfs(self, path):
|
285 |
|
- stv = os.statvfs(self.root)
|
|
308 |
+ stv = os.statvfs(self.get_conf().root)
|
286 |
309 |
return dict((key, getattr(stv, key)) for key in ('f_bavail', 'f_bfree',
|
287 |
310 |
'f_blocks', 'f_bsize', 'f_favail', 'f_ffree', 'f_files', 'f_flag',
|
288 |
311 |
'f_frsize', 'f_namemax'))
|
... |
... |
@@ -293,10 +316,7 @@ class CombinedFS(Operations):
|
293 |
316 |
raise FuseOSError(errno.EINVAL)
|
294 |
317 |
|
295 |
318 |
def main(conf_path, mountpoint, foreground):
|
296 |
|
- conf = {}
|
297 |
|
- with open(conf_path) as conf_file:
|
298 |
|
- conf = yaml.load(conf_file.read())
|
299 |
|
- FUSE(CombinedFS(conf), mountpoint, foreground=foreground, ro=True, default_permissions=True, allow_other=True)
|
|
319 |
+ FUSE(CombinedFS(conf_path), mountpoint, foreground=foreground, ro=True, default_permissions=True, allow_other=True)
|
300 |
320 |
|
301 |
321 |
if __name__ == '__main__':
|
302 |
322 |
parser = argparse.ArgumentParser(description='Expose a transformed, version of Let\'s Encrypt / Certbot\'s "live" directory')
|