/src/dovecot/src/lib-dict-extra/dict-fs.c
Line | Count | Source |
1 | | /* Copyright (c) Dovecot authors, see top-level COPYING file */ |
2 | | |
3 | | #include "lib.h" |
4 | | #include "array.h" |
5 | | #include "fs-api.h" |
6 | | #include "istream.h" |
7 | | #include "str.h" |
8 | | #include "dict-transaction-memory.h" |
9 | | #include "dict-private.h" |
10 | | |
11 | | struct fs_dict { |
12 | | struct dict dict; |
13 | | struct fs *fs; |
14 | | }; |
15 | | |
16 | | struct fs_dict_iterate_context { |
17 | | struct dict_iterate_context ctx; |
18 | | char *path; |
19 | | enum dict_iterate_flags flags; |
20 | | unsigned int key_count; |
21 | | pool_t value_pool; |
22 | | struct fs_iter *fs_iter; |
23 | | const char *const *values; |
24 | | char *error; |
25 | | }; |
26 | | |
27 | | static int |
28 | | fs_dict_init(const struct dict *dict_driver, struct event *event, |
29 | | struct dict **dict_r, const char **error_r) |
30 | 0 | { |
31 | 0 | struct fs_parameters fs_param; |
32 | 0 | struct fs *fs; |
33 | 0 | struct fs_dict *dict; |
34 | |
|
35 | 0 | i_zero(&fs_param); |
36 | 0 | if (fs_init_auto(event, &fs_param, &fs, error_r) <= 0) |
37 | 0 | return -1; |
38 | | |
39 | 0 | dict = i_new(struct fs_dict, 1); |
40 | 0 | dict->dict = *dict_driver; |
41 | 0 | dict->fs = fs; |
42 | |
|
43 | 0 | *dict_r = &dict->dict; |
44 | 0 | return 0; |
45 | 0 | } |
46 | | |
47 | | static void fs_dict_deinit(struct dict *_dict) |
48 | 0 | { |
49 | 0 | struct fs_dict *dict = (struct fs_dict *)_dict; |
50 | |
|
51 | 0 | fs_deinit(&dict->fs); |
52 | 0 | i_free(dict); |
53 | 0 | } |
54 | | |
55 | | /* Remove unsafe paths */ |
56 | | static const char *fs_dict_escape_key(const char *key) |
57 | 0 | { |
58 | 0 | const char *ptr; |
59 | 0 | string_t *new_key = NULL; |
60 | | /* A key always starts with either "priv/" or "shared/". Usernames can |
61 | | start with "./" or "../". Thus make sure the given key has no such |
62 | | prefix. */ |
63 | 0 | if (str_begins_with(key, "./") || str_begins_with(key, "../")) { |
64 | 0 | new_key = t_str_new(strlen(key)); |
65 | 0 | str_append(new_key, ".."); |
66 | 0 | } |
67 | | /* we take the slow path always if we see potential |
68 | | need for escaping */ |
69 | 0 | while ((ptr = strstr(key, "/.")) != NULL) { |
70 | | /* move to the first dot */ |
71 | 0 | const char *ptr2 = ptr + 1; |
72 | | /* find position of non-dot */ |
73 | 0 | while (*ptr2 == '.') ptr2++; |
74 | 0 | if (new_key == NULL) |
75 | 0 | new_key = t_str_new(strlen(key)); |
76 | 0 | str_append_data(new_key, key, ptr - key); |
77 | | /* if ptr2 is / or end of string, escape */ |
78 | 0 | if (*ptr2 == '/' || *ptr2 == '\0') |
79 | 0 | str_append(new_key, "/..."); |
80 | 0 | else |
81 | 0 | str_append(new_key, "/."); |
82 | 0 | key = ptr + 2; |
83 | 0 | } |
84 | 0 | if (new_key == NULL) |
85 | 0 | return key; |
86 | 0 | str_append(new_key, key); |
87 | 0 | return str_c(new_key); |
88 | 0 | } |
89 | | |
90 | | static const char *fs_dict_get_full_key(const char *username, const char *key) |
91 | 0 | { |
92 | 0 | key = fs_dict_escape_key(key); |
93 | 0 | if (str_begins(key, DICT_PATH_SHARED, &key)) |
94 | 0 | return key; |
95 | 0 | else if (str_begins(key, DICT_PATH_PRIVATE, &key)) |
96 | 0 | return t_strdup_printf("%s/%s", fs_dict_escape_key(username), key); |
97 | 0 | else |
98 | 0 | i_unreached(); |
99 | 0 | } |
100 | | |
101 | | static int fs_dict_lookup(struct dict *_dict, const struct dict_op_settings *set, |
102 | | pool_t pool, const char *key, |
103 | | const char *const **values_r, const char **error_r) |
104 | 0 | { |
105 | 0 | struct fs_dict *dict = (struct fs_dict *)_dict; |
106 | 0 | struct fs_file *file; |
107 | 0 | struct istream *input; |
108 | 0 | const unsigned char *data; |
109 | 0 | size_t size; |
110 | 0 | const char *path; |
111 | 0 | string_t *str; |
112 | 0 | int ret; |
113 | |
|
114 | 0 | path = fs_dict_get_full_key(set->username, key); |
115 | 0 | file = fs_file_init(dict->fs, path, FS_OPEN_MODE_READONLY); |
116 | 0 | input = fs_read_stream(file, IO_BLOCK_SIZE); |
117 | 0 | (void)i_stream_read(input); |
118 | |
|
119 | 0 | str = str_new(pool, i_stream_get_data_size(input)+1); |
120 | 0 | while ((ret = i_stream_read_more(input, &data, &size)) > 0) { |
121 | 0 | str_append_data(str, data, size); |
122 | 0 | i_stream_skip(input, size); |
123 | 0 | } |
124 | 0 | i_assert(ret == -1); |
125 | | |
126 | 0 | if (input->stream_errno == 0) { |
127 | 0 | const char **values = p_new(pool, const char *, 2); |
128 | 0 | values[0] = str_c(str); |
129 | 0 | *values_r = values; |
130 | 0 | ret = 1; |
131 | 0 | } else { |
132 | 0 | if (input->stream_errno == ENOENT) |
133 | 0 | ret = 0; |
134 | 0 | else { |
135 | 0 | *error_r = t_strdup_printf("read(%s) failed: %s", |
136 | 0 | path, i_stream_get_error(input)); |
137 | 0 | } |
138 | 0 | } |
139 | |
|
140 | 0 | i_stream_unref(&input); |
141 | 0 | fs_file_deinit(&file); |
142 | 0 | return ret; |
143 | 0 | } |
144 | | |
145 | | static struct dict_iterate_context * |
146 | | fs_dict_iterate_init(struct dict *_dict, const struct dict_op_settings *set, |
147 | | const char *path, enum dict_iterate_flags flags) |
148 | 0 | { |
149 | 0 | struct fs_dict *dict = (struct fs_dict *)_dict; |
150 | 0 | struct fs_dict_iterate_context *iter; |
151 | |
|
152 | 0 | iter = i_new(struct fs_dict_iterate_context, 1); |
153 | 0 | iter->ctx.dict = _dict; |
154 | 0 | iter->path = i_strdup(path); |
155 | 0 | iter->flags = flags; |
156 | 0 | iter->value_pool = pool_alloconly_create("iterate value pool", 128); |
157 | 0 | iter->fs_iter = fs_iter_init(dict->fs, |
158 | 0 | fs_dict_get_full_key(set->username, path), 0); |
159 | |
|
160 | 0 | const char *unsupported = NULL; |
161 | 0 | if ((flags & DICT_ITERATE_FLAG_RECURSE) != 0) |
162 | 0 | unsupported = "DICT_ITERATE_FLAG_RECURSE"; |
163 | 0 | else if ((flags & DICT_ITERATE_FLAG_SORT_BY_KEY) != 0) |
164 | 0 | unsupported = "DICT_ITERATE_FLAG_SORT_BY_KEY"; |
165 | 0 | else if ((flags & DICT_ITERATE_FLAG_SORT_BY_VALUE) != 0) |
166 | 0 | unsupported = "DICT_ITERATE_FLAG_SORT_BY_VALUE"; |
167 | 0 | if (unsupported != NULL) { |
168 | 0 | iter->error = i_strdup_printf( |
169 | 0 | "dict-fs doesn't currently support %s", unsupported); |
170 | 0 | } |
171 | 0 | return &iter->ctx; |
172 | 0 | } |
173 | | |
174 | | static bool fs_dict_iterate(struct dict_iterate_context *ctx, |
175 | | const char **key_r, const char *const **values_r) |
176 | 0 | { |
177 | 0 | struct fs_dict_iterate_context *iter = |
178 | 0 | (struct fs_dict_iterate_context *)ctx; |
179 | 0 | const char *path, *error; |
180 | 0 | int ret; |
181 | |
|
182 | 0 | if (iter->error != NULL || iter->fs_iter == NULL) |
183 | 0 | return FALSE; |
184 | | |
185 | 0 | *key_r = fs_iter_next(iter->fs_iter); |
186 | 0 | if (*key_r == NULL) { |
187 | 0 | if (iter->key_count > 0 || |
188 | 0 | (iter->flags & DICT_ITERATE_FLAG_EXACT_KEY) == 0) |
189 | 0 | return FALSE; |
190 | | |
191 | 0 | if (fs_iter_deinit(&iter->fs_iter, &error) == 0) |
192 | 0 | return FALSE; |
193 | 0 | if (errno != ENOTDIR) { |
194 | 0 | iter->error = i_strdup(error); |
195 | 0 | return FALSE; |
196 | 0 | } |
197 | | /* the path itself is the only key we are going to return */ |
198 | 0 | *key_r = ""; |
199 | 0 | } |
200 | 0 | iter->key_count++; |
201 | |
|
202 | 0 | p_clear(iter->value_pool); |
203 | 0 | path = p_strconcat(iter->value_pool, iter->path, *key_r, NULL); |
204 | 0 | if ((iter->flags & DICT_ITERATE_FLAG_NO_VALUE) != 0) { |
205 | 0 | *key_r = path; |
206 | 0 | return TRUE; |
207 | 0 | } |
208 | 0 | struct dict_op_settings set = { |
209 | 0 | .username = ctx->set.username, |
210 | 0 | }; |
211 | 0 | ret = fs_dict_lookup(ctx->dict, &set, iter->value_pool, path, |
212 | 0 | &iter->values, &error); |
213 | 0 | if (ret < 0) { |
214 | | /* I/O error */ |
215 | 0 | iter->error = i_strdup(error); |
216 | 0 | return FALSE; |
217 | 0 | } else if (ret == 0) { |
218 | | /* file was just deleted, just skip to next one */ |
219 | 0 | return fs_dict_iterate(ctx, key_r, values_r); |
220 | 0 | } |
221 | 0 | *key_r = path; |
222 | 0 | *values_r = iter->values; |
223 | 0 | return TRUE; |
224 | 0 | } |
225 | | |
226 | | static int fs_dict_iterate_deinit(struct dict_iterate_context *ctx, |
227 | | const char **error_r) |
228 | 0 | { |
229 | 0 | struct fs_dict_iterate_context *iter = |
230 | 0 | (struct fs_dict_iterate_context *)ctx; |
231 | 0 | const char *error; |
232 | 0 | int ret; |
233 | |
|
234 | 0 | if (fs_iter_deinit(&iter->fs_iter, &error) < 0 && iter->error == NULL) |
235 | 0 | iter->error = i_strdup(error); |
236 | |
|
237 | 0 | ret = iter->error != NULL ? -1 : 0; |
238 | 0 | *error_r = t_strdup(iter->error); |
239 | |
|
240 | 0 | pool_unref(&iter->value_pool); |
241 | 0 | i_free(iter->path); |
242 | 0 | i_free(iter->error); |
243 | 0 | i_free(iter); |
244 | 0 | return ret; |
245 | 0 | } |
246 | | |
247 | | static struct dict_transaction_context * |
248 | | fs_dict_transaction_init(struct dict *_dict) |
249 | 0 | { |
250 | 0 | struct dict_transaction_memory_context *ctx; |
251 | 0 | pool_t pool; |
252 | |
|
253 | 0 | pool = pool_alloconly_create("file dict transaction", 2048); |
254 | 0 | ctx = p_new(pool, struct dict_transaction_memory_context, 1); |
255 | 0 | dict_transaction_memory_init(ctx, _dict, pool); |
256 | 0 | return &ctx->ctx; |
257 | 0 | } |
258 | | |
259 | | static int fs_dict_write_changes(struct dict_transaction_memory_context *ctx, |
260 | | const char **error_r) |
261 | 0 | { |
262 | 0 | struct fs_dict *dict = (struct fs_dict *)ctx->ctx.dict; |
263 | 0 | struct fs_file *file; |
264 | 0 | const struct dict_transaction_memory_change *change; |
265 | 0 | const char *key; |
266 | 0 | int ret = 0; |
267 | |
|
268 | 0 | array_foreach(&ctx->changes, change) { |
269 | 0 | key = fs_dict_get_full_key(ctx->ctx.set.username, change->key); |
270 | 0 | switch (change->type) { |
271 | 0 | case DICT_CHANGE_TYPE_SET: |
272 | 0 | file = fs_file_init(dict->fs, key, |
273 | 0 | FS_OPEN_MODE_REPLACE); |
274 | 0 | if (fs_write(file, change->value.str, strlen(change->value.str)) < 0) { |
275 | 0 | *error_r = t_strdup_printf( |
276 | 0 | "fs_write(%s) failed: %s", key, |
277 | 0 | fs_file_last_error(file)); |
278 | 0 | ret = -1; |
279 | 0 | } |
280 | 0 | fs_file_deinit(&file); |
281 | 0 | break; |
282 | 0 | case DICT_CHANGE_TYPE_UNSET: |
283 | 0 | file = fs_file_init(dict->fs, key, FS_OPEN_MODE_READONLY); |
284 | 0 | if (fs_delete(file) < 0) { |
285 | 0 | *error_r = t_strdup_printf( |
286 | 0 | "fs_delete(%s) failed: %s", key, |
287 | 0 | fs_file_last_error(file)); |
288 | 0 | ret = -1; |
289 | 0 | } |
290 | 0 | fs_file_deinit(&file); |
291 | 0 | break; |
292 | 0 | case DICT_CHANGE_TYPE_INC: |
293 | 0 | i_unreached(); |
294 | 0 | } |
295 | 0 | if (ret < 0) |
296 | 0 | return -1; |
297 | 0 | } |
298 | 0 | return 0; |
299 | 0 | } |
300 | | |
301 | | static void |
302 | | fs_dict_transaction_commit(struct dict_transaction_context *_ctx, |
303 | | bool async ATTR_UNUSED, |
304 | | dict_transaction_commit_callback_t *callback, |
305 | | void *context) |
306 | 0 | { |
307 | 0 | struct dict_transaction_memory_context *ctx = |
308 | 0 | (struct dict_transaction_memory_context *)_ctx; |
309 | 0 | struct dict_commit_result result = { .ret = 1 }; |
310 | | |
311 | |
|
312 | 0 | if (fs_dict_write_changes(ctx, &result.error) < 0) |
313 | 0 | result.ret = -1; |
314 | 0 | pool_unref(&ctx->pool); |
315 | |
|
316 | 0 | callback(&result, context); |
317 | 0 | } |
318 | | |
319 | | struct dict dict_driver_fs = { |
320 | | .name = "fs", |
321 | | .v = { |
322 | | .init = fs_dict_init, |
323 | | .deinit = fs_dict_deinit, |
324 | | .lookup = fs_dict_lookup, |
325 | | .iterate_init = fs_dict_iterate_init, |
326 | | .iterate = fs_dict_iterate, |
327 | | .iterate_deinit = fs_dict_iterate_deinit, |
328 | | .transaction_init = fs_dict_transaction_init, |
329 | | .transaction_commit = fs_dict_transaction_commit, |
330 | | .transaction_rollback = dict_transaction_memory_rollback, |
331 | | .set = dict_transaction_memory_set, |
332 | | .unset = dict_transaction_memory_unset, |
333 | | } |
334 | | }; |