Skip to content

Commit 9e4d503

Browse files
http_out: Added support for http_bearer_token_file configuration that allows a bearer token to be used in an Authorization header
Signed-off-by: Fernando Alexandre <fernandoalexandre@users.noreply.github.com>
1 parent fd5ea1f commit 9e4d503

6 files changed

Lines changed: 260 additions & 5 deletions

File tree

plugins/out_http/http.c

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,48 @@ static void append_headers(struct flb_http_client *c,
109109
}
110110
}
111111

112+
/*
113+
* Reads ctx->http_bearer_token_file fresh (no caching), trims trailing whitespace/newlines,
114+
* and sets the Authorization: Bearer header on the given HTTP client. Returns 0 on
115+
* success, -1 on read failure or empty-after-trim content (error already logged, path
116+
* and reason only, never token content).
117+
*/
118+
static int http_bearer_token_file_auth(struct flb_out_http *ctx,
119+
struct flb_http_client *c)
120+
{
121+
int ret;
122+
char *bearer_buf = NULL;
123+
size_t bearer_size = 0;
124+
125+
ret = flb_utils_read_file(ctx->http_bearer_token_file, &bearer_buf, &bearer_size);
126+
if (ret == -1) {
127+
flb_plg_error(ctx->ins, "could not read http_bearer_token_file '%s'",
128+
ctx->http_bearer_token_file);
129+
return -1;
130+
}
131+
132+
while (bearer_size > 0 &&
133+
(bearer_buf[bearer_size - 1] == '\r' ||
134+
bearer_buf[bearer_size - 1] == '\n' ||
135+
bearer_buf[bearer_size - 1] == ' ' ||
136+
bearer_buf[bearer_size - 1] == '\t')) {
137+
bearer_size--;
138+
}
139+
bearer_buf[bearer_size] = '\0';
140+
141+
if (bearer_size == 0) {
142+
flb_plg_error(ctx->ins, "http_bearer_token_file '%s' is empty after trimming",
143+
ctx->http_bearer_token_file);
144+
flb_free(bearer_buf);
145+
return -1;
146+
}
147+
148+
flb_http_bearer_auth(c, bearer_buf);
149+
flb_free(bearer_buf);
150+
151+
return 0;
152+
}
153+
112154
static int http_request(struct flb_out_http *ctx,
113155
const void *body, size_t body_len,
114156
const char *tag, int tag_len,
@@ -266,6 +308,17 @@ static int http_request(struct flb_out_http *ctx,
266308
if (ctx->http_user && ctx->http_passwd) {
267309
flb_http_basic_auth(c, ctx->http_user, ctx->http_passwd);
268310
}
311+
else if (ctx->http_bearer_token_file) {
312+
ret = http_bearer_token_file_auth(ctx, c);
313+
if (ret == -1) {
314+
if (payload_buf != body) {
315+
flb_free(payload_buf);
316+
}
317+
flb_http_client_destroy(c);
318+
flb_upstream_conn_release(u_conn);
319+
return FLB_RETRY;
320+
}
321+
}
269322

270323
flb_http_add_header(c, "User-Agent", 10, "Fluent-Bit", 10);
271324

@@ -723,6 +776,14 @@ static struct flb_config_map config_map[] = {
723776
0, FLB_TRUE, offsetof(struct flb_out_http, http_passwd),
724777
"Set HTTP auth password"
725778
},
779+
{
780+
FLB_CONFIG_MAP_STR, "http_bearer_token_file", NULL,
781+
0, FLB_TRUE, offsetof(struct flb_out_http, http_bearer_token_file),
782+
"Path to a file containing a Bearer token. The file is re-read on every "
783+
"outgoing request (no caching), so token rotation on disk is picked up "
784+
"without a restart. Mutually exclusive with http_user/http_passwd and "
785+
"oauth2.enable."
786+
},
726787
{
727788
FLB_CONFIG_MAP_BOOL, "oauth2.enable", "false",
728789
0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.enabled),

plugins/out_http/http.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ struct flb_out_http {
4141
/* HTTP Auth */
4242
char *http_user;
4343
char *http_passwd;
44+
char *http_bearer_token_file;
4445

4546
/* AWS Auth */
4647
#ifdef FLB_HAVE_SIGNV4

plugins/out_http/http_conf.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,22 @@ struct flb_out_http *flb_http_conf_create(struct flb_output_instance *ins,
8787
}
8888
}
8989

90+
if (ctx->http_bearer_token_file) {
91+
if (ctx->http_user) {
92+
flb_plg_error(ctx->ins, "http_bearer_token_file cannot be used together with "
93+
"http_user/http_passwd (Basic Auth); configure exactly one "
94+
"authentication mechanism");
95+
flb_free(ctx);
96+
return NULL;
97+
}
98+
if (ctx->oauth2_config.enabled == FLB_TRUE) {
99+
flb_plg_error(ctx->ins, "http_bearer_token_file cannot be used together with "
100+
"oauth2.enable; configure exactly one authentication mechanism");
101+
flb_free(ctx);
102+
return NULL;
103+
}
104+
}
105+
90106
if (ctx->headers_key && !ctx->body_key) {
91107
flb_plg_error(ctx->ins, "when setting headers_key, body_key is also required");
92108
flb_free(ctx);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
service:
2+
flush: 1
3+
log_level: info
4+
http_server: on
5+
http_port: ${FLUENT_BIT_HTTP_MONITORING_PORT}
6+
7+
pipeline:
8+
inputs:
9+
- name: dummy
10+
tag: out_http
11+
dummy: '{"message":"hello from bearer_token_file","source":"dummy"}'
12+
13+
outputs:
14+
- name: http
15+
match: out_http
16+
host: 127.0.0.1
17+
port: ${TEST_SUITE_HTTP_PORT}
18+
uri: /data
19+
format: json
20+
json_date_key: false
21+
http_bearer_token_file: ${BEARER_TOKEN_FILE_TEST}

tests/integration/scenarios/out_http/tests/test_out_http_001.py

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,25 @@ def _wait_for_http_server_port(timeout=5):
5252

5353

5454
class Service:
55-
def __init__(self, config_file, *, response_setup=None, use_tls=False):
55+
def __init__(self, config_file, *, response_setup=None, use_tls=False, extra_env=None):
5656
self.config_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "../config", config_file))
5757
test_path = os.path.dirname(os.path.abspath(__file__))
5858
cert_dir = os.path.abspath(os.path.join(test_path, "../../in_splunk/certificate"))
5959
self.tls_crt_file = os.path.join(cert_dir, "certificate.pem")
6060
self.tls_key_file = os.path.join(cert_dir, "private_key.pem")
6161
self.response_setup = response_setup
6262
self.use_tls = use_tls
63+
env = {
64+
"CERTIFICATE_TEST": self.tls_crt_file,
65+
"PRIVATE_KEY_TEST": self.tls_key_file,
66+
}
67+
if extra_env:
68+
env.update(extra_env)
6369
self.service = FluentBitTestService(
6470
self.config_file,
6571
data_storage=data_storage,
6672
data_keys=["payloads", "requests"],
67-
extra_env={
68-
"CERTIFICATE_TEST": self.tls_crt_file,
69-
"PRIVATE_KEY_TEST": self.tls_key_file,
70-
},
73+
extra_env=env,
7174
pre_start=self._start_receiver,
7275
post_stop=self._stop_receiver,
7376
)
@@ -235,6 +238,95 @@ def test_out_http_oauth2_private_key_jwt_adds_bearer_token():
235238
assert data_request["headers"].get("Authorization") == "Bearer oauth-access-token"
236239

237240

241+
def test_out_http_bearer_token_file_adds_bearer_header(tmp_path):
242+
token_path = tmp_path / "bearer_token_file_test.token"
243+
token_path.write_text("test-token-value\n", encoding="utf-8")
244+
245+
service = Service(
246+
"out_http_bearer_token_file.yaml",
247+
extra_env={"BEARER_TOKEN_FILE_TEST": str(token_path)},
248+
)
249+
service.start()
250+
configure_http_response(status_code=200, body={"status": "received"})
251+
252+
requests_seen = service.wait_for_requests(1)
253+
service.stop()
254+
255+
data_request = requests_seen[0]
256+
assert data_request["headers"].get("Authorization") == "Bearer test-token-value"
257+
258+
259+
def test_out_http_bearer_token_file_picks_up_rotation(tmp_path):
260+
token_path = tmp_path / "bearer_token_file_rotation.token"
261+
token_path.write_text("token-v1", encoding="utf-8")
262+
263+
service = Service(
264+
"out_http_bearer_token_file.yaml",
265+
extra_env={"BEARER_TOKEN_FILE_TEST": str(token_path)},
266+
)
267+
service.start()
268+
configure_http_response(status_code=200, body={"status": "received"})
269+
270+
try:
271+
service.wait_for_requests(1)
272+
assert any(
273+
req["headers"].get("Authorization") == "Bearer token-v1"
274+
for req in data_storage["requests"]
275+
)
276+
277+
token_path.write_text("token-v2", encoding="utf-8")
278+
279+
service.service.wait_for_condition(
280+
lambda: True if any(
281+
req["headers"].get("Authorization") == "Bearer token-v2"
282+
for req in data_storage["requests"]
283+
) else None,
284+
timeout=15,
285+
interval=0.5,
286+
description="a request using the rotated bearer token",
287+
)
288+
finally:
289+
service.stop()
290+
291+
292+
def test_out_http_bearer_token_file_missing_retries(tmp_path):
293+
missing_token_path = tmp_path / "bearer_token_file_missing.token"
294+
295+
service = Service(
296+
"out_http_bearer_token_file.yaml",
297+
extra_env={"BEARER_TOKEN_FILE_TEST": str(missing_token_path)},
298+
)
299+
service.start()
300+
301+
try:
302+
log_text = service.wait_for_log_message("could not read http_bearer_token_file", timeout=15)
303+
finally:
304+
service.stop()
305+
306+
assert str(missing_token_path) in log_text
307+
assert "test-token-value" not in log_text
308+
assert not data_storage["requests"]
309+
310+
311+
def test_out_http_bearer_token_file_empty_retries(tmp_path):
312+
empty_token_path = tmp_path / "bearer_token_file_empty.token"
313+
empty_token_path.write_text("\n", encoding="utf-8")
314+
315+
service = Service(
316+
"out_http_bearer_token_file.yaml",
317+
extra_env={"BEARER_TOKEN_FILE_TEST": str(empty_token_path)},
318+
)
319+
service.start()
320+
321+
try:
322+
log_text = service.wait_for_log_message("is empty after trimming", timeout=15)
323+
finally:
324+
service.stop()
325+
326+
assert str(empty_token_path) in log_text
327+
assert not data_storage["requests"]
328+
329+
238330
def test_out_http_oauth2_timeout_retries_hung_token_endpoint():
239331
service = Service(
240332
"out_http_oauth2_timeout.yaml",

tests/runtime/out_http.c

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,66 @@ void flb_test_in_http()
11441144
test_ctx_destroy(ctx);
11451145
}
11461146

1147+
void flb_test_http_bearer_token_file_conflicts_with_http_user()
1148+
{
1149+
struct test_ctx *ctx;
1150+
int ret;
1151+
1152+
ctx = test_ctx_create();
1153+
if (!TEST_CHECK(ctx != NULL)) {
1154+
TEST_MSG("test_ctx_create failed");
1155+
exit(EXIT_FAILURE);
1156+
}
1157+
1158+
ret = flb_output_set(ctx->flb, ctx->o_ffd,
1159+
"match", "*",
1160+
"http_bearer_token_file", "/tmp/does-not-need-to-exist.token",
1161+
"http_user", "user1",
1162+
"http_passwd", "passwd1",
1163+
NULL);
1164+
TEST_CHECK(ret == 0);
1165+
1166+
ret = flb_start(ctx->flb);
1167+
if (!TEST_CHECK(ret != 0)) {
1168+
TEST_MSG("expected startup failure for http_bearer_token_file + http_user");
1169+
}
1170+
1171+
/* flb_start failed, so there is no running engine to stop. */
1172+
flb_destroy(ctx->flb);
1173+
flb_free(ctx);
1174+
}
1175+
1176+
void flb_test_http_bearer_token_file_conflicts_with_oauth2()
1177+
{
1178+
struct test_ctx *ctx;
1179+
int ret;
1180+
1181+
ctx = test_ctx_create();
1182+
if (!TEST_CHECK(ctx != NULL)) {
1183+
TEST_MSG("test_ctx_create failed");
1184+
exit(EXIT_FAILURE);
1185+
}
1186+
1187+
ret = flb_output_set(ctx->flb, ctx->o_ffd,
1188+
"match", "*",
1189+
"http_bearer_token_file", "/tmp/does-not-need-to-exist.token",
1190+
"oauth2.enable", "true",
1191+
"oauth2.token_url", "http://127.0.0.1:8888/oauth/token",
1192+
"oauth2.client_id", "client1",
1193+
"oauth2.client_secret", "secret1",
1194+
NULL);
1195+
TEST_CHECK(ret == 0);
1196+
1197+
ret = flb_start(ctx->flb);
1198+
if (!TEST_CHECK(ret != 0)) {
1199+
TEST_MSG("expected startup failure for http_bearer_token_file + oauth2.enable");
1200+
}
1201+
1202+
/* flb_start failed, so there is no running engine to stop. */
1203+
flb_destroy(ctx->flb);
1204+
flb_free(ctx);
1205+
}
1206+
11471207
/* Test list */
11481208
TEST_LIST = {
11491209
{"format_msgpack" , flb_test_format_msgpack},
@@ -1161,5 +1221,9 @@ TEST_LIST = {
11611221
{"json_date_format_iso8601" , flb_test_json_date_format_iso8601},
11621222
{"json_date_format_java_sql_timestamp" , flb_test_json_date_format_java_sql_timestamp},
11631223
{"in_http", flb_test_in_http},
1224+
{"http_bearer_token_file_conflicts_with_http_user",
1225+
flb_test_http_bearer_token_file_conflicts_with_http_user},
1226+
{"http_bearer_token_file_conflicts_with_oauth2",
1227+
flb_test_http_bearer_token_file_conflicts_with_oauth2},
11641228
{NULL, NULL}
11651229
};

0 commit comments

Comments
 (0)