Coverage Report

Created: 2026-08-13 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/proftpd/src/throttle.c
Line
Count
Source
1
/*
2
 * ProFTPD - FTP server daemon
3
 * Copyright (c) 2008-2026 The ProFTPD Project team
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License
16
 * along with this program; if not, see <https://www.gnu.org/licenses/>.
17
 *
18
 * As a special exemption, The ProFTPD Project and other respective copyright
19
 * holders give permission to link this program with OpenSSL, and distribute
20
 * the resulting executable, without including the source code for OpenSSL in
21
 * the source distribution.
22
 */
23
24
/* TransferRate throttling */
25
26
#include "conf.h"
27
28
/* Transfer rate variables */
29
static long double xfer_rate_kbps = 0.0, xfer_rate_bps = 0.0;
30
static off_t xfer_rate_freebytes = 0.0;
31
static int have_xfer_rate = FALSE;
32
static unsigned int xfer_rate_scoreboard_updates = 0;
33
34
/* Very similar to the {block,unblock}_signals() function, this masks most
35
 * of the same signals -- except for TERM.  This allows a throttling process
36
 * to be killed by the admin.
37
 */
38
0
static void xfer_rate_sigmask(int block) {
39
0
  static sigset_t sig_set;
40
41
0
  if (block) {
42
0
    sigemptyset(&sig_set);
43
44
0
    sigaddset(&sig_set, SIGCHLD);
45
0
    sigaddset(&sig_set, SIGUSR1);
46
0
    sigaddset(&sig_set, SIGINT);
47
0
    sigaddset(&sig_set, SIGQUIT);
48
0
#ifdef SIGIO
49
0
    sigaddset(&sig_set, SIGIO);
50
0
#endif /* SIGIO */
51
0
#ifdef SIGBUS
52
0
    sigaddset(&sig_set, SIGBUS);
53
0
#endif /* SIGBUS */
54
0
    sigaddset(&sig_set, SIGHUP);
55
56
0
    while (sigprocmask(SIG_BLOCK, &sig_set, NULL) < 0) {
57
0
      if (errno == EINTR) {
58
0
        pr_signals_handle();
59
0
        continue;
60
0
      }
61
62
0
      break;
63
0
    }
64
65
0
  } else {
66
0
    while (sigprocmask(SIG_UNBLOCK, &sig_set, NULL) < 0) {
67
0
      if (errno == EINTR) {
68
0
        pr_signals_handle();
69
0
        continue;
70
0
      }
71
72
0
      break;
73
0
    }
74
0
  }
75
0
}
76
77
/* Returns the difference, in milliseconds, between the given timeval and
78
 * now.
79
 */
80
0
static long xfer_rate_since(struct timeval *then) {
81
0
  struct timeval now;
82
0
  gettimeofday(&now, NULL);
83
84
0
  return (((now.tv_sec - then->tv_sec) * 1000L) +
85
0
    ((now.tv_usec - then->tv_usec) / 1000L));
86
0
}
87
88
0
int pr_throttle_have_rate(void) {
89
0
  return have_xfer_rate;
90
0
}
91
92
0
void pr_throttle_init(cmd_rec *cmd) {
93
0
  config_rec *c = NULL;
94
0
  char *xfer_cmd = NULL;
95
0
  unsigned char have_user_rate = FALSE, have_group_rate = FALSE,
96
0
    have_class_rate = FALSE;
97
0
  unsigned int precedence = 0;
98
99
  /* Make sure the variables are (re)initialized */
100
0
  xfer_rate_kbps = xfer_rate_bps = 0.0;
101
0
  xfer_rate_freebytes = 0;
102
0
  xfer_rate_scoreboard_updates = 0;
103
0
  have_xfer_rate = FALSE;
104
105
0
  c = find_config(CURRENT_CONF, CONF_PARAM, "TransferRate", FALSE);
106
107
  /* Note: need to cycle through all the matching config_recs, and using
108
   * the information from the current config_rec only if it matches
109
   * the target *and* has a higher precedence than any of the previously
110
   * found config_recs.
111
   */
112
0
  while (c) {
113
0
    char **cmdlist = (char **) c->argv[0];
114
0
    int matched_cmd = FALSE;
115
116
0
    pr_signals_handle();
117
118
    /* Does this TransferRate apply to the current command?  Note: this
119
     * could be made more efficient by using bitmasks rather than string
120
     * comparisons.
121
     */
122
0
    for (xfer_cmd = *cmdlist; xfer_cmd; xfer_cmd = *(cmdlist++)) {
123
0
      if (strcasecmp(xfer_cmd, cmd->argv[0]) == 0) {
124
0
        matched_cmd = TRUE;
125
0
        break;
126
0
      }
127
0
    }
128
129
    /* No -- continue on to the next TransferRate. */
130
0
    if (!matched_cmd) {
131
0
      c = find_config_next(c, c->next, CONF_PARAM, "TransferRate", FALSE);
132
0
      continue;
133
0
    }
134
135
0
    if (c->argc > 4) {
136
0
      if (strcasecmp(c->argv[4], "user") == 0) {
137
0
        if (pr_expr_eval_user_or((char **) &c->argv[5]) == TRUE &&
138
0
            *((unsigned int *) c->argv[3]) > precedence) {
139
140
          /* Set the precedence. */
141
0
          precedence = *((unsigned int *) c->argv[3]);
142
143
0
          xfer_rate_kbps = *((long double *) c->argv[1]);
144
0
          xfer_rate_freebytes = *((off_t *) c->argv[2]);
145
0
          have_xfer_rate = TRUE;
146
0
          have_user_rate = TRUE;
147
0
          have_group_rate = have_class_rate = FALSE;
148
0
        }
149
150
0
      } else if (strcasecmp(c->argv[4], "group") == 0) {
151
0
        if (pr_expr_eval_group_and((char **) &c->argv[5]) == TRUE &&
152
0
            *((unsigned int *) c->argv[3]) > precedence) {
153
154
          /* Set the precedence. */
155
0
          precedence = *((unsigned int *) c->argv[3]);
156
157
0
          xfer_rate_kbps = *((long double *) c->argv[1]);
158
0
          xfer_rate_freebytes = *((off_t *) c->argv[2]);
159
0
          have_xfer_rate = TRUE;
160
0
          have_group_rate = TRUE;
161
0
          have_user_rate = have_class_rate = FALSE;
162
0
        }
163
164
0
      } else if (strcasecmp(c->argv[4], "class") == 0) {
165
0
        if (pr_expr_eval_class_or((char **) &c->argv[5]) == TRUE &&
166
0
          *((unsigned int *) c->argv[3]) > precedence) {
167
168
          /* Set the precedence. */
169
0
          precedence = *((unsigned int *) c->argv[3]);
170
171
0
          xfer_rate_kbps = *((long double *) c->argv[1]);
172
0
          xfer_rate_freebytes = *((off_t *) c->argv[2]);
173
0
          have_xfer_rate = TRUE;
174
0
          have_class_rate = TRUE;
175
0
          have_user_rate = have_group_rate = FALSE;
176
0
        }
177
0
      }
178
179
0
    } else {
180
0
      if (*((unsigned int *) c->argv[3]) > precedence) {
181
        /* Set the precedence. */
182
0
        precedence = *((unsigned int *) c->argv[3]);
183
184
0
        xfer_rate_kbps = *((long double *) c->argv[1]);
185
0
        xfer_rate_freebytes = *((off_t *) c->argv[2]);
186
0
        have_xfer_rate = TRUE;
187
0
        have_user_rate = have_group_rate = have_class_rate = FALSE;
188
0
      }
189
0
    }
190
191
0
    c = find_config_next(c, c->next, CONF_PARAM, "TransferRate", FALSE);
192
0
  }
193
194
  /* Print out a helpful debugging message. */
195
0
  if (have_xfer_rate) {
196
0
    pr_log_debug(DEBUG3, "TransferRate (%.3Lf KB/s, %" PR_LU
197
0
        " bytes free) in effect%s", xfer_rate_kbps,
198
0
      (pr_off_t) xfer_rate_freebytes,
199
0
      have_user_rate ? " for current user" :
200
0
      have_group_rate ? " for current group" :
201
0
      have_class_rate ? " for current class" : "");
202
203
    /* Convert the configured Kbps to bytes per usec, for use later.
204
     * The 1024.0 factor converts for Kbytes to bytes, and the
205
     * 1000000.0 factor converts from secs to usecs.
206
     */
207
0
    xfer_rate_bps = xfer_rate_kbps * 1024.0;
208
0
  }
209
0
}
210
211
0
void pr_throttle_pause(off_t xferlen, int update_scoreboard, off_t xfer_done) {
212
0
  long ideal = 0, elapsed = 0;
213
0
  off_t orig_xferlen = xferlen;
214
215
0
  if (XFER_ABORTED) {
216
0
    return;
217
0
  }
218
219
  /* Calculate the time interval since the transfer of data started. */
220
0
  elapsed = xfer_rate_since(&session.xfer.start_time);
221
222
  /* Perform no throttling if no throttling has been configured. */
223
0
  if (!have_xfer_rate) {
224
0
    xfer_rate_scoreboard_updates++;
225
226
0
    if (update_scoreboard == TRUE ||
227
0
        xfer_rate_scoreboard_updates % PR_TUNABLE_XFER_SCOREBOARD_UPDATES == 0) {
228
      /* Update the scoreboard. */
229
0
      pr_scoreboard_entry_update(session.pid,
230
0
        PR_SCORE_XFER_LEN, orig_xferlen,
231
0
        PR_SCORE_XFER_DONE, xfer_done,
232
0
        PR_SCORE_XFER_ELAPSED, (unsigned long) elapsed,
233
0
        NULL);
234
235
0
      xfer_rate_scoreboard_updates = 0;
236
0
    }
237
238
0
    return;
239
0
  }
240
241
  /* Give credit for any configured freebytes. */
242
0
  if (xferlen > 0 &&
243
0
      xfer_rate_freebytes > 0) {
244
245
0
    if (xferlen > xfer_rate_freebytes) {
246
      /* Decrement the number of bytes transferred by the freebytes, so that
247
       * any throttling does not take into account the freebytes.
248
       */
249
0
      xferlen -= xfer_rate_freebytes;
250
251
0
    } else {
252
0
      xfer_rate_scoreboard_updates++;
253
254
      /* The number of bytes transferred is less than the freebytes.  Just
255
       * update the scoreboard -- no throttling needed.
256
       */
257
258
0
      if (update_scoreboard == TRUE ||
259
0
          xfer_rate_scoreboard_updates % PR_TUNABLE_XFER_SCOREBOARD_UPDATES == 0) {
260
0
        pr_scoreboard_entry_update(session.pid,
261
0
          PR_SCORE_XFER_LEN, orig_xferlen,
262
0
          PR_SCORE_XFER_DONE, xfer_done,
263
0
          PR_SCORE_XFER_ELAPSED, (unsigned long) elapsed,
264
0
          NULL);
265
266
0
        xfer_rate_scoreboard_updates = 0;
267
0
      }
268
269
0
      return;
270
0
    }
271
0
  }
272
273
0
  ideal = xferlen * 1000L / xfer_rate_bps;
274
275
0
  if (ideal > elapsed) {
276
0
    struct timeval tv;
277
278
    /* Setup for the select.  We use select() instead of usleep() because it
279
     * seems to be far more portable across platforms.
280
     *
281
     * ideal and elapsed are in milleconds, but tv_usec will be microseconds,
282
     * so be sure to convert properly.
283
     */
284
0
    tv.tv_usec = (ideal - elapsed) * 1000;
285
0
    tv.tv_sec = tv.tv_usec / 1000000L;
286
0
    tv.tv_usec = tv.tv_usec % 1000000L;
287
288
0
    pr_log_debug(DEBUG7, "transferring too fast, delaying %ld sec%s, %ld usecs",
289
0
      (long int) tv.tv_sec, tv.tv_sec == 1 ? "" : "s", (long int) tv.tv_usec);
290
291
    /* No interruptions, please... */
292
0
    xfer_rate_sigmask(TRUE);
293
294
0
    if (select(0, NULL, NULL, NULL, &tv) < 0) {
295
0
      int xerrno = errno;
296
297
0
      if (XFER_ABORTED) {
298
0
        pr_log_pri(PR_LOG_NOTICE, "throttling interrupted, transfer aborted");
299
0
        xfer_rate_sigmask(FALSE);
300
0
        return;
301
0
      }
302
303
      /* At this point, we've probably been interrupted by one of the few
304
       * signals not masked off, e.g. SIGTERM.
305
       */
306
0
      if (xerrno != EINTR) {
307
0
        pr_log_debug(DEBUG0, "unable to throttle bandwidth: %s",
308
0
          strerror(xerrno));
309
0
      }
310
0
    }
311
312
0
    xfer_rate_sigmask(FALSE);
313
0
    pr_signals_handle();
314
315
    /* Update the scoreboard. */
316
0
    pr_scoreboard_entry_update(session.pid,
317
0
      PR_SCORE_XFER_LEN, orig_xferlen,
318
0
      PR_SCORE_XFER_DONE, xfer_done,
319
0
      PR_SCORE_XFER_ELAPSED, (unsigned long) ideal,
320
0
      NULL);
321
322
0
  } else {
323
    /* Update the scoreboard. */
324
0
    pr_scoreboard_entry_update(session.pid,
325
0
      PR_SCORE_XFER_LEN, orig_xferlen,
326
0
      PR_SCORE_XFER_DONE, xfer_done,
327
0
      PR_SCORE_XFER_ELAPSED, (unsigned long) elapsed,
328
      NULL);
329
0
  }
330
0
}