Skip to content

Commit 3e6708b

Browse files
committed
fix: mixed-content on HTTP pages
Firefox 74 does not mark *.localhost subdomains as Secure Context yet (https://bugzilla.mozilla.org/show_bug.cgi?id=1220810#c23) so we can't redirect there when we have IPFS resource embedded on HTTPS page (eg. image loaded from a public gateway) because that would cause mixed-content warning and subresource would fail to load. Given the fact that localhost/ipfs/* provided by go-ipfs 0.5+ returns a redirect to *.ipfs.localhost subdomain we need to check requests for subresources, and manually replace 'localhost' hostname with '127.0.0.1' (IP is hardcoded as Secure Context in Firefox). The need for this workaround can be revisited when Firefox closes mentioned bug. Chromium 80 seems to force HTTPS in the final URL (after all redirects) so https://*.localhost fails. This needs additional research (could be a bug in Chromium). For now we reuse the same workaround as Firefox. To unify use of 127.0.0.1 and localhost in address bar (eg. when user opens an image in a new tab etc) when Subdomain Proxy is enabled we normalize address bar requests made to the local gateway and replace raw IP with 'localhost' hostname to take advantage of subdomain redirect provided by go-ipfs >= 0.5
1 parent 207fd76 commit 3e6708b

6 files changed

+146
-20
lines changed

add-on/src/lib/dnslink.js

+1
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ module.exports = function createDnslinkResolver (getState) {
201201
// in url.hostname OR in url.pathname (/ipns/<fqdn>)
202202
// and return matching FQDN if present
203203
findDNSLinkHostname (url) {
204+
if (!url) return
204205
// Normalize subdomain and path gateways to to /ipns/<fqdn>
205206
const contentPath = ipfsContentPath(url)
206207
if (IsIpfs.ipnsPath(contentPath)) {

add-on/src/lib/ipfs-path.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ function sameGateway (url, gwUrl) {
9494
return url.hostname === gwUrl.hostname
9595
}
9696

97-
const gws = [gwUrl.hostname]
97+
const gws = [gwUrl.host]
9898

9999
// localhost gateway has more than one hostname
100100
if (gwUrl.hostname === 'localhost') {
@@ -106,7 +106,7 @@ function sameGateway (url, gwUrl) {
106106

107107
for (const gwName of gws) {
108108
// match against the end to include subdomain gateways
109-
if (url.hostname.endsWith(gwName)) return true
109+
if (url.host.endsWith(gwName)) return true
110110
}
111111
return false
112112
}

add-on/src/lib/ipfs-request.js

+48-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const LRU = require('lru-cache')
99
const isIPFS = require('is-ipfs')
1010
const isFQDN = require('is-fqdn')
1111
const { pathAtHttpGateway, sameGateway } = require('./ipfs-path')
12+
const { safeURL } = require('./options')
1213

1314
const redirectOptOutHint = 'x-ipfs-companion-no-redirect'
1415
const recoverableNetworkErrors = new Set([
@@ -142,6 +143,15 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
142143
onBeforeRequest (request) {
143144
const state = getState()
144145
if (!state.active) return
146+
147+
// When Subdomain Proxy is enabled we normalize address bar requests made
148+
// to the local gateway and replace raw IP with 'localhost' hostname to
149+
// take advantage of subdomain redirect provided by go-ipfs >= 0.5
150+
if (state.redirect && request.type === 'main_frame' && sameGateway(request.url, state.gwURL)) {
151+
const redirectUrl = safeURL(request.url, { useLocalhostName: state.useSubdomainProxy }).toString()
152+
if (redirectUrl !== request.url) return { redirectUrl }
153+
}
154+
145155
// early sanity checks
146156
if (preNormalizationSkip(state, request)) {
147157
return
@@ -169,7 +179,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
169179
}
170180
// Detect valid /ipfs/ and /ipns/ on any site
171181
if (ipfsPathValidator.publicIpfsOrIpnsResource(request.url) && isSafeToRedirect(request, runtime)) {
172-
return redirectToGateway(request, request.url, state, ipfsPathValidator)
182+
return redirectToGateway(request, request.url, state, ipfsPathValidator, runtime)
173183
}
174184
// Detect dnslink using heuristics enabled in Preferences
175185
if (state.dnslinkPolicy && dnslinkResolver.canLookupURL(request.url)) {
@@ -358,7 +368,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
358368
return { redirectUrl }
359369
}
360370
}
361-
return redirectToGateway(request, request.url, state, ipfsPathValidator)
371+
return redirectToGateway(request, request.url, state, ipfsPathValidator, runtime)
362372
}
363373

364374
// Detect X-Ipfs-Path Header and upgrade transport to IPFS:
@@ -406,7 +416,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
406416
// redirect only if local node is around
407417
if (newUrl && state.localGwAvailable) {
408418
log(`onHeadersReceived: normalized ${request.url} to ${newUrl}`)
409-
return redirectToGateway(request, newUrl, state, ipfsPathValidator)
419+
return redirectToGateway(request, newUrl, state, ipfsPathValidator, runtime)
410420
}
411421
}
412422
}
@@ -505,10 +515,42 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
505515
exports.redirectOptOutHint = redirectOptOutHint
506516
exports.createRequestModifier = createRequestModifier
507517

508-
function redirectToGateway (request, url, state, ipfsPathValidator) {
518+
// Returns a string with URL at the active gateway (local or public)
519+
function redirectToGateway (request, url, state, ipfsPathValidator, runtime) {
509520
const { resolveToPublicUrl, resolveToLocalUrl } = ipfsPathValidator
510-
const redirectUrl = state.localGwAvailable ? resolveToLocalUrl(url) : resolveToPublicUrl(url)
511-
// redirect only if we actually change anything
521+
let redirectUrl = state.localGwAvailable ? resolveToLocalUrl(url) : resolveToPublicUrl(url)
522+
523+
// SUBRESOURCE ON HTTPS PAGE: THE WORKAROUND EXTRAVAGANZA
524+
// ------------------------------------------------------ \o/
525+
//
526+
// Firefox 74 does not mark *.localhost subdomains as Secure Context yet
527+
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1220810#c23) so we can't
528+
// redirect there when we have IPFS resource embedded on HTTPS page (eg.
529+
// image loaded from a public gateway) because that would cause mixed-content
530+
// warning and subresource would fail to load. Given the fact that
531+
// localhost/ipfs/* provided by go-ipfs 0.5+ returns a redirect to
532+
// *.ipfs.localhost subdomain we need to check requests for subresources, and
533+
// manually replace 'localhost' hostname with '127.0.0.1' (IP is hardcoded as
534+
// Secure Context in Firefox). The need for this workaround can be revisited
535+
// when Firefox closes mentioned bug.
536+
//
537+
// Chromium 80 seems to force HTTPS in the final URL (after all redirects) so
538+
// https://*.localhost fails TODO: needs additional research (could be a bug
539+
// in Chromium). For now we reuse the same workaround as Firefox.
540+
//
541+
if (state.localGwAvailable) {
542+
const { type, originUrl, initiator } = request
543+
// match request types for embedded subdresources, but skip ones coming from local gateway
544+
const parentUrl = originUrl || initiator // FF || Chromium
545+
if (type !== 'main_frame' && (parentUrl && !sameGateway(parentUrl, state.gwURL))) {
546+
// use raw IP to ensure subresource will be loaded from the path gateway
547+
// at 127.0.0.1, which is marked as Secure Context in all browsers
548+
const useLocalhostName = false
549+
redirectUrl = safeURL(redirectUrl, { useLocalhostName }).toString()
550+
}
551+
}
552+
553+
// return a redirect only if URL changed
512554
if (redirectUrl && request.url !== redirectUrl) return { redirectUrl }
513555
}
514556

test/functional/lib/ipfs-path.test.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { stub } = require('sinon')
33
const { describe, it, beforeEach, afterEach } = require('mocha')
44
const { expect } = require('chai')
55
const { URL } = require('url')
6-
const { ipfsContentPath, createIpfsPathValidator } = require('../../../add-on/src/lib/ipfs-path')
6+
const { ipfsContentPath, createIpfsPathValidator, sameGateway } = require('../../../add-on/src/lib/ipfs-path')
77
const { initState } = require('../../../add-on/src/lib/state')
88
const createDnslinkResolver = require('../../../add-on/src/lib/dnslink')
99
const { optionDefaults } = require('../../../add-on/src/lib/options')
@@ -102,6 +102,29 @@ describe('ipfs-path.js', function () {
102102
})
103103
})
104104

105+
describe('sameGateway', function () {
106+
it('should return true on direct host match', function () {
107+
const url = 'https://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
108+
const gw = 'http://127.0.0.1:8080'
109+
expect(sameGateway(url, gw)).to.equal(true)
110+
})
111+
it('should return true on localhost/127.0.0.1 host match', function () {
112+
const url = 'https://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
113+
const gw = 'http://127.0.0.1:8080'
114+
expect(sameGateway(url, gw)).to.equal(true)
115+
})
116+
it('should return true on 127.0.0.1/localhost host match', function () {
117+
const url = 'https://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
118+
const gw = 'http://localhost:8080'
119+
expect(sameGateway(url, gw)).to.equal(true)
120+
})
121+
it('should return false on hostname match but different port', function () {
122+
const url = 'https://localhost:8081/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar'
123+
const gw = 'http://localhost:8080'
124+
expect(sameGateway(url, gw)).to.equal(false)
125+
})
126+
})
127+
105128
describe('validIpfsOrIpns', function () {
106129
// this is just a smoke test, extensive tests are in is-ipfs package
107130
it('should return true for IPFS NURI', function () {

test/functional/lib/ipfs-request-gateway-redirect.test.js

+21-11
Original file line numberDiff line numberDiff line change
@@ -111,30 +111,30 @@ describe('modifyRequest.onBeforeRequest:', function () {
111111
})
112112
})
113113

114-
describe('XHR request for a path matching /ipfs/{CIDv0}', function () {
114+
describe('XHR request for a path matching /ipfs/{CIDv0} coming from 3rd party Origin', function () {
115115
describe('with external node', function () {
116116
beforeEach(function () {
117117
state.ipfsNodeType = 'external'
118118
})
119119
it('should be served from custom gateway if fetched from the same origin and redirect is enabled in Firefox', function () {
120120
runtime.isFirefox = true
121121
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://google.com/' }
122-
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
122+
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
123123
})
124124
it('should be served from custom gateway if fetched from the same origin and redirect is enabled in Chromium', function () {
125125
runtime.isFirefox = false
126126
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://google.com/' }
127-
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
127+
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
128128
})
129129
it('should be served from custom gateway if XHR is cross-origin and redirect is enabled in Chromium', function () {
130130
runtime.isFirefox = false
131131
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
132-
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
132+
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
133133
})
134134
it('should be served from custom gateway if XHR is cross-origin and redirect is enabled in Firefox', function () {
135135
runtime.isFirefox = true
136136
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
137-
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
137+
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
138138
})
139139
})
140140
describe('with embedded node', function () {
@@ -170,17 +170,17 @@ describe('modifyRequest.onBeforeRequest:', function () {
170170
it('should be served from custom gateway if fetched from the same origin and redirect is enabled in Firefox', function () {
171171
runtime.isFirefox = true
172172
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://google.com/' }
173-
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
173+
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
174174
})
175175
it('should be served from custom gateway if fetched from the same origin and redirect is enabled in non-Firefox', function () {
176176
runtime.isFirefox = false
177177
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://google.com/' }
178-
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
178+
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
179179
})
180180
it('should be served from custom gateway if XHR is cross-origin and redirect is enabled in non-Firefox', function () {
181181
runtime.isFirefox = false
182182
const xhrRequest = { url: 'https://google.com/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
183-
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
183+
expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
184184
})
185185
it('should be served from custom gateway via late redirect in onHeadersReceived if XHR is cross-origin and redirect is enabled in Firefox', function () {
186186
// Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
@@ -189,7 +189,7 @@ describe('modifyRequest.onBeforeRequest:', function () {
189189
// onBeforeRequest should not change anything, as it will trigger false-positive CORS error
190190
expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
191191
// onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
192-
expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal('http://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
192+
expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal('http://127.0.0.1:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR?argTest#hashTest')
193193
})
194194
})
195195
})
@@ -255,7 +255,7 @@ describe('modifyRequest.onBeforeRequest:', function () {
255255
})
256256
})
257257

258-
describe('request to a public subdomain gateway (CID in subdomain)', function () {
258+
describe('request to a subdomain gateway', function () {
259259
const cid = 'bafybeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge'
260260
const peerid = 'bafzbeigxjv2o4jse2lajbd5c7xxl5rluhyqg5yupln42252e5tcao7hbge'
261261

@@ -339,7 +339,7 @@ describe('modifyRequest.onBeforeRequest:', function () {
339339
state.redirect = true
340340
})
341341
describe(`with ${nodeType} node:`, function () {
342-
describe('request for IPFS path at a localhost', function () {
342+
describe('request for IPFS path at the localhost', function () {
343343
// we do not touch local requests, as it may interfere with other nodes running at the same machine
344344
// or could produce false-positives such as redirection from localhost:5001/ipfs/path to localhost:8080/ipfs/path
345345
it('should be left untouched if localhost is used', function () {
@@ -362,6 +362,16 @@ describe('modifyRequest.onBeforeRequest:', function () {
362362
const request = url2request('http://[::1]:5001/ipfs/QmPhnvn747LqwPYMJmQVorMaGbMSgA7mRRoyyZYz3DoZRQ/')
363363
expectNoRedirect(modifyRequest, request)
364364
})
365+
it('should be redirected to localhost (subdomain in go-ipfs >0.5) if type=main_frame and 127.0.0.1 (path gw) is used un URL', function () {
366+
state.redirect = true
367+
state.useSubdomainProxy = true
368+
expect(state.gwURL.hostname).to.equal('localhost')
369+
const cid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
370+
const request = url2request(`http://127.0.0.1:8080/ipfs/${cid}?arg=val#hash`)
371+
request.type = 'main_frame' // explicit
372+
expect(modifyRequest.onBeforeRequest(request).redirectUrl)
373+
.to.equal(`http://localhost:8080/ipfs/${cid}?arg=val#hash`)
374+
})
365375
})
366376
})
367377
})

test/functional/lib/ipfs-request-workarounds.test.js

+50
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,56 @@ describe('modifyRequest processing', function () {
3939
modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
4040
})
4141

42+
// Additional handling is required for redirected IPFS subresources on regular HTTPS pages
43+
// (eg. image embedded from public gateway on HTTPS website)
44+
describe('a subresource request on HTTPS website', function () {
45+
const cid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
46+
it('should be routed to "127.0.0.1" gw in Chromium if type is image', function () {
47+
runtime.isFirefox = false
48+
const request = {
49+
method: 'GET',
50+
type: 'image',
51+
url: `https://ipfs.io/ipfs/${cid}`,
52+
initiator: 'https://some-website.example.com' // Chromium
53+
}
54+
expect(modifyRequest.onBeforeRequest(request).redirectUrl)
55+
.to.equal(`http://127.0.0.1:8080/ipfs/${cid}`)
56+
})
57+
it('should be routed to "localhost" gw in Chromium if not a subresource', function () {
58+
runtime.isFirefox = false
59+
const request = {
60+
method: 'GET',
61+
type: 'main_frame',
62+
url: `https://ipfs.io/ipfs/${cid}`,
63+
initiator: 'https://some-website.example.com' // Chromium
64+
}
65+
expect(modifyRequest.onBeforeRequest(request).redirectUrl)
66+
.to.equal(`http://localhost:8080/ipfs/${cid}`)
67+
})
68+
it('should be routed to "127.0.0.1" gw to avoid mixed content warning in Firefox', function () {
69+
runtime.isFirefox = true
70+
const request = {
71+
method: 'GET',
72+
type: 'image',
73+
url: `https://ipfs.io/ipfs/${cid}`,
74+
originUrl: 'https://some-website.example.com/some/page.html' // FF only
75+
}
76+
expect(modifyRequest.onBeforeRequest(request).redirectUrl)
77+
.to.equal(`http://127.0.0.1:8080/ipfs/${cid}`)
78+
})
79+
it('should be routed to "localhost" gw in Firefox if not a subresource', function () {
80+
runtime.isFirefox = true
81+
const request = {
82+
method: 'GET',
83+
type: 'main_frame',
84+
url: `https://ipfs.io/ipfs/${cid}`,
85+
originUrl: 'https://some-website.example.com/some/page.html' // FF only
86+
}
87+
expect(modifyRequest.onBeforeRequest(request).redirectUrl)
88+
.to.equal(`http://localhost:8080/ipfs/${cid}`)
89+
})
90+
})
91+
4292
describe('a request to <apiURL>/api/v0/add with stream-channels=true', function () {
4393
const expectHeader = { name: 'Expect', value: '100-continue' }
4494
it('should apply the "Expect: 100-continue" fix for https://github.com/ipfs/go-ipfs/issues/5168 ', function () {

0 commit comments

Comments
 (0)