summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/docs/1117
-rw-r--r--lib/Multi/API.pm40
2 files changed, 51 insertions, 6 deletions
diff --git a/data/docs/11 b/data/docs/11
index 202d9024..70c5b2f5 100644
--- a/data/docs/11
+++ b/data/docs/11
@@ -53,7 +53,14 @@ server resources and prevent abuse of this service.</p>
<li>5 connections per IP. All connections that are opened after reaching this limit will be immediately closed.</li>
<li>3 connections per user. The login command will reply with a 'sesslimit' error when reaching this limit.</li>
<li>Each command currently returns at most 10 results. <i>TODO: make configurable?</i></li>
- <li><i>more to come...</i></li>
+ <li>30 commands per minute per user. Server will reply with a 'throttled' error (type="cmd") when reaching this limit.</li>
+ <li>
+ 1 second of SQL time per minute per user. SQL time is the total time taken to
+ run the database queries for each command. This depends on both the command
+ (filters and get flags) and server load, and is thus not very predictable.
+ Server will reply with a 'throttled' error with type="sql" upon reaching
+ this limit.
+ </li>
</ul>
<br />
@@ -421,6 +428,14 @@ however still required.<br />
<dt>missing</dt><dd>A JSON object argument is missing a required member. The name of which is given in the additional "field" member.</dd>
<dt>badarg</dt><dd>A JSON value is of the wrong type or in the wrong format. The name of the incorrect field is given in a "field" member.</dd>
<dt>needlogin</dt><dd>Need to be logged in to issue this command.</dd>
+ <dt>throttled</td><dd>
+ You have used too many server resources within a short time, and need to wait
+ a bit before sending the next command. The type of throttle is given in the
+ "type" member, and the "minwait" and "fullwait" members tell you how long you
+ need to wait before sending the next command and when you can start bursting
+ again (this is the recommended waiting time), respectively. Both values are in
+ seconds, with one decimal after the point.
+ </dd>
<dt>auth</dt><dd>(login) Incorrect username/password combination.</dd>
<dt>loggedin</dt><dd>(login) Already logged in. Only one successful login command can be issues on one connection.</dd>
<dt>sesslimit</dt><dd>(login) Too many open sessions for the current user.</dd>
diff --git a/lib/Multi/API.pm b/lib/Multi/API.pm
index 3271e27b..7b36755d 100644
--- a/lib/Multi/API.pm
+++ b/lib/Multi/API.pm
@@ -13,6 +13,7 @@ use POE 'Wheel::SocketFactory', 'Wheel::ReadWrite';
use POE::Filter::VNDBAPI 'encode_filters';
use Digest::SHA 'sha256_hex';
use Encode 'encode_utf8';
+use Time::HiRes 'time'; # important for throttling
# not exported by Socket, taken from netinet/tcp.h (specific to Linux, AFAIK)
@@ -21,6 +22,11 @@ sub TCP_KEEPINTVL { 5 }
sub TCP_KEEPCNT { 6 }
+# Global throttle hash, key = username, value = [ cmd_time, sql_time ]
+# TODO: clean up items in this hash when username isn't connected anymore and throttle times < current time
+my %throttle;
+
+
sub spawn {
my $p = shift;
POE::Session->create(
@@ -37,6 +43,8 @@ sub spawn {
conn_per_ip => 5,
sess_per_user => 3,
tcp_keepalive => [ 120, 60, 3 ], # time, intvl, probes
+ throttle_cmd => [ 2, 30 ], # interval between each command, allowed burst
+ throttle_sql => [ 60, 1 ], # sql time multiplier, allowed burst (in sql time)
@_,
c => {},
},
@@ -247,13 +255,28 @@ sub client_input {
# when we're here, we can assume that $cmd contains a valid command
# and the arguments are syntactically valid
- # login
+ # handle login command
return $_[KERNEL]->yield(login => $c, @$arg) if $cmd eq 'login';
-
return cerr $c, needlogin => 'Not logged in.' if !$c->{username};
- # TODO: throttling
- # get
+ # update throttle array of the current user
+ my $time = time;
+ $_ < $time && ($_ = $time) for @{$c->{throttle}};
+
+ # check for thottle rule violation
+ my @limits = ('cmd', 'sql');
+ for (0..$#limits) {
+ my $threshold = $_[HEAP]{"throttle_$limits[$_]"}[0]*$_[HEAP]{"throttle_$limits[$_]"}[1];
+ return cerr $c, throttled => 'Throttle limit reached.', type => $limits[$_],
+ minwait => int(10*($c->{throttle}[$_]-$time-$threshold))/10+1,
+ fullwait => int(10*($c->{throttle}[$_]-$time))/10+1
+ if $c->{throttle}[$_]-$time > $threshold;
+ }
+
+ # update commands/second throttle
+ $c->{throttle}[0] += $_[HEAP]{throttle_cmd}[0];
+
+ # handle get command
return cerr $c, 'parse', "Unkown command '$cmd'" if $cmd ne 'get';
my $type = shift @$arg;
return cerr $c, 'gettype', "Unknown get type: '$type'" if $type ne 'vn';
@@ -293,8 +316,12 @@ sub login_res { # num, res, [ c, arg ]
my $encrypted = sha256_hex($VNDB::S{global_salt}.encode_utf8($arg->{password}).encode_utf8($res->[0]{salt}));
return cerr $c, auth => "Wrong password for user '$arg->{username}'" if lc($encrypted) ne lc($res->[0]{passwd});
- $c->{wheel}->put(['ok']);
+ # link this connection to the users' throttle array (create this if necessary)
+ $throttle{$arg->{username}} = [ time, time ] if !$throttle{$arg->{username}};
+ $c->{throttle} = $throttle{$arg->{username}};
+
$c->{username} = $arg->{username};
+ $c->{wheel}->put(['ok']);
$_[KERNEL]->yield(log => $c,
'Successful login by %s using client "%s" ver. %s', $arg->{username}, $arg->{client}, $arg->{clientver});
}
@@ -425,6 +452,9 @@ sub get_vn_res {
\@ids, 'get_vn_res', { %$get, type => 'relations' });
}
+ # update sql throttle
+ $get->{c}{throttle}[1] += $get->{time}*$_[HEAP]{throttle_sql}[0];
+
# send and log
delete $_->{latest} for @{$get->{list}};
$num = @{$get->{list}};