Skip to content

Commit fcdeec0

Browse files
committed
Fix Redirection http redirection across different hosts
Signed-off-by: Paolo Di Tommaso <[email protected]>
1 parent dd32f80 commit fcdeec0

File tree

3 files changed

+60
-28
lines changed

3 files changed

+60
-28
lines changed

modules/nf-httpfs/src/main/nextflow/file/http/XFileSystemProvider.groovy

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package nextflow.file.http
1818

19+
import static nextflow.file.http.XFileSystemConfig.*
20+
1921
import java.nio.ByteBuffer
2022
import java.nio.channels.SeekableByteChannel
2123
import java.nio.file.AccessDeniedException
@@ -44,13 +46,9 @@ import groovy.transform.PackageScope
4446
import groovy.util.logging.Slf4j
4547
import nextflow.SysEnv
4648
import nextflow.extension.FilesEx
49+
import nextflow.file.FileHelper
4750
import nextflow.util.InsensitiveMap
4851
import sun.net.www.protocol.ftp.FtpURLConnection
49-
50-
import static XFileSystemConfig.*
51-
52-
import static nextflow.file.http.XFileSystemConfig.config
53-
5452
/**
5553
* Implements a read-only JSR-203 compliant file system provider for http/ftp protocols
5654
*
@@ -64,6 +62,7 @@ abstract class XFileSystemProvider extends FileSystemProvider {
6462

6563
private Map<URI, FileSystem> fileSystemMap = new LinkedHashMap<>(20)
6664

65+
private static final int[] REDIRECT_CODES = [301, 302, 307, 308]
6766

6867
protected static String config(String name, def defValue) {
6968
return SysEnv.containsKey(name) ? SysEnv.get(name) : defValue.toString()
@@ -185,18 +184,25 @@ abstract class XFileSystemProvider extends FileSystemProvider {
185184
protected URLConnection toConnection0(URL url, int attempt) {
186185
final conn = url.openConnection()
187186
conn.setRequestProperty("User-Agent", 'Nextflow/httpfs')
187+
if( conn instanceof HttpURLConnection ) {
188+
// by default HttpURLConnection does redirect only within the same host
189+
// disable the built-in to implement custom redirection logic (see below)
190+
conn.setInstanceFollowRedirects(false)
191+
}
188192
if( url.userInfo ) {
189193
conn.setRequestProperty("Authorization", auth(url.userInfo));
190194
}
191195
else {
192196
XAuthRegistry.instance.authorize(conn)
193197
}
194-
if ( conn instanceof HttpURLConnection && conn.getResponseCode() in [307, 308] && attempt < MAX_REDIRECT_HOPS ) {
198+
if ( conn instanceof HttpURLConnection && conn.getResponseCode() in REDIRECT_CODES && attempt < MAX_REDIRECT_HOPS ) {
195199
final header = InsensitiveMap.of(conn.getHeaderFields())
196-
String location = header.get("Location")?.get(0)
197-
URL newPath = new URI(location).toURL()
198-
log.debug "Remote redirect URL: $newPath"
199-
return toConnection0(newPath, attempt+1)
200+
final location = header.get("Location")?.get(0)
201+
log.debug "Remote redirect location: $location"
202+
final newUrl = new URI(absLocation(location,url)).toURL()
203+
if( url.protocol=='https' && newUrl.protocol=='http' )
204+
throw new IOException("Refuse to follow redirection from HTTPS to HTTP (unsafe) URL - origin: $url - target: $newUrl")
205+
return toConnection0(newUrl, attempt+1)
200206
}
201207
else if( conn instanceof HttpURLConnection && conn.getResponseCode() in config().retryCodes() && attempt < config().maxAttempts() ) {
202208
final delay = (Math.pow(config().backOffBase(), attempt) as long) * config().backOffDelay()
@@ -212,6 +218,18 @@ abstract class XFileSystemProvider extends FileSystemProvider {
212218
return conn
213219
}
214220

221+
protected String absLocation(String location, URL target) {
222+
assert location, "Missing location argument"
223+
assert target, "Missing target URL argument"
224+
225+
final base = FileHelper.baseUrl(location)
226+
if( base )
227+
return location
228+
if( !location.startsWith('/') )
229+
location = '/' + location
230+
return "${target.protocol}://${target.authority}$location"
231+
}
232+
215233
@Override
216234
SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
217235

modules/nf-httpfs/src/test/nextflow/file/http/HttpFilesTests.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ class HttpFilesTests extends Specification {
112112
def lines = Files.readAllLines(path, Charset.forName('UTF-8'))
113113
then:
114114
lines.size()>0
115-
lines[0] == '<html>'
115+
lines[0] == '<!DOCTYPE html>'
116116

117117
}
118118

modules/nf-httpfs/src/test/nextflow/file/http/XFileSystemProviderTest.groovy

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import java.nio.file.Path
2121

2222
import com.github.tomakehurst.wiremock.junit.WireMockRule
2323
import com.github.tomjankes.wiremock.WireMockGroovy
24-
import nextflow.SysEnv
2524
import org.junit.Rule
2625
import spock.lang.Specification
2726
import spock.lang.Unroll
@@ -106,7 +105,7 @@ class XFileSystemProviderTest extends Specification {
106105
when:
107106
def attrs = fsp.readHttpAttributes(path)
108107
then:
109-
attrs.lastModifiedTime() == null
108+
attrs.lastModifiedTime()
110109
attrs.size() > 0
111110
}
112111

@@ -157,6 +156,7 @@ class XFileSystemProviderTest extends Specification {
157156
@Rule
158157
WireMockRule wireMockRule = new WireMockRule(18080)
159158

159+
@Unroll
160160
def 'should follow a redirect when read a http file '() {
161161
given:
162162
def wireMock = new WireMockGroovy(18080)
@@ -180,14 +180,14 @@ class XFileSystemProviderTest extends Specification {
180180
response {
181181
status HTTP_CODE
182182
headers {
183-
"Location" "http://localhost:18080/redirected.html"
183+
"Location" "http://localhost:18080/target.html"
184184
}
185185
}
186186
}
187187
wireMock.stub {
188188
request {
189189
method "GET"
190-
url "/redirected.html"
190+
url "/target.html"
191191
}
192192
response {
193193
status 200
@@ -212,22 +212,36 @@ class XFileSystemProviderTest extends Specification {
212212
Files.size(path) == EXPECTED
213213

214214
where:
215-
HTTP_CODE | REDIRECT_TO | EXPECTED
216-
300 | "/redirected.html" | 10
217-
300 | "/index2.html" | 10
218-
219-
301 | "/redirected.html" | 10
220-
301 | "/index2.html" | 10
215+
HTTP_CODE | REDIRECT_TO | EXPECTED
216+
301 | "/target.html" | 10
217+
301 | "/index2.html" | 10
221218

222-
302 | "/redirected.html" | 10
223-
302 | "/index2.html" | 10
219+
302 | "/target.html" | 10
220+
302 | "/index2.html" | 10
224221

225-
307 | "/redirected.html" | 10
226-
307 | "/index2.html" | 10
222+
307 | "/target.html" | 10
223+
307 | "/index2.html" | 10
227224

228-
308 | "/redirected.html" | 10
229-
308 | "/index2.html" | 10
225+
308 | "/target.html" | 10
226+
308 | "/index2.html" | 10
230227
//infinite redirect to himself
231-
308 | "/index.html" | -1
228+
308 | "/index.html" | -1
229+
}
230+
231+
def 'should normalize location' () {
232+
given:
233+
def provider = Spy(XFileSystemProvider)
234+
235+
expect:
236+
provider.absLocation(LOCATION, new URL(TARGET)) == EXPECTED
237+
238+
where:
239+
LOCATION | TARGET | EXPECTED
240+
'https://this/that' | 'http://foo.com:123' | 'https://this/that'
241+
'/' | 'http://foo.com:123' | 'http://foo.com:123/'
242+
'/this/that' | 'http://foo.com:123' | 'http://foo.com:123/this/that'
243+
'/this/that' | 'http://foo.com:123/abc' | 'http://foo.com:123/this/that'
244+
'this/that' | 'http://foo.com:123/abc' | 'http://foo.com:123/this/that'
245+
232246
}
233247
}

0 commit comments

Comments
 (0)