Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ low_speed_time = 0
# average transfer speed to be below during "low_speed_time" seconds
low_speed_rate = 0

# average download speed cap in bytes per second for rate limiting
# has no effect when stream_bundle = true
download_speed_limit = 0

# reboot after a successful update
post_update_reboot = false

Expand Down
6 changes: 6 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ Optional options:
See https://curl.se/libcurl/c/CURLOPT_LOW_SPEED_LIMIT.html.
Has no effect when used with ``stream_bundle=true``.

``download_speed_limit=<bytes per second>``
Average download speed cap for rate-limited environments.
Defaults to ``0`` (unlimited).
See https://curl.se/libcurl/c/CURLOPT_MAX_RECV_SPEED_LARGE.html.
Has no effect when used with ``stream_bundle=true``.

``resume_downloads=<boolean>``
Whether to resume aborted downloads or not.
Defaults to ``false``.
Expand Down
1 change: 1 addition & 0 deletions include/config-file.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ typedef struct Config_ {
int retry_wait; /**< wait between retries */
int low_speed_time; /**< time to be below the speed to trigger low speed abort */
int low_speed_rate; /**< low speed limit to abort transfer */
gint64 download_speed_limit; /**< average download speed cap for rate limiting */
GLogLevelFlags log_level; /**< log level */
GHashTable* device; /**< Additional attributes sent to hawkBit */
gboolean send_download_authentication; /**< Send security header in download requests */
Expand Down
42 changes: 42 additions & 0 deletions src/config-file.c
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,46 @@ static gboolean get_key_int(GKeyFile *key_file, const gchar *group, const gchar
return TRUE;
}

/**
* @brief Get 64-bit integer value from key_file for key in group, default_value must be specified,
* returned in case key not found in group.
*
* @param[in] key_file GKeyFile to look value up
* @param[in] group A group name
* @param[in] key A key
* @param[out] value Output 64-bit integer value
* @param[in] default_value Return this value in case no value found
* @param[out] error Error
* @return FALSE on error (error is set), TRUE otherwise. Note that TRUE is returned if key in
* group is not found, value is set to default_value in this case.
*/
static gboolean get_key_int64(GKeyFile *key_file, const gchar *group, const gchar *key, gint64 *value,
const gint64 default_value, GError **error)
{
GError *ierror = NULL;
gint64 val;

g_return_val_if_fail(key_file, FALSE);
g_return_val_if_fail(group, FALSE);
g_return_val_if_fail(key, FALSE);
g_return_val_if_fail(value, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

val = g_key_file_get_int64(key_file, group, key, &ierror);

if (g_error_matches(ierror, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) {
g_clear_error(&ierror);
*value = default_value;
return TRUE;
} else if (ierror) {
g_propagate_error(error, ierror);
return FALSE;
}

*value = val;
return TRUE;
}

/**
* @brief Get GHashTable containing keys/values from group in key_file.
*
Expand Down Expand Up @@ -328,6 +368,8 @@ Config* load_config_file(const gchar *config_file, GError **error)
return NULL;
if (!get_key_int(ini_file, "client", "low_speed_time", &config->low_speed_time, 60, error))
return NULL;
if (!get_key_int64(ini_file, "client", "download_speed_limit", &config->download_speed_limit, 0, error))
return NULL;
if (!get_key_bool(ini_file, "client", "resume_downloads", &config->resume_downloads, FALSE,
error))
return NULL;
Expand Down
2 changes: 2 additions & 0 deletions src/hawkbit-client.c
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ static gboolean get_binary(const gchar *download_url, const gchar *file, curl_of
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, hawkbit_config->low_speed_time);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, hawkbit_config->low_speed_rate);

curl_easy_setopt(curl, CURLOPT_MAX_RECV_SPEED_LARGE, (curl_off_t) hawkbit_config->download_speed_limit);

curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, resume_from);

if (hawkbit_config->send_download_authentication &&
Expand Down
28 changes: 28 additions & 0 deletions test/test_download.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# SPDX-License-Identifier: LGPL-2.1-only
# SPDX-FileCopyrightText: 2021-2025 Bastian Krause <bst@pengutronix.de>, Pengutronix

import os
import re
import time

from helper import run

Expand Down Expand Up @@ -193,3 +195,29 @@ def test_download_only_without_auth_header(hawkbit, adjust_config, assign_bundle
assert status[0]['type'] == 'downloaded'
# check last status message
assert 'File checksum OK.' in status[0]['messages']

def test_download_speed_limit(hawkbit, assign_bundle, adjust_config, rauc_bundle):
"""Test client-side download speed limiting."""
bundle_size = os.path.getsize(rauc_bundle)
speed_limit = 100 * 1024 # 100KB/s
min_time = bundle_size / speed_limit * 0.8 # 20% tolerance since cap is across average speed

config = adjust_config({'client': {'download_speed_limit': str(speed_limit)}})
assign_bundle(params={'type': 'downloadonly'}) # Don't attempt installation that would fail

start_time = time.monotonic()
out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r', timeout=120)
elapsed_time = time.monotonic() - start_time

assert 'Start downloading' in out
assert 'Download complete.' in out
assert 'File checksum OK.' in out
assert err == ''
assert exitcode == 0

status = hawkbit.get_action_status()
assert status[0]['type'] == 'downloaded'

assert elapsed_time >= min_time, \
f'Download too fast: {bundle_size} bytes in {elapsed_time:.2f}s ' \
f'(average speed: {bundle_size/elapsed_time:.0f} B/s, limit: {speed_limit} B/s)'
Loading