1 /* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
2
3 This program is free software: you can redistribute it and/or modify
4 it under the terms of the GNU General Public License as published by
5 the Free Software Foundation, either version 3 of the License, or
6 (at your option) any later version.
7
8 This program is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License
14 along with this program. If not, see <https://www.gnu.org/licenses/>.
15 */
16
17 #include <stdio.h>
18 #include <histedit.h>
19
20 #include "knot/common/log.h"
21 #include "utils/common/lookup.h"
22 #include "utils/knotc/interactive.h"
23 #include "utils/knotc/commands.h"
24 #include "contrib/string.h"
25
26 #define PROGRAM_NAME "knotc"
27 #define HISTORY_FILE ".knotc_history"
28
29 extern params_t params;
30
cmds_lookup(EditLine * el,const char * str,size_t str_len)31 static void cmds_lookup(EditLine *el, const char *str, size_t str_len)
32 {
33 lookup_t lookup;
34 int ret = lookup_init(&lookup);
35 if (ret != KNOT_EOK) {
36 return;
37 }
38
39 // Fill the lookup with command names.
40 for (const cmd_desc_t *desc = cmd_table; desc->name != NULL; desc++) {
41 ret = lookup_insert(&lookup, desc->name, NULL);
42 if (ret != KNOT_EOK) {
43 goto cmds_lookup_finish;
44 }
45 }
46
47 lookup_complete(&lookup, str, str_len, el, true);
48
49 cmds_lookup_finish:
50 lookup_deinit(&lookup);
51 }
52
local_zones_lookup(EditLine * el,const char * str,size_t str_len)53 static void local_zones_lookup(EditLine *el, const char *str, size_t str_len)
54 {
55 lookup_t lookup;
56 int ret = lookup_init(&lookup);
57 if (ret != KNOT_EOK) {
58 return;
59 }
60
61 knot_dname_txt_storage_t buff;
62
63 // Fill the lookup with local zone names.
64 for (conf_iter_t iter = conf_iter(conf(), C_ZONE);
65 iter.code == KNOT_EOK; conf_iter_next(conf(), &iter)) {
66 conf_val_t val = conf_iter_id(conf(), &iter);
67 char *name = knot_dname_to_str(buff, conf_dname(&val), sizeof(buff));
68
69 ret = lookup_insert(&lookup, name, NULL);
70 if (ret != KNOT_EOK) {
71 conf_iter_finish(conf(), &iter);
72 goto local_zones_lookup_finish;
73 }
74 }
75
76 lookup_complete(&lookup, str, str_len, el, true);
77
78 local_zones_lookup_finish:
79 lookup_deinit(&lookup);
80 }
81
get_id_name(const char * section)82 static char *get_id_name(const char *section)
83 {
84 const cmd_desc_t *desc = cmd_table;
85 while (desc->name != NULL && desc->cmd != CTL_CONF_LIST) {
86 desc++;
87 }
88 assert(desc->name != NULL);
89
90 knot_ctl_data_t query = {
91 [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(desc->cmd),
92 [KNOT_CTL_IDX_SECTION] = section
93 };
94
95 knot_ctl_t *ctl = NULL;
96 knot_ctl_type_t type;
97 knot_ctl_data_t reply;
98
99 // Try to get the first group item (possible id).
100 if (set_ctl(&ctl, params.socket, DEFAULT_CTL_TIMEOUT_MS, desc) != KNOT_EOK ||
101 knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, &query) != KNOT_EOK ||
102 knot_ctl_send(ctl, KNOT_CTL_TYPE_BLOCK, NULL) != KNOT_EOK ||
103 knot_ctl_receive(ctl, &type, &reply) != KNOT_EOK ||
104 type != KNOT_CTL_TYPE_DATA || reply[KNOT_CTL_IDX_ERROR] != NULL) {
105 unset_ctl(ctl);
106 return NULL;
107 }
108
109 char *id_name = strdup(reply[KNOT_CTL_IDX_ITEM]);
110
111 unset_ctl(ctl);
112
113 return id_name;
114 }
115
id_lookup(EditLine * el,const char * str,size_t str_len,const cmd_desc_t * cmd_desc,const char * section,bool add_space)116 static void id_lookup(EditLine *el, const char *str, size_t str_len,
117 const cmd_desc_t *cmd_desc, const char *section, bool add_space)
118 {
119 // Decide which confdb transaction to ask.
120 unsigned ctl_code = (cmd_desc->flags & CMD_FREQ_TXN) ?
121 CTL_CONF_GET : CTL_CONF_READ;
122
123 const cmd_desc_t *desc = cmd_table;
124 while (desc->name != NULL && desc->cmd != ctl_code) {
125 desc++;
126 }
127 assert(desc->name != NULL);
128
129 char *id_name = get_id_name(section);
130 if (id_name == NULL) {
131 return;
132 }
133
134 knot_ctl_data_t query = {
135 [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(desc->cmd),
136 [KNOT_CTL_IDX_SECTION] = section,
137 [KNOT_CTL_IDX_ITEM] = id_name
138 };
139
140 lookup_t lookup;
141 knot_ctl_t *ctl = NULL;
142
143 if (set_ctl(&ctl, params.socket, DEFAULT_CTL_TIMEOUT_MS, desc) != KNOT_EOK ||
144 knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, &query) != KNOT_EOK ||
145 knot_ctl_send(ctl, KNOT_CTL_TYPE_BLOCK, NULL) != KNOT_EOK ||
146 lookup_init(&lookup) != KNOT_EOK) {
147 unset_ctl(ctl);
148 free(id_name);
149 return;
150 }
151
152 free(id_name);
153
154 while (true) {
155 knot_ctl_type_t type;
156 knot_ctl_data_t reply;
157
158 // Receive one section id.
159 if (knot_ctl_receive(ctl, &type, &reply) != KNOT_EOK) {
160 goto id_lookup_finish;
161 }
162
163 // Stop if finished transfer.
164 if (type != KNOT_CTL_TYPE_DATA) {
165 break;
166 }
167
168 // Insert the id into the lookup.
169 if (reply[KNOT_CTL_IDX_ERROR] != NULL ||
170 lookup_insert(&lookup, reply[KNOT_CTL_IDX_DATA], NULL) != KNOT_EOK) {
171 goto id_lookup_finish;
172 }
173 }
174
175 lookup_complete(&lookup, str, str_len, el, add_space);
176
177 id_lookup_finish:
178 lookup_deinit(&lookup);
179 unset_ctl(ctl);
180 }
181
list_lookup(EditLine * el,const char * section,const char * item)182 static void list_lookup(EditLine *el, const char *section, const char *item)
183 {
184 const cmd_desc_t *desc = cmd_table;
185 while (desc->name != NULL && desc->cmd != CTL_CONF_LIST) {
186 desc++;
187 }
188 assert(desc->name != NULL);
189
190 knot_ctl_data_t query = {
191 [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(desc->cmd),
192 [KNOT_CTL_IDX_SECTION] = section
193 };
194
195 lookup_t lookup;
196 knot_ctl_t *ctl = NULL;
197
198 if (set_ctl(&ctl, params.socket, DEFAULT_CTL_TIMEOUT_MS, desc) != KNOT_EOK ||
199 knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, &query) != KNOT_EOK ||
200 knot_ctl_send(ctl, KNOT_CTL_TYPE_BLOCK, NULL) != KNOT_EOK ||
201 lookup_init(&lookup) != KNOT_EOK) {
202 unset_ctl(ctl);
203 return;
204 }
205
206 while (true) {
207 knot_ctl_type_t type;
208 knot_ctl_data_t reply;
209
210 // Receive one section/item name.
211 if (knot_ctl_receive(ctl, &type, &reply) != KNOT_EOK) {
212 goto list_lookup_finish;
213 }
214
215 // Stop if finished transfer.
216 if (type != KNOT_CTL_TYPE_DATA) {
217 break;
218 }
219
220 const char *str = (section == NULL) ? reply[KNOT_CTL_IDX_SECTION] :
221 reply[KNOT_CTL_IDX_ITEM];
222
223 // Insert the name into the lookup.
224 if (reply[KNOT_CTL_IDX_ERROR] != NULL ||
225 lookup_insert(&lookup, str, NULL) != KNOT_EOK) {
226 goto list_lookup_finish;
227 }
228 }
229
230 lookup_complete(&lookup, item, strlen(item), el, section != NULL);
231
232 list_lookup_finish:
233 lookup_deinit(&lookup);
234 unset_ctl(ctl);
235 }
236
item_lookup(EditLine * el,const char * str,const cmd_desc_t * cmd_desc)237 static void item_lookup(EditLine *el, const char *str, const cmd_desc_t *cmd_desc)
238 {
239 // List all sections.
240 if (str == NULL) {
241 list_lookup(el, NULL, "");
242 return;
243 }
244
245 // Check for id specification.
246 char *id = (strchr(str, '['));
247 if (id != NULL) {
248 char *section = strndup(str, id - str);
249
250 // Check for completed id specification.
251 char *id_stop = (strchr(id, ']'));
252 if (id_stop != NULL) {
253 // Complete the item name.
254 if (*(id_stop + 1) == '.') {
255 list_lookup(el, section, id_stop + 2);
256 }
257 } else {
258 // Complete the section id.
259 id_lookup(el, id + 1, strlen(id + 1), cmd_desc, section, false);
260 }
261
262 free(section);
263 } else {
264 // Check for item specification.
265 char *dot = (strchr(str, '.'));
266 if (dot != NULL) {
267 // Complete the item name.
268 char *section = strndup(str, dot - str);
269 list_lookup(el, section, dot + 1);
270 free(section);
271 } else {
272 // Complete the section name.
273 list_lookup(el, NULL, str);
274 }
275 }
276 }
277
complete(EditLine * el,int ch)278 static unsigned char complete(EditLine *el, int ch)
279 {
280 int argc, token, pos;
281 const char **argv;
282
283 const LineInfo *li = el_line(el);
284 Tokenizer *tok = tok_init(NULL);
285
286 // Parse the line.
287 int ret = tok_line(tok, li, &argc, &argv, &token, &pos);
288 if (ret != 0) {
289 goto complete_exit;
290 }
291
292 // Show possible commands.
293 if (argc == 0) {
294 print_commands();
295 goto complete_exit;
296 }
297
298 // Complete the command name.
299 if (token == 0) {
300 cmds_lookup(el, argv[0], pos);
301 goto complete_exit;
302 }
303
304 // Find the command descriptor.
305 const cmd_desc_t *desc = cmd_table;
306 while (desc->name != NULL && strcmp(desc->name, argv[0]) != 0) {
307 desc++;
308 }
309 if (desc->name == NULL) {
310 goto complete_exit;
311 }
312
313 // Finish if a command with no or unsupported arguments.
314 if (desc->flags == CMD_FNONE || desc->flags == CMD_FREAD ||
315 desc->flags == CMD_FWRITE) {
316 goto complete_exit;
317 }
318
319 ret = set_config(desc, ¶ms);
320 if (ret != KNOT_EOK) {
321 goto complete_exit;
322 }
323
324 // Complete the zone name.
325 if (desc->flags & (CMD_FREQ_ZONE | CMD_FOPT_ZONE)) {
326 if (token > 1 && !(desc->flags & CMD_FOPT_ZONE)) {
327 goto complete_exit;
328 }
329
330 if (desc->flags & CMD_FREAD) {
331 local_zones_lookup(el, argv[token], pos);
332 } else {
333 id_lookup(el, argv[token], pos, desc, "zone", true);
334 }
335 goto complete_exit;
336 // Complete the section/id/item name.
337 } else if (desc->flags & (CMD_FOPT_ITEM | CMD_FREQ_ITEM)) {
338 if (token == 1) {
339 item_lookup(el, argv[1], desc);
340 }
341 goto complete_exit;
342 }
343
344 complete_exit:
345 conf_update(NULL, CONF_UPD_FNONE);
346 tok_reset(tok);
347 tok_end(tok);
348
349 return CC_REDISPLAY;
350 }
351
prompt(EditLine * el)352 static char *prompt(EditLine *el)
353 {
354 return PROGRAM_NAME"> ";
355 }
356
interactive_loop(params_t * process_params)357 int interactive_loop(params_t *process_params)
358 {
359 char *hist_file = NULL;
360 const char *home = getenv("HOME");
361 if (home != NULL) {
362 hist_file = sprintf_alloc("%s/%s", home, HISTORY_FILE);
363 }
364 if (hist_file == NULL) {
365 log_notice("failed to get home directory");
366 }
367
368 EditLine *el = el_init(PROGRAM_NAME, stdin, stdout, stderr);
369 if (el == NULL) {
370 log_error("interactive mode not available");
371 free(hist_file);
372 return KNOT_ERROR;
373 }
374
375 History *hist = history_init();
376 if (hist == NULL) {
377 log_error("interactive mode not available");
378 el_end(el);
379 free(hist_file);
380 return KNOT_ERROR;
381 }
382
383 HistEvent hev = { 0 };
384 history(hist, &hev, H_SETSIZE, 1000);
385 history(hist, &hev, H_SETUNIQUE, 1);
386 el_set(el, EL_HIST, history, hist);
387 history(hist, &hev, H_LOAD, hist_file);
388
389 el_set(el, EL_TERMINAL, NULL);
390 el_set(el, EL_EDITOR, "emacs");
391 el_set(el, EL_PROMPT, prompt);
392 el_set(el, EL_SIGNAL, 1);
393 el_source(el, NULL);
394
395 // Warning: these two el_sets()'s always leak -- in libedit2 library!
396 // For more details see this commit's message.
397 el_set(el, EL_ADDFN, PROGRAM_NAME"-complete",
398 "Perform "PROGRAM_NAME" completion.", complete);
399 el_set(el, EL_BIND, "^I", PROGRAM_NAME"-complete", NULL);
400
401 int count;
402 const char *line;
403 while ((line = el_gets(el, &count)) != NULL && count > 0) {
404 Tokenizer *tok = tok_init(NULL);
405
406 // Tokenize the current line.
407 int argc;
408 const char **argv;
409 const LineInfo *li = el_line(el);
410 int ret = tok_line(tok, li, &argc, &argv, NULL, NULL);
411 if (ret == 0 && argc != 0) {
412 history(hist, &hev, H_ENTER, line);
413 history(hist, &hev, H_SAVE, hist_file);
414
415 // Process the command.
416 ret = process_cmd(argc, argv, process_params);
417 }
418
419 tok_reset(tok);
420 tok_end(tok);
421
422 // Check for the exit command.
423 if (ret == KNOT_CTL_ESTOP) {
424 break;
425 }
426 }
427
428 history_end(hist);
429 free(hist_file);
430
431 el_end(el);
432
433 return KNOT_EOK;
434 }
435