Skip to content

Commit

Permalink
Fix SSF-703
Browse files Browse the repository at this point in the history
  • Loading branch information
nquinquenel committed Jan 27, 2025
1 parent cd6990a commit 1eb13c7
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ public void start() {
.setConnectionReuseStrategy(new DontKeepAliveReuseStrategy())
.setListenerPort(triedPort)
.setSocketConfig(socketConfig)
.addFilterFirst("CORS", new CorsFilter())
.addFilterFirst("RateLimiter", new RateLimitFilter())
.addFilterAfter("RateLimiter", "CORS", new CorsFilter())
.register("/sonarlint/api/status", statusRequestHandler)
.register("/sonarlint/api/token", generatedUserTokenHandler)
.register("/sonarlint/api/hotspots/show", showHotspotRequestHandler)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* SonarLint Core - Implementation
* Copyright (C) 2016-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.core.embedded.server;

import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.HttpFilterChain;
import org.apache.hc.core5.http.io.HttpFilterHandler;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.http.protocol.HttpContext;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

public class RateLimitFilter implements HttpFilterHandler {

private static final int MAX_REQUESTS_PER_ORIGIN = 10;
private static final long TIME_FRAME_MS = TimeUnit.SECONDS.toMillis(10);
private final ConcurrentHashMap<String, RequestCounter> requestCounters = new ConcurrentHashMap<>();

@Override
public void handle(ClassicHttpRequest request, HttpFilterChain.ResponseTrigger responseTrigger, HttpContext context, HttpFilterChain chain)
throws HttpException, IOException {
var originHeader = request.getHeader("Origin");
var origin = originHeader != null ? originHeader.getValue() : null;
if (origin == null) {
var response = new BasicClassicHttpResponse(HttpStatus.SC_BAD_REQUEST);
responseTrigger.submitResponse(response);
} else {
if (!isRequestAllowed(origin)) {
var response = new BasicClassicHttpResponse(HttpStatus.SC_TOO_MANY_REQUESTS);
responseTrigger.submitResponse(response);
} else {
chain.proceed(request, responseTrigger, context);
}
}
}

private boolean isRequestAllowed(String origin) {
long currentTime = System.currentTimeMillis();
var counter = requestCounters.computeIfAbsent(origin, k -> new RequestCounter(currentTime));
synchronized (counter) {
if (currentTime - counter.timestamp > TIME_FRAME_MS) {
counter.timestamp = currentTime;
counter.count = 1;
return true;
} else if (counter.count < MAX_REQUESTS_PER_ORIGIN) {
counter.count++;
return true;
} else {
return false;
}
}
}

private static class RequestCounter {
long timestamp;
int count;

RequestCounter(long timestamp) {
this.timestamp = timestamp;
this.count = 1;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ void it_should_return_the_ide_name_and_empty_description_if_the_origin_is_not_tr
var embeddedServerPort = backend.getEmbeddedServerPort();
var request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + embeddedServerPort + "/sonarlint/api/status"))
.header("Origin", "https://untrusted")
.GET().build();
var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());

Expand Down Expand Up @@ -141,4 +142,41 @@ void it_should_receive_bad_request_response_if_not_right_method(SonarLintTestHar
assertThat(responseToken.headers().map()).doesNotContainKey("Content-Security-Policy-Report-Only");
assertThat(responseStatus.headers().map()).doesNotContainKey("Content-Security-Policy-Report-Only");
}

@SonarLintTest
void it_should_rate_limit_origin_if_too_many_requests(SonarLintTestHarness harness) throws IOException, InterruptedException {
var fakeClient = harness.newFakeClient().build();
var backend = harness.newBackend().withEmbeddedServer().withClientName("ClientName").start(fakeClient);

var embeddedServerPort = backend.getEmbeddedServerPort();
var request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + embeddedServerPort + "/sonarlint/api/status"))
.header("Origin", "https://sonar")
.GET().build();
for (int i = 0; i < 10; i++) {
java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
}
var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());

assertThat(response)
.extracting(HttpResponse::statusCode, HttpResponse::body)
.containsExactly(HttpStatus.TOO_MANY_REQUESTS_429, "");
}

@SonarLintTest
void it_should_not_allow_request_if_origin_is_missing(SonarLintTestHarness harness) throws IOException, InterruptedException {
var fakeClient = harness.newFakeClient().build();
var backend = harness.newBackend().withEmbeddedServer().withClientName("ClientName").start(fakeClient);

var embeddedServerPort = backend.getEmbeddedServerPort();
var request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + embeddedServerPort + "/sonarlint/api/status"))
.GET().build();
var response = java.net.http.HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());

assertThat(response)
.extracting(HttpResponse::statusCode, HttpResponse::body)
.containsExactly(HttpStatus.BAD_REQUEST_400, "");
}

}

0 comments on commit 1eb13c7

Please sign in to comment.