Skip to content
This repository has been archived by the owner on Aug 2, 2021. It is now read-only.

Add Content-Disposition http header #945

Conversation

nizsheanez
Copy link

For #527

…sphere#527, ethersphere#944)

Changed swarm/api.Upload:
 - when using WaitGroup no reason to use done counter.
 - f.Close() must be called in Defer - otherwise panic or future added early return will cause leak of file descriptors
 - use common DetectContentType method
 - one error was suppressed
Copy link
Member

@acud acud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution 👍 🍺

Please see my comments in the code.

awg := &sync.WaitGroup{}

for i, entry := range list {
if i >= dcnt+maxParallelFiles {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you have removed important I/O throttling code here. either convert to a worker pattern or preserve as is

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay! I will revert it.

@@ -119,7 +118,7 @@ func upload(ctx *cli.Context) {
}
defer f.Close()
if mimeType == "" {
mimeType = detectMimeType(file)
mimeType = api.DetectContentType(file)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if we really need to open a new file handle inside of api.DetectContentType. at this point we already have f as an open file handle. maybe we could use that instead. WDYT?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found problem:

  • If DetectContentType func will accept *File, then it must current preserve position in file
  • Here we working with *swarm.File not *os.File which has no Seek() method to save and restore position

It doesn't look possible to get content probe from *swarm.File
Maybe you have an idea?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, *swarm.File it's local file, then i can cast it to Seeker

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can also try to change the swarm.File spec to be

Reader,
Closer,
Seeker

instead of just ReadCloser. that might make your life easier. but you also might need to move the type around a bit because this will probably generate a circular dependency when you'll try to reference client from api :)

"github.com/ethereum/go-ethereum/log"
)

// detect content type by file content
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment should follow godoc style:

// DetectContentType by file content with fallback
// to file extension and "application/octet-stream"

// otherwise by file extension
// returns "application/octet-stream" in worst case
func DetectContentType(file string) string {
var contentType = "application/octet-stream" // default value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract to const MIME_OCTET_STREAM = "application/octet-stream", then reuse across the code (DRY)


f, err := os.Open(file)
if err != nil {
log.Warn("detectMimeType: can't open file", "file", file, "err", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log.Error - err != nil
also - you're not returning on error (the rest of the code below will continue executing - resulting in an error). if you choose to have another handle opened here - please return an empty string in this code block - the function has to return here.
as mentioned above - i would remove this part and already use the open handle that is created in the enclosing call

}

func testDetectCTCreateFile(t *testing.T, fileName, content, expectedContentType string) {
path := os.TempDir() + fileName
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use ioutil.TempFile instead

testDetectCTCreateFile(t, "file.css", "Lorem Ipsum", "text/css; charset=utf-8")
}

func testDetectCTCreateFile(t *testing.T, fileName, content, expectedContentType string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

content param not used - can be removed

@@ -201,6 +201,7 @@ func (s *Server) HandleBzzGet(w http.ResponseWriter, r *http.Request) {
defer reader.Close()

w.Header().Set("Content-Type", "application/x-tar")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.tar\"", path.Base(r.URL.Path)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two issues with this:

  1. do not use r.URL as it will contain the entire requested URI e.g. /bzz:/somename.eth/something etc. please use the uri variable above which comes from the middleware that parses the bzz uri. it also has a Path member which you can use to get to the path inside the bzz uri.
  2. there could be a situation where you would be downloading a tarball but the filename might be empty. in that case, you should fallback to the Addr from uri (or untitled)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about case:

  • when i upload 1 file, then download it by hash only (without file name in url)
    Is there known way to get file name? (As i understand - only way is manifest retrieve)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even if its one file - if there's an enclosing manifest, i think there should be a filename there too (need to double check. in any case - there should be - let's assume that)

@@ -862,6 +881,10 @@ func (s *Server) HandleGetFile(w http.ResponseWriter, r *http.Request) {
return
}

if contentType == "" {
contentType = "application/octet-stream"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use const

@@ -891,6 +914,7 @@ func (s *Server) HandleGetFile(w http.ResponseWriter, r *http.Request) {
}

w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(r.URL.Path)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please amend this - use uri from context instead of request URI. also - provide the filename fallback

@acud acud assigned acud and unassigned acud Sep 22, 2018
…eReview (ethersphere#527, ethersphere#944)

Fixes for ethersphere#945

- revert back channel which handle I/O throttling
- add tests on real files
- add tests when file extension doesn't match content
- move code to existing files, no real reason to have dedicated file for such functions
- move http headers validation to swarm/api/http package
- swarm.Open now checking content of file to detect Content Type
@nizsheanez
Copy link
Author

nizsheanez commented Sep 22, 2018

Fixed all comments, but I need carefully revisit code about " I/O throttling" tomorrow.

Found edge case:
if .css file has any content - http.DetectContentType return text/plain
looks like in case of text/plain also need to fallback to extension-based check
Do you feel ok with it? Not sure if other edge cases are around...

@acud
Copy link
Member

acud commented Sep 22, 2018

hmmm this is starting to suck. we should probably provide an override for all web types because browsers need to have them correctly served, or rather, just have it the way you originally implemented (first file extension, then content based, then fallback to octet-streams. sorry for the zig-zag (i thought that css could be parsed even if its content type is set to text/plain)

@nizsheanez
Copy link
Author

I will look around to existing webservers tonight. For example Nginx has mime configuration file, default is: https://github.com/nginx/nginx/blob/master/conf/mime.types

For deeper content check https://github.com/h2non/filetype may help, but it looks overkill for me.

@nizsheanez
Copy link
Author

nizsheanez commented Sep 23, 2018

Here are my thoughts.

About content sniff:

  • (thank Need more support mime-types. ipfs/kubo#2164 ) - Found that http.DetectContentType is already used when http.ServeContent is invoked. It's mean - need to remove Content-Type detection when we serving content, just double-check that we using http.ServeContent. And I will copy-paste code from there and check if it will work well for Uploading (there are extension check firs... i still feel that we can sniff first and fallback when sniff return "text/plain; charset=utf-8" or "application/octet-stream", but too much open questions - why don't fallback on "text/plain; charset=utf-16be"? or is there any other edge cases? - let's go with ext check first, and maybe open another issue for possible attacks research - because now attacker don't need to put crazy content to .css file, he just can provide any valid Content-Type header and we will trust it).
  • Fun question: html - What if client sent us header "X-Content-Type-Options=nosniff"?
  • I saw that swarm/api/testdata has Website serving test, I think need to add non-utf-8 Website to test (but there is a mess :-) .css spec forcing utf-8, while .html and .js allow other encodings :-) )

About extension-based checks:

@acud
Copy link
Member

acud commented Sep 23, 2018

need to remove Content-Type detection when we serving content, just double-check that we using http.ServeContent. And I will copy-paste code from there and check if it will work well for Uploading.

We don't use it. When fetching content from Swarm - we usually use io.Copy to write into the response, not http.ServeContent. See server.go#HandleBzzGet

Fun question: html - What if client sent us header "X-Content-Type-Options=nosniff"?

Ignore? this is a response header.
We should definitely add this header to our responses. But let's first make sure that this works otherwise we could be breaking stuff (the header should be set only if we have a content type set - to fallback when a scenario of no content-type in manifest occurs)

I saw that swarm/api/testdata has Website serving test, I think need to add non-utf-8 Website to test (but there is a mess :-) .css spec forcing utf-u8, while .html and .js allow other encodings :-) )

Definitely, I also think that adding tests for utf8 content (filenames included) is a must. I'll open an issue for that :)

advise to hardcode own list of extension-based ContentTypes

I completely agree. The list from nginx looks pretty exhaustive for this point in time.

…phere#944)

- add hardcoded mime types list
- follow stdlib way of ContentType checking
@nizsheanez
Copy link
Author

Ok. Added. Not solved parts yet:

  • at swarm up README.md client doesn't send file name (even if send, looks like no place to store it... ) - I will investigate tomorrow

@acud
Copy link
Member

acud commented Sep 23, 2018

This is due to another problem and is outside the scope of this issue. I'm already have this issue at mind and it will be taken care of as part of ongoing efforts to refactor and cleanup swarm manifests.

swarm/api/api.go Outdated
// MimeOctetStream default value of http Content-Type header
const MimeOctetStream = "application/octet-stream"

var once sync.Once // guards initMime
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed - use init() instead

swarm/api/api.go Outdated
".avi": "video/x-msvideo",
}

func initMime() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to init()

swarm/api/api.go Outdated

// DetectContentType by file content, or fallback to file extension
func DetectContentType(f *os.File) (string, error) {
once.Do(initMime)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testDetectContentType(t, "file-no-extension", "Lorem Ipsum", "text/plain; charset=utf-8")
testDetectContentType(t, "file-no-extension-no-content", "", "text/plain; charset=utf-8")

// TODO: research, define behavior when content doesn't mach file extension. Now just rely on extension.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove comment

return
}

if err := wait(ctx); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather have this wait() before the mime detection happens, as in the original code

@@ -891,6 +914,7 @@ func (s *Server) HandleGetFile(w http.ResponseWriter, r *http.Request) {
}

w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path.Base(r.URL.Path)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please amend this - use uri from context instead of request URI. also - provide the filename fallback

@@ -942,3 +972,16 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
func isDecryptError(err error) bool {
return strings.Contains(err.Error(), api.ErrDecrypt.Error())
}

func ValidateContentTypeHeader(r *http.Request) (string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lmars what is your input on this?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure what the purpose of this code is?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validating incoming mime type on upload from header, to make sure that the input we're getting is correct before storing it in a manifest - just to make sure we are on par with IETF RFCs

@@ -0,0 +1 @@
package api
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove file

…where possible (ethersphere#527, ethersphere#944)

- simplified I/O throttling by semaphore primitive
- don't use URL from request, use parsed Swarm uri for file name detection
@nizsheanez
Copy link
Author

Fixed all comments.

Question: do i need to delete force of "application/octet-stream" in bzz-resource: https://github.com/ethersphere/go-ethereum/blob/master/swarm/api/http/server.go#L659 ?
Next line http.ServeContent must sniff content well.

@acud
Copy link
Member

acud commented Sep 23, 2018

Great work on this! :)
the code is fine as-is - http.ServeHttp tries to sniff the content just in the case that Content-Type isn't set (L191-L193)

@acud acud removed the in progress label Sep 23, 2018
@nizsheanez
Copy link
Author

Thank you for on-boarding.

swarm/api/api.go Outdated
// MimeOctetStream default value of http Content-Type header
const MimeOctetStream = "application/octet-stream"

// builtinTypesLower stores copy of https://github.com/nginx/nginx/blob/master/conf/mime.types
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally feel this mime.types file should be vendored (or just copied into a Go file) to make it easy to keep in-sync with the upstream file.

I also think it should be clear why this is here (if I saw this I'd probably just think it was unnecessary because the Go mime package does the right thing in pretty much all cases).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By vendoring of mime.types file - you mean write some go generate command to parse it and gen code? Same approach as here https://github.com/bradrydzewski/go-mimetype (it based on mailcap instead of nginx, but there is same mime file) ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like that, yes, which would make it much easier to keep in sync, and more obvious where the types come from.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lmars do you think the overhead is justified in this case? I mean, the mime types base list doesn't fluctuate that much.

swarm/api/api.go Outdated

func init() {
for ext, t := range builtinTypesLower {
mime.AddExtensionType(ext, t)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should panic if there is an error (in case some one adds an extension or mime type that doesn't parse)

swarm/api/api.go Outdated
}
}

// DetectContentType by file content, or fallback to file extension
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is not consistent with what the method actually does.

if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could avoid the need for a temp file by modifying DetectContentType to take (filename string, data io.ReadSeeker) and just use a bytes.Reader in the tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, it will simplify everything


func TestDetectContentType(t *testing.T) {
// internally use http.DetectContentType, so here are test cases only about fallback to file extension check
testDetectContentType(t, "file.css", "body {background-color: orange}", "text/css; charset=utf-8")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use t.Run to isolate each test.

if found := path.Base(uri.Path); found != "" && found != "." && found != "/" {
fileName = found
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure we want to make all files attachments? If I go to bzz://mysite.eth/index.html in my browser, I expect to see the HTML in the browser, not download it to a local file.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, how much you right. Right now i will change it to:
Content-Disposition: inline; filename="example.html"
But i will rise question in Gitter - maybe exist some cases when attachment make sense, for example in S3 owner can add any headers to it's file/folder by UI, and Content-Disposition: attachment too.

@@ -942,3 +972,16 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
func isDecryptError(err error) bool {
return strings.Contains(err.Error(), api.ErrDecrypt.Error())
}

func ValidateContentTypeHeader(r *http.Request) (string, error) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure what the purpose of this code is?

@nizsheanez nizsheanez force-pushed the add-content-disposition-http-header branch from 7c3c4c1 to b632a2b Compare September 24, 2018 02:09
@nizsheanez
Copy link
Author

2 more changes:

…on only since go1.11, a bit of copy-paste (ethersphere#527, ethersphere#944)

- added text/markdown content type
@nizsheanez nizsheanez force-pushed the add-content-disposition-http-header branch from d3e2886 to afb750a Compare September 24, 2018 04:19
@nizsheanez
Copy link
Author

nizsheanez commented Sep 24, 2018

@lmars about I'm not really sure what the purpose of this code is? - I just moved Content-Type header check from 3 places to that function (ValidateContentTypeHeader). Do you feel it unnecessary or need rename to RequireContentTypeHeader?

…ion from attachment to inline (ethersphere#527, ethersphere#944)

- If I go to bzz://mysite.eth/index.html in my browser, I expect to see the HTML in the browser, not download it to a local file.
@nizsheanez nizsheanez force-pushed the add-content-disposition-http-header branch from 08bc172 to 1e87e3d Compare September 24, 2018 08:49
@zelig
Copy link
Member

zelig commented Sep 24, 2018

@nizsheanez @justelad status? shall we move this to main repo?

@acud
Copy link
Member

acud commented Sep 24, 2018

@zelig let's first resolve all issues here then squash+reopen on upstream

@nizsheanez
Copy link
Author

Status: fixed all CR comments except one:

  • From Imars: "I personally feel this mime.types file should be vendored (or just copied into a Go file) to make it easy to keep in-sync with the upstream file." - Waiting for hist reply now.
    I have airplane tomorrow, will push something about it - after tomorrow.

t.Fatalf("Content-Type header expected: application/x-tar, got: %s", h)
}

// See header to suggest file name for Browsers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove comment

if found := path.Base(uri.Path); found != "" && found != "." && found != "/" {
fileName = found
}
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s.tar\"", fileName))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am wondering - maybe in this case we should actually send an attachment header. @lmars?

if found := path.Base(uri.Path); found != "" && found != "." && found != "/" {
fileName = found
}
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lmars I think this should be:
if contentType == 'application/*' - return attachment
else return inline.
WDYT?

Copy link
Author

@nizsheanez nizsheanez Sep 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worry about "application/pdf" and "application/rss+xml", I'm personally fine if .pdf opening in browser (re-checked, browsers do download .pdf file with Content-Disposition: attachment; file="name.pdf" header, and only Safari on my Mac auto-open it in Preview).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've seen the rss issue too but i think its a non-issue as its a machine readable format and is not supposed to be read by the user as-is (AFAIK)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my MacBook:

With inline:

  • Firefox: has Built-in RSS reader. it displays RSS in readable way and has "subscribe" button.
  • Safari: forward link to installed RSS Reader app.

With attachment:

  • Firefox: download file
  • Safari: forward link to installed RSS Reader app

Looks like we breaking some UX here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok - keep it as it is for now (inline it)

@nizsheanez
Copy link
Author

Added generator based on mime.types file as lmars recommended.

// mime.types file provided by mailcap, which follow https://www.iana.org/assignments/media-types/media-types.xhtml
//
// Get last version of mime.types file by:
// docker run --rm -v $(pwd):/tmp alpine:edge /bin/sh -c "apk add -U mailcap; mv /etc/mime.types /tmp"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now seems to do exactly what the package you linked to does (i.e. https://github.com/bradrydzewski/go-mimetype), should we not just use that package?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That package looks unmaintained, but let me try to rise PR there, here is list of differences:

  • go-mimetype doesn't fail if mime.AddExtensionType returns error
  • outdated mime.types file

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I don't mean for you to go back and forth on this. I assumed you didn't want to use that package because you wanted to use nginx mime types, but if we want to use the same method as that package then we should try and PR upstream (the maintainer looks active on other projects)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, just that mime.types file easier to parse. File from nginx has nginx.config format you can see there are some bracers: https://github.com/nginx/nginx/blob/master/conf/mime.types

No problem, already created PR: bradrydzewski/go-mimetype#1

Anyway we still discussing about Content-Disposition

@nizsheanez nizsheanez force-pushed the add-content-disposition-http-header branch from e89f0ef to 5c00804 Compare September 26, 2018 11:15
@lmars
Copy link

lmars commented Sep 26, 2018

@nizsheanez @justelad it seems this PR is covering two things:

  • Improved detection and validation of Content-Type
  • Add Content-Disposition

It seems we've got to a good solution for improving Content-Type, but I don't think it's clear what problem we are trying to solve with the Content-Disposition header.

What is driving the decision to add this Content-Disposition header? Is it a user-requested feature? If it has been requested, I personally think we should expose some form of option to the user to allow them to set Content-Disposition if they want, but I don't think Swarm should be trying to preempt how the user wants their content to be served (either inline or as an attachment).

@acud
Copy link
Member

acud commented Sep 26, 2018

@lmars this solves the problem that the browser does not always know under which filename to save swarm content and this is rather annoying.
Content-Disposition solves this.
No user requested this. I noticed this problem and opened the ticket on my own accord.

@nizsheanez
Copy link
Author

I agree that Content-Disposition: attachment must be configurable by user - Because we discovered too much edge cases already (maybe even on MRU level instead of file, but i'm not sure).

Adding Content-Disposition: inline - solved browser does not always know under which filename to save swarm content problem (with one exception: when client uploading single file - it doesn't send to server fileName. Justelad, says that it's out of scope and he will fix it later: #945 (comment) )

@justelad , now I need to do squash and open new PR with good description?

@acud
Copy link
Member

acud commented Sep 28, 2018

@nizsheanez many thanks for this comprehensive PR!

Please squash all commits and reopen this PR against ethereum/master with a description.
:shipit:

@nizsheanez
Copy link
Author

Done ethereum/go-ethereum#17782

@acud acud closed this Sep 29, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants