Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file app/bootstrap.c
6 : * @brief Shared startup path: directories, logger, config overrides (FEAT-38).
7 : */
8 :
9 : #include "app/bootstrap.h"
10 : #include "app/dc_config.h"
11 :
12 : #include "logger.h"
13 : #include "fs_util.h"
14 : #include "platform/path.h"
15 : #include "telegram_server_key.h"
16 :
17 : #include <stdio.h>
18 : #include <string.h>
19 : #include <stdlib.h>
20 :
21 : #define BOOTSTRAP_CONFIG_SUBDIR "tg-cli"
22 : #define BOOTSTRAP_CONFIG_FILE "config.ini"
23 :
24 : /**
25 : * Read the value of @p key from INI file at @p path into @p out (cap @p cap).
26 : *
27 : * Supports two value formats:
28 : * key = single line value (unquoted, trailing whitespace stripped)
29 : * key = "multi (double-quoted: content spans until the
30 : * line next " on any line; newlines preserved)
31 : * line"
32 : *
33 : * Respects comment lines (# and ;). Last occurrence of the key wins.
34 : * Returns 0 on success, -1 if not found, -2 if value exceeds cap-1 bytes.
35 : */
36 12 : static int bootstrap_read_ini_key(const char *path, const char *key,
37 : char *out, size_t cap) {
38 12 : FILE *fp = fopen(path, "r");
39 12 : if (!fp) return -1;
40 :
41 : char line[8192];
42 0 : size_t klen = strlen(key);
43 0 : int result = -1;
44 :
45 0 : while (fgets(line, sizeof(line), fp)) {
46 : /* Detect truncated line. */
47 0 : size_t line_len = strlen(line);
48 0 : if (line_len == sizeof(line) - 1 && line[line_len - 1] != '\n') {
49 : int c;
50 0 : while ((c = fgetc(fp)) != '\n' && c != EOF) {}
51 0 : fprintf(stderr,
52 : "bootstrap: config.ini: line for key '%s' exceeds %zu bytes"
53 : " and was truncated\n", key, sizeof(line) - 1);
54 : }
55 :
56 0 : char *p = line;
57 0 : while (*p == ' ' || *p == '\t') p++;
58 0 : if (!*p || *p == '\n' || *p == '\r' || *p == '#' || *p == ';') continue;
59 0 : if (strncmp(p, key, klen) != 0) continue;
60 0 : p += klen;
61 0 : while (*p == ' ' || *p == '\t') p++;
62 0 : if (*p != '=') continue;
63 0 : p++;
64 0 : while (*p == ' ' || *p == '\t') p++;
65 :
66 0 : if (*p == '"') {
67 : /* Quoted value — may span multiple lines until the next '"'. */
68 0 : p++; /* skip opening '"' */
69 0 : size_t pos = 0;
70 0 : for (;;) {
71 0 : char *close = strchr(p, '"');
72 0 : if (close) {
73 : /* Closing quote found on this line. */
74 0 : size_t chunk = (size_t)(close - p);
75 0 : if (pos + chunk >= cap) { fclose(fp); return -2; }
76 0 : memcpy(out + pos, p, chunk);
77 0 : pos += chunk;
78 0 : out[pos] = '\0';
79 0 : result = (pos > 0) ? 0 : -1;
80 0 : break;
81 : }
82 : /* No closing quote yet — copy line content + '\n', read next. */
83 0 : size_t chunk = strlen(p);
84 : /* Strip trailing CR/LF before appending '\n'. */
85 0 : while (chunk > 0 && (p[chunk-1] == '\n' || p[chunk-1] == '\r'))
86 0 : chunk--;
87 0 : if (pos + chunk + 1 >= cap) { fclose(fp); return -2; }
88 0 : memcpy(out + pos, p, chunk);
89 0 : pos += chunk;
90 0 : out[pos++] = '\n';
91 0 : out[pos] = '\0';
92 0 : if (!fgets(line, sizeof(line), fp)) {
93 0 : result = (pos > 0) ? 0 : -1;
94 0 : break;
95 : }
96 0 : p = line;
97 : }
98 : } else {
99 : /* Unquoted single-line value — rtrim whitespace. */
100 0 : size_t n = strlen(p);
101 0 : if (n >= cap) { fclose(fp); return -2; }
102 0 : memcpy(out, p, n);
103 0 : out[n] = '\0';
104 0 : while (n > 0 && (out[n-1] == '\n' || out[n-1] == '\r' ||
105 0 : out[n-1] == ' ' || out[n-1] == '\t'))
106 0 : out[--n] = '\0';
107 0 : result = (n > 0) ? 0 : -1;
108 : }
109 : }
110 0 : fclose(fp);
111 0 : return result;
112 : }
113 :
114 : /**
115 : * Apply DC-host and RSA-key overrides from config.ini.
116 : */
117 2 : static void apply_config_overrides(const char *config_dir) {
118 2 : if (!config_dir) return;
119 :
120 : char path[1024];
121 2 : snprintf(path, sizeof(path), "%s/%s/%s",
122 : config_dir, BOOTSTRAP_CONFIG_SUBDIR, BOOTSTRAP_CONFIG_FILE);
123 :
124 2 : logger_log(LOG_INFO, "bootstrap: reading config from %s", path);
125 :
126 : /* rsa_pem */
127 : char rsa_buf[8192];
128 2 : int rc = bootstrap_read_ini_key(path, "rsa_pem", rsa_buf, sizeof(rsa_buf));
129 2 : if (rc == 0) {
130 0 : size_t pem_len = strlen(rsa_buf);
131 : /* Log the first line of the PEM to confirm format, not the full key. */
132 : char first_line[80];
133 0 : size_t fl = 0;
134 0 : while (fl < pem_len && fl < sizeof(first_line) - 1 &&
135 0 : rsa_buf[fl] != '\n' && rsa_buf[fl] != '\\') {
136 0 : first_line[fl] = rsa_buf[fl];
137 0 : fl++;
138 : }
139 0 : first_line[fl] = '\0';
140 0 : logger_log(LOG_INFO,
141 : "bootstrap: rsa_pem found (%zu chars), first line: \"%s\"",
142 : pem_len, first_line);
143 0 : if (telegram_server_key_set_override(rsa_buf) != 0) {
144 0 : logger_log(LOG_ERROR,
145 : "bootstrap: rsa_pem could not be loaded as an RSA public key. "
146 : "Expected PEM starting with "
147 : "\"-----BEGIN PUBLIC KEY-----\" (PKCS#8) or "
148 : "\"-----BEGIN RSA PUBLIC KEY-----\" (PKCS#1)");
149 : }
150 2 : } else if (rc == -2) {
151 0 : logger_log(LOG_ERROR,
152 : "bootstrap: rsa_pem value in config.ini was truncated "
153 : "(exceeds 8191 bytes) — key not loaded");
154 : } else {
155 2 : logger_log(LOG_WARN,
156 : "bootstrap: rsa_pem not found in %s", path);
157 : }
158 :
159 : /* dc_N_host overrides (1..5). */
160 12 : for (int id = 1; id <= 5; id++) {
161 : char key[16];
162 10 : snprintf(key, sizeof(key), "dc_%d_host", id);
163 : char host_buf[256];
164 10 : if (bootstrap_read_ini_key(path, key, host_buf, sizeof(host_buf)) == 0) {
165 0 : dc_config_set_host_override(id, host_buf);
166 0 : logger_log(LOG_INFO,
167 : "bootstrap: DC %d host overridden to %s", id, host_buf);
168 : }
169 : }
170 : }
171 :
172 2 : int app_bootstrap(AppContext *ctx, const char *program_name) {
173 2 : if (!ctx || !program_name) return -1;
174 :
175 2 : memset(ctx, 0, sizeof(*ctx));
176 2 : ctx->cache_dir = platform_cache_dir();
177 2 : ctx->config_dir = platform_config_dir();
178 :
179 2 : if (ctx->cache_dir) fs_mkdir_p(ctx->cache_dir, 0700);
180 2 : if (ctx->config_dir) fs_mkdir_p(ctx->config_dir, 0700);
181 :
182 2 : const char *base = ctx->cache_dir ? ctx->cache_dir : "/tmp/tg-cli";
183 2 : snprintf(ctx->log_path, sizeof(ctx->log_path), "%s/logs", base);
184 2 : fs_mkdir_p(ctx->log_path, 0700);
185 :
186 2 : size_t dir_len = strlen(ctx->log_path);
187 2 : snprintf(ctx->log_path + dir_len, sizeof(ctx->log_path) - dir_len,
188 : "/%s.log", program_name);
189 :
190 2 : logger_init(ctx->log_path, LOG_INFO);
191 2 : logger_log(LOG_INFO, "%s starting", program_name);
192 :
193 : /* Apply DC-host and RSA-key overrides from config.ini. */
194 2 : apply_config_overrides(ctx->config_dir);
195 :
196 2 : if (!telegram_server_key_get_pem()) {
197 0 : logger_log(LOG_ERROR,
198 : "No RSA public key configured. "
199 : "Add rsa_pem = <key> to ~/.config/tg-cli/config.ini "
200 : "(obtain your api_id, api_hash, and RSA key at https://my.telegram.org)");
201 0 : return -1;
202 : }
203 :
204 2 : return 0;
205 : }
206 :
207 2 : void app_shutdown(AppContext *ctx) {
208 : (void)ctx;
209 2 : logger_log(LOG_INFO, "shutdown");
210 2 : logger_close();
211 2 : }
|