Skip to content

Commit 098fe84

Browse files
authored
Add Git response max length check (#6190)
Signed-off-by: Paolo Di Tommaso <[email protected]>
1 parent 83428ee commit 098fe84

File tree

5 files changed

+182
-0
lines changed

5 files changed

+182
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2013-2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package nextflow.exception
18+
19+
import groovy.transform.CompileStatic
20+
import groovy.transform.InheritConstructors
21+
22+
/**
23+
* Exception thrown when the http response length exceed
24+
* the max allowed size.
25+
*
26+
* @author Paolo Di Tommaso <[email protected]>
27+
*/
28+
@CompileStatic
29+
@InheritConstructors
30+
class HttpResponseLengthExceedException extends IOException {
31+
}

modules/nextflow/src/main/groovy/nextflow/scm/RepositoryProvider.groovy

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ import groovy.transform.CompileStatic
4040
import groovy.transform.Memoized
4141
import groovy.util.logging.Slf4j
4242
import nextflow.Const
43+
import nextflow.SysEnv
4344
import nextflow.exception.AbortOperationException
45+
import nextflow.exception.HttpResponseLengthExceedException
4446
import nextflow.exception.RateLimitExceededException
4547
import nextflow.util.RetryConfig
4648
import nextflow.util.Threads
@@ -242,6 +244,7 @@ abstract class RepositoryProvider {
242244
final HttpResponse<byte[]> resp = httpSend0(request)
243245
// check the response code
244246
checkResponse(resp)
247+
checkMaxLength(resp)
245248
// return the body as byte array
246249
return resp.body()
247250
}
@@ -310,6 +313,17 @@ abstract class RepositoryProvider {
310313
}
311314
}
312315

316+
protected void checkMaxLength(HttpResponse<byte[]> response) {
317+
final max = SysEnv.getLong("NXF_GIT_RESPONSE_MAX_LENGTH", 0)
318+
if( max<=0 )
319+
return
320+
final length = response.headers().firstValueAsLong('Content-Length').orElse(0)
321+
if( length<=0 )
322+
return
323+
if( length>max )
324+
throw new HttpResponseLengthExceedException("HTTP response '${response.uri()}' is too big - response length: ${length}; max allowed length: ${max}")
325+
}
326+
313327
protected <T> List<T> invokeAndResponseWithPaging(String request, Closure<T> parse) {
314328
// this is needed because apparently bytebuddy used by testing framework is not able
315329
// to handle properly this method signature using both generics and `@Memoized` annotation.

modules/nextflow/src/test/groovy/nextflow/scm/RepositoryProviderTest.groovy

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
package nextflow.scm
1818

1919
import java.net.http.HttpClient
20+
import java.net.http.HttpRequest
2021
import java.net.http.HttpResponse
2122
import java.nio.channels.UnresolvedAddressException
23+
import javax.net.ssl.SSLSession
2224

25+
import nextflow.SysEnv
26+
import nextflow.exception.HttpResponseLengthExceedException
2327
import nextflow.util.RetryConfig
2428
import spock.lang.Specification
2529
/**
@@ -169,4 +173,82 @@ class RepositoryProviderTest extends Specification {
169173
false | new SocketTimeoutException()
170174
}
171175

176+
def 'should not validate when max length is not configured via SysEnv' () {
177+
given:
178+
def provider = Spy(RepositoryProvider)
179+
def response = createMockResponseWithContentLength(1500)
180+
181+
when:
182+
SysEnv.push([:])
183+
provider.checkMaxLength(response)
184+
then:
185+
noExceptionThrown()
186+
187+
cleanup:
188+
SysEnv.pop()
189+
}
190+
191+
def 'should validate and pass when content is within limit via SysEnv' () {
192+
given:
193+
def provider = Spy(RepositoryProvider)
194+
def response = createMockResponseWithContentLength(1500)
195+
196+
when:
197+
SysEnv.push(['NXF_GIT_RESPONSE_MAX_LENGTH': '2000'])
198+
provider.checkMaxLength(response)
199+
then:
200+
noExceptionThrown()
201+
202+
cleanup:
203+
SysEnv.pop()
204+
}
205+
206+
def 'should validate and fail when content exceeds limit via SysEnv' () {
207+
given:
208+
def provider = Spy(RepositoryProvider)
209+
def response = createMockResponseWithContentLength(1500)
210+
211+
when:
212+
SysEnv.push(['NXF_GIT_RESPONSE_MAX_LENGTH': '1000'])
213+
provider.checkMaxLength(response)
214+
then:
215+
thrown(HttpResponseLengthExceedException)
216+
217+
cleanup:
218+
SysEnv.pop()
219+
}
220+
221+
private createMockResponseWithContentLength(long contentLength) {
222+
return new HttpResponse<byte[]>() {
223+
@Override
224+
int statusCode() { return 200 }
225+
226+
@Override
227+
HttpRequest request() { return null }
228+
229+
@Override
230+
Optional<HttpResponse<byte[]>> previousResponse() { return Optional.empty() }
231+
232+
@Override
233+
java.net.http.HttpHeaders headers() {
234+
return java.net.http.HttpHeaders.of(
235+
['Content-Length': [contentLength.toString()]],
236+
(a, b) -> true
237+
)
238+
}
239+
240+
@Override
241+
byte[] body() { return new byte[0] }
242+
243+
@Override
244+
Optional<SSLSession> sslSession() { return Optional.empty() }
245+
246+
@Override
247+
URI uri() { return new URI('https://api.github.com/repos/test/repo') }
248+
249+
@Override
250+
HttpClient.Version version() { return HttpClient.Version.HTTP_1_1 }
251+
}
252+
}
253+
172254
}

modules/nf-commons/src/main/nextflow/SysEnv.groovy

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ class SysEnv {
5454
return Boolean.parseBoolean(result)
5555
}
5656

57+
static Integer getInteger(String name, Integer defValue) {
58+
final result = get(name, defValue!=null ? String.valueOf(defValue) : null)
59+
return result!=null ? Integer.valueOf(result) : null
60+
}
61+
62+
static Long getLong(String name, Long defValue) {
63+
final result = get(name, defValue!=null ? String.valueOf(defValue) : null)
64+
return result!=null ? Long.valueOf(result) : null
65+
}
66+
5767
static void push(Map<String,String> env) {
5868
history.push(holder.getTarget())
5969
holder.setTarget(env)

modules/nf-commons/src/test/nextflow/SysEnvTest.groovy

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,51 @@ class SysEnvTest extends Specification {
7373
[:] | true | true
7474
[FOO:'false'] | false | false
7575
[FOO:'true'] | true | true
76+
}
77+
78+
@Unroll
79+
def 'should get integer value' () {
80+
given:
81+
SysEnv.push(STATE)
82+
83+
expect:
84+
SysEnv.getInteger('FOO', DEF) == EXPECTED
7685

86+
where:
87+
STATE | DEF | EXPECTED
88+
[:] | null | null
89+
[FOO:'0'] | null | 0
90+
[FOO:'1'] | null | 1
91+
and:
92+
[:] | 0 | 0
93+
[FOO:'0'] | 0 | 0
94+
[FOO:'100'] | 0 | 100
95+
and:
96+
[:] | 1 | 1
97+
[FOO:'0'] | 1 | 0
98+
[FOO:'100'] | 1 | 100
99+
}
100+
101+
@Unroll
102+
def 'should get long value' () {
103+
given:
104+
SysEnv.push(STATE)
105+
106+
expect:
107+
SysEnv.getLong('FOO', DEF) == EXPECTED
108+
109+
where:
110+
STATE | DEF | EXPECTED
111+
[:] | null | null
112+
[FOO:'0'] | null | 0
113+
[FOO:'1'] | null | 1
114+
and:
115+
[:] | 0 | 0
116+
[FOO:'0'] | 0 | 0
117+
[FOO:'100'] | 0 | 100
118+
and:
119+
[:] | 1 | 1
120+
[FOO:'0'] | 1 | 0
121+
[FOO:'100'] | 1 | 100
77122
}
78123
}

0 commit comments

Comments
 (0)