diff options
author | Rafael Matias <rafael@skyle.net> | 2019-06-04 21:35:36 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2019-06-04 21:35:36 +0800 |
commit | 42b81f94adba9aae7f7727951f35e92184b1eedb (patch) | |
tree | 73f3004ba6cd922bc54579fe886a7b3cb1df1741 /cmd | |
parent | 15f24ff1896835e6ab908b0d17c1cc36b300c408 (diff) | |
download | go-tangerine-42b81f94adba9aae7f7727951f35e92184b1eedb.tar go-tangerine-42b81f94adba9aae7f7727951f35e92184b1eedb.tar.gz go-tangerine-42b81f94adba9aae7f7727951f35e92184b1eedb.tar.bz2 go-tangerine-42b81f94adba9aae7f7727951f35e92184b1eedb.tar.lz go-tangerine-42b81f94adba9aae7f7727951f35e92184b1eedb.tar.xz go-tangerine-42b81f94adba9aae7f7727951f35e92184b1eedb.tar.zst go-tangerine-42b81f94adba9aae7f7727951f35e92184b1eedb.zip |
swarm: code cleanup, move to ethersphere/swarm (#19661)
Diffstat (limited to 'cmd')
41 files changed, 0 insertions, 12276 deletions
diff --git a/cmd/swarm/access.go b/cmd/swarm/access.go deleted file mode 100644 index cc0cc8201..000000000 --- a/cmd/swarm/access.go +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. -package main - -import ( - "crypto/rand" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "strings" - - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/swarm/api" - "github.com/ethereum/go-ethereum/swarm/api/client" - "gopkg.in/urfave/cli.v1" -) - -var ( - salt = make([]byte, 32) - accessCommand = cli.Command{ - CustomHelpTemplate: helpTemplate, - Name: "access", - Usage: "encrypts a reference and embeds it into a root manifest", - ArgsUsage: "<ref>", - Description: "encrypts a reference and embeds it into a root manifest", - Subcommands: []cli.Command{ - { - CustomHelpTemplate: helpTemplate, - Name: "new", - Usage: "encrypts a reference and embeds it into a root manifest", - ArgsUsage: "<ref>", - Description: "encrypts a reference and embeds it into a root access manifest and prints the resulting manifest", - Subcommands: []cli.Command{ - { - Action: accessNewPass, - CustomHelpTemplate: helpTemplate, - Flags: []cli.Flag{ - utils.PasswordFileFlag, - SwarmDryRunFlag, - }, - Name: "pass", - Usage: "encrypts a reference with a password and embeds it into a root manifest", - ArgsUsage: "<ref>", - Description: "encrypts a reference and embeds it into a root access manifest and prints the resulting manifest", - }, - { - Action: accessNewPK, - CustomHelpTemplate: helpTemplate, - Flags: []cli.Flag{ - utils.PasswordFileFlag, - SwarmDryRunFlag, - SwarmAccessGrantKeyFlag, - }, - Name: "pk", - Usage: "encrypts a reference with the node's private key and a given grantee's public key and embeds it into a root manifest", - ArgsUsage: "<ref>", - Description: "encrypts a reference and embeds it into a root access manifest and prints the resulting manifest", - }, - { - Action: accessNewACT, - CustomHelpTemplate: helpTemplate, - Flags: []cli.Flag{ - SwarmAccessGrantKeysFlag, - SwarmDryRunFlag, - utils.PasswordFileFlag, - }, - Name: "act", - Usage: "encrypts a reference with the node's private key and a given grantee's public key and embeds it into a root manifest", - ArgsUsage: "<ref>", - Description: "encrypts a reference and embeds it into a root access manifest and prints the resulting manifest", - }, - }, - }, - }, - } -) - -func init() { - if _, err := io.ReadFull(rand.Reader, salt); err != nil { - panic("reading from crypto/rand failed: " + err.Error()) - } -} - -func accessNewPass(ctx *cli.Context) { - args := ctx.Args() - if len(args) != 1 { - utils.Fatalf("Expected 1 argument - the ref") - } - - var ( - ae *api.AccessEntry - accessKey []byte - err error - ref = args[0] - password = getPassPhrase("", 0, makePasswordList(ctx)) - dryRun = ctx.Bool(SwarmDryRunFlag.Name) - ) - accessKey, ae, err = api.DoPassword(ctx, password, salt) - if err != nil { - utils.Fatalf("error getting session key: %v", err) - } - m, err := api.GenerateAccessControlManifest(ctx, ref, accessKey, ae) - if err != nil { - utils.Fatalf("had an error generating the manifest: %v", err) - } - if dryRun { - err = printManifests(m, nil) - if err != nil { - utils.Fatalf("had an error printing the manifests: %v", err) - } - } else { - err = uploadManifests(ctx, m, nil) - if err != nil { - utils.Fatalf("had an error uploading the manifests: %v", err) - } - } -} - -func accessNewPK(ctx *cli.Context) { - args := ctx.Args() - if len(args) != 1 { - utils.Fatalf("Expected 1 argument - the ref") - } - - var ( - ae *api.AccessEntry - sessionKey []byte - err error - ref = args[0] - privateKey = getPrivKey(ctx) - granteePublicKey = ctx.String(SwarmAccessGrantKeyFlag.Name) - dryRun = ctx.Bool(SwarmDryRunFlag.Name) - ) - sessionKey, ae, err = api.DoPK(ctx, privateKey, granteePublicKey, salt) - if err != nil { - utils.Fatalf("error getting session key: %v", err) - } - m, err := api.GenerateAccessControlManifest(ctx, ref, sessionKey, ae) - if err != nil { - utils.Fatalf("had an error generating the manifest: %v", err) - } - if dryRun { - err = printManifests(m, nil) - if err != nil { - utils.Fatalf("had an error printing the manifests: %v", err) - } - } else { - err = uploadManifests(ctx, m, nil) - if err != nil { - utils.Fatalf("had an error uploading the manifests: %v", err) - } - } -} - -func accessNewACT(ctx *cli.Context) { - args := ctx.Args() - if len(args) != 1 { - utils.Fatalf("Expected 1 argument - the ref") - } - - var ( - ae *api.AccessEntry - actManifest *api.Manifest - accessKey []byte - err error - ref = args[0] - pkGrantees []string - passGrantees []string - pkGranteesFilename = ctx.String(SwarmAccessGrantKeysFlag.Name) - passGranteesFilename = ctx.String(utils.PasswordFileFlag.Name) - privateKey = getPrivKey(ctx) - dryRun = ctx.Bool(SwarmDryRunFlag.Name) - ) - if pkGranteesFilename == "" && passGranteesFilename == "" { - utils.Fatalf("you have to provide either a grantee public-keys file or an encryption passwords file (or both)") - } - - if pkGranteesFilename != "" { - bytes, err := ioutil.ReadFile(pkGranteesFilename) - if err != nil { - utils.Fatalf("had an error reading the grantee public key list") - } - pkGrantees = strings.Split(strings.Trim(string(bytes), "\n"), "\n") - } - - if passGranteesFilename != "" { - bytes, err := ioutil.ReadFile(passGranteesFilename) - if err != nil { - utils.Fatalf("could not read password filename: %v", err) - } - passGrantees = strings.Split(strings.Trim(string(bytes), "\n"), "\n") - } - accessKey, ae, actManifest, err = api.DoACT(ctx, privateKey, salt, pkGrantees, passGrantees) - if err != nil { - utils.Fatalf("error generating ACT manifest: %v", err) - } - - if err != nil { - utils.Fatalf("error getting session key: %v", err) - } - m, err := api.GenerateAccessControlManifest(ctx, ref, accessKey, ae) - if err != nil { - utils.Fatalf("error generating root access manifest: %v", err) - } - - if dryRun { - err = printManifests(m, actManifest) - if err != nil { - utils.Fatalf("had an error printing the manifests: %v", err) - } - } else { - err = uploadManifests(ctx, m, actManifest) - if err != nil { - utils.Fatalf("had an error uploading the manifests: %v", err) - } - } -} - -func printManifests(rootAccessManifest, actManifest *api.Manifest) error { - js, err := json.Marshal(rootAccessManifest) - if err != nil { - return err - } - fmt.Println(string(js)) - - if actManifest != nil { - js, err := json.Marshal(actManifest) - if err != nil { - return err - } - fmt.Println(string(js)) - } - return nil -} - -func uploadManifests(ctx *cli.Context, rootAccessManifest, actManifest *api.Manifest) error { - bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") - client := client.NewClient(bzzapi) - - var ( - key string - err error - ) - if actManifest != nil { - key, err = client.UploadManifest(actManifest, false) - if err != nil { - return err - } - - rootAccessManifest.Entries[0].Access.Act = key - } - key, err = client.UploadManifest(rootAccessManifest, false) - if err != nil { - return err - } - fmt.Println(key) - return nil -} - -// makePasswordList reads password lines from the file specified by the global --password flag -// and also by the same subcommand --password flag. -// This function ia a fork of utils.MakePasswordList to lookup cli context for subcommand. -// Function ctx.SetGlobal is not setting the global flag value that can be accessed -// by ctx.GlobalString using the current version of cli package. -func makePasswordList(ctx *cli.Context) []string { - path := ctx.GlobalString(utils.PasswordFileFlag.Name) - if path == "" { - path = ctx.String(utils.PasswordFileFlag.Name) - if path == "" { - return nil - } - } - text, err := ioutil.ReadFile(path) - if err != nil { - utils.Fatalf("Failed to read password file: %v", err) - } - lines := strings.Split(string(text), "\n") - // Sanitise DOS line endings. - for i := range lines { - lines[i] = strings.TrimRight(lines[i], "\r") - } - return lines -} diff --git a/cmd/swarm/access_test.go b/cmd/swarm/access_test.go deleted file mode 100644 index 0aaaad030..000000000 --- a/cmd/swarm/access_test.go +++ /dev/null @@ -1,617 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "bytes" - "crypto/rand" - "encoding/hex" - "encoding/json" - "io" - "io/ioutil" - gorand "math/rand" - "net/http" - "os" - "runtime" - "strings" - "testing" - "time" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/swarm/api" - swarmapi "github.com/ethereum/go-ethereum/swarm/api/client" - "github.com/ethereum/go-ethereum/swarm/testutil" - "golang.org/x/crypto/sha3" -) - -const ( - hashRegexp = `[a-f\d]{128}` - data = "notsorandomdata" -) - -var DefaultCurve = crypto.S256() - -func TestACT(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip() - } - - cluster := newTestCluster(t, clusterSize) - defer cluster.Shutdown() - - cases := []struct { - name string - f func(t *testing.T, cluster *testCluster) - }{ - {"Password", testPassword}, - {"PK", testPK}, - {"ACTWithoutBogus", testACTWithoutBogus}, - {"ACTWithBogus", testACTWithBogus}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - tc.f(t, cluster) - }) - } -} - -// testPassword tests for the correct creation of an ACT manifest protected by a password. -// The test creates bogus content, uploads it encrypted, then creates the wrapping manifest with the Access entry -// The parties participating - node (publisher), uploads to second node then disappears. Content which was uploaded -// is then fetched through 2nd node. since the tested code is not key-aware - we can just -// fetch from the 2nd node using HTTP BasicAuth -func testPassword(t *testing.T, cluster *testCluster) { - dataFilename := testutil.TempFileWithContent(t, data) - defer os.RemoveAll(dataFilename) - - // upload the file with 'swarm up' and expect a hash - up := runSwarm(t, - "--bzzapi", - cluster.Nodes[0].URL, - "up", - "--encrypt", - dataFilename) - _, matches := up.ExpectRegexp(hashRegexp) - up.ExpectExit() - - if len(matches) < 1 { - t.Fatal("no matches found") - } - - ref := matches[0] - tmp, err := ioutil.TempDir("", "swarm-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmp) - password := "smth" - passwordFilename := testutil.TempFileWithContent(t, "smth") - defer os.RemoveAll(passwordFilename) - - up = runSwarm(t, - "access", - "new", - "pass", - "--dry-run", - "--password", - passwordFilename, - ref, - ) - - _, matches = up.ExpectRegexp(".+") - up.ExpectExit() - - if len(matches) == 0 { - t.Fatalf("stdout not matched") - } - - var m api.Manifest - - err = json.Unmarshal([]byte(matches[0]), &m) - if err != nil { - t.Fatalf("unmarshal manifest: %v", err) - } - - if len(m.Entries) != 1 { - t.Fatalf("expected one manifest entry, got %v", len(m.Entries)) - } - - e := m.Entries[0] - - ct := "application/bzz-manifest+json" - if e.ContentType != ct { - t.Errorf("expected %q content type, got %q", ct, e.ContentType) - } - - if e.Access == nil { - t.Fatal("manifest access is nil") - } - - a := e.Access - - if a.Type != "pass" { - t.Errorf(`got access type %q, expected "pass"`, a.Type) - } - if len(a.Salt) < 32 { - t.Errorf(`got salt with length %v, expected not less the 32 bytes`, len(a.Salt)) - } - if a.KdfParams == nil { - t.Fatal("manifest access kdf params is nil") - } - if a.Publisher != "" { - t.Fatal("should be empty") - } - - client := swarmapi.NewClient(cluster.Nodes[0].URL) - - hash, err := client.UploadManifest(&m, false) - if err != nil { - t.Fatal(err) - } - - url := cluster.Nodes[0].URL + "/" + "bzz:/" + hash - - httpClient := &http.Client{} - response, err := httpClient.Get(url) - if err != nil { - t.Fatal(err) - } - if response.StatusCode != http.StatusUnauthorized { - t.Fatal("should be a 401") - } - authHeader := response.Header.Get("WWW-Authenticate") - if authHeader == "" { - t.Fatal("should be something here") - } - - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - t.Fatal(err) - } - req.SetBasicAuth("", password) - - response, err = http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - defer response.Body.Close() - - if response.StatusCode != http.StatusOK { - t.Errorf("expected status %v, got %v", http.StatusOK, response.StatusCode) - } - d, err := ioutil.ReadAll(response.Body) - if err != nil { - t.Fatal(err) - } - if string(d) != data { - t.Errorf("expected decrypted data %q, got %q", data, string(d)) - } - - wrongPasswordFilename := testutil.TempFileWithContent(t, "just wr0ng") - defer os.RemoveAll(wrongPasswordFilename) - - //download file with 'swarm down' with wrong password - up = runSwarm(t, - "--bzzapi", - cluster.Nodes[0].URL, - "down", - "bzz:/"+hash, - tmp, - "--password", - wrongPasswordFilename) - - _, matches = up.ExpectRegexp("unauthorized") - if len(matches) != 1 && matches[0] != "unauthorized" { - t.Fatal(`"unauthorized" not found in output"`) - } - up.ExpectExit() -} - -// testPK tests for the correct creation of an ACT manifest between two parties (publisher and grantee). -// The test creates bogus content, uploads it encrypted, then creates the wrapping manifest with the Access entry -// The parties participating - node (publisher), uploads to second node (which is also the grantee) then disappears. -// Content which was uploaded is then fetched through the grantee's http proxy. Since the tested code is private-key aware, -// the test will fail if the proxy's given private key is not granted on the ACT. -func testPK(t *testing.T, cluster *testCluster) { - dataFilename := testutil.TempFileWithContent(t, data) - defer os.RemoveAll(dataFilename) - - // upload the file with 'swarm up' and expect a hash - up := runSwarm(t, - "--bzzapi", - cluster.Nodes[0].URL, - "up", - "--encrypt", - dataFilename) - _, matches := up.ExpectRegexp(hashRegexp) - up.ExpectExit() - - if len(matches) < 1 { - t.Fatal("no matches found") - } - - ref := matches[0] - pk := cluster.Nodes[0].PrivateKey - granteePubKey := crypto.CompressPubkey(&pk.PublicKey) - - publisherDir, err := ioutil.TempDir("", "swarm-account-dir-temp") - if err != nil { - t.Fatal(err) - } - - passwordFilename := testutil.TempFileWithContent(t, testPassphrase) - defer os.RemoveAll(passwordFilename) - - _, publisherAccount := getTestAccount(t, publisherDir) - up = runSwarm(t, - "--bzzaccount", - publisherAccount.Address.String(), - "--password", - passwordFilename, - "--datadir", - publisherDir, - "--bzzapi", - cluster.Nodes[0].URL, - "access", - "new", - "pk", - "--dry-run", - "--grant-key", - hex.EncodeToString(granteePubKey), - ref, - ) - - _, matches = up.ExpectRegexp(".+") - up.ExpectExit() - - if len(matches) == 0 { - t.Fatalf("stdout not matched") - } - - //get the public key from the publisher directory - publicKeyFromDataDir := runSwarm(t, - "--bzzaccount", - publisherAccount.Address.String(), - "--password", - passwordFilename, - "--datadir", - publisherDir, - "print-keys", - "--compressed", - ) - _, publicKeyString := publicKeyFromDataDir.ExpectRegexp(".+") - publicKeyFromDataDir.ExpectExit() - pkComp := strings.Split(publicKeyString[0], "=")[1] - var m api.Manifest - - err = json.Unmarshal([]byte(matches[0]), &m) - if err != nil { - t.Fatalf("unmarshal manifest: %v", err) - } - - if len(m.Entries) != 1 { - t.Fatalf("expected one manifest entry, got %v", len(m.Entries)) - } - - e := m.Entries[0] - - ct := "application/bzz-manifest+json" - if e.ContentType != ct { - t.Errorf("expected %q content type, got %q", ct, e.ContentType) - } - - if e.Access == nil { - t.Fatal("manifest access is nil") - } - - a := e.Access - - if a.Type != "pk" { - t.Errorf(`got access type %q, expected "pk"`, a.Type) - } - if len(a.Salt) < 32 { - t.Errorf(`got salt with length %v, expected not less the 32 bytes`, len(a.Salt)) - } - if a.KdfParams != nil { - t.Fatal("manifest access kdf params should be nil") - } - if a.Publisher != pkComp { - t.Fatal("publisher key did not match") - } - client := swarmapi.NewClient(cluster.Nodes[0].URL) - - hash, err := client.UploadManifest(&m, false) - if err != nil { - t.Fatal(err) - } - - httpClient := &http.Client{} - - url := cluster.Nodes[0].URL + "/" + "bzz:/" + hash - response, err := httpClient.Get(url) - if err != nil { - t.Fatal(err) - } - if response.StatusCode != http.StatusOK { - t.Fatal("should be a 200") - } - d, err := ioutil.ReadAll(response.Body) - if err != nil { - t.Fatal(err) - } - if string(d) != data { - t.Errorf("expected decrypted data %q, got %q", data, string(d)) - } -} - -// testACTWithoutBogus tests the creation of the ACT manifest end-to-end, without any bogus entries (i.e. default scenario = 3 nodes 1 unauthorized) -func testACTWithoutBogus(t *testing.T, cluster *testCluster) { - testACT(t, cluster, 0) -} - -// testACTWithBogus tests the creation of the ACT manifest end-to-end, with 100 bogus entries (i.e. 100 EC keys + default scenario = 3 nodes 1 unauthorized = 103 keys in the ACT manifest) -func testACTWithBogus(t *testing.T, cluster *testCluster) { - testACT(t, cluster, 100) -} - -// testACT tests the e2e creation, uploading and downloading of an ACT access control with both EC keys AND password protection -// the test fires up a 3 node cluster, then randomly picks 2 nodes which will be acting as grantees to the data -// set and also protects the ACT with a password. the third node should fail decoding the reference as it will not be granted access. -// the third node then then tries to download using a correct password (and succeeds) then uses a wrong password and fails. -// the publisher uploads through one of the nodes then disappears. -func testACT(t *testing.T, cluster *testCluster, bogusEntries int) { - var uploadThroughNode = cluster.Nodes[0] - client := swarmapi.NewClient(uploadThroughNode.URL) - - r1 := gorand.New(gorand.NewSource(time.Now().UnixNano())) - nodeToSkip := r1.Intn(clusterSize) // a number between 0 and 2 (node indices in `cluster`) - dataFilename := testutil.TempFileWithContent(t, data) - defer os.RemoveAll(dataFilename) - - // upload the file with 'swarm up' and expect a hash - up := runSwarm(t, - "--bzzapi", - cluster.Nodes[0].URL, - "up", - "--encrypt", - dataFilename) - _, matches := up.ExpectRegexp(hashRegexp) - up.ExpectExit() - - if len(matches) < 1 { - t.Fatal("no matches found") - } - - ref := matches[0] - var grantees []string - for i, v := range cluster.Nodes { - if i == nodeToSkip { - continue - } - pk := v.PrivateKey - granteePubKey := crypto.CompressPubkey(&pk.PublicKey) - grantees = append(grantees, hex.EncodeToString(granteePubKey)) - } - - if bogusEntries > 0 { - var bogusGrantees []string - - for i := 0; i < bogusEntries; i++ { - prv, err := ecies.GenerateKey(rand.Reader, DefaultCurve, nil) - if err != nil { - t.Fatal(err) - } - bogusGrantees = append(bogusGrantees, hex.EncodeToString(crypto.CompressPubkey(&prv.ExportECDSA().PublicKey))) - } - r2 := gorand.New(gorand.NewSource(time.Now().UnixNano())) - for i := 0; i < len(grantees); i++ { - insertAtIdx := r2.Intn(len(bogusGrantees)) - bogusGrantees = append(bogusGrantees[:insertAtIdx], append([]string{grantees[i]}, bogusGrantees[insertAtIdx:]...)...) - } - grantees = bogusGrantees - } - granteesPubkeyListFile := testutil.TempFileWithContent(t, strings.Join(grantees, "\n")) - defer os.RemoveAll(granteesPubkeyListFile) - - publisherDir, err := ioutil.TempDir("", "swarm-account-dir-temp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(publisherDir) - - passwordFilename := testutil.TempFileWithContent(t, testPassphrase) - defer os.RemoveAll(passwordFilename) - actPasswordFilename := testutil.TempFileWithContent(t, "smth") - defer os.RemoveAll(actPasswordFilename) - _, publisherAccount := getTestAccount(t, publisherDir) - up = runSwarm(t, - "--bzzaccount", - publisherAccount.Address.String(), - "--password", - passwordFilename, - "--datadir", - publisherDir, - "--bzzapi", - cluster.Nodes[0].URL, - "access", - "new", - "act", - "--grant-keys", - granteesPubkeyListFile, - "--password", - actPasswordFilename, - ref, - ) - - _, matches = up.ExpectRegexp(`[a-f\d]{64}`) - up.ExpectExit() - - if len(matches) == 0 { - t.Fatalf("stdout not matched") - } - - //get the public key from the publisher directory - publicKeyFromDataDir := runSwarm(t, - "--bzzaccount", - publisherAccount.Address.String(), - "--password", - passwordFilename, - "--datadir", - publisherDir, - "print-keys", - "--compressed", - ) - _, publicKeyString := publicKeyFromDataDir.ExpectRegexp(".+") - publicKeyFromDataDir.ExpectExit() - pkComp := strings.Split(publicKeyString[0], "=")[1] - - hash := matches[0] - m, _, err := client.DownloadManifest(hash) - if err != nil { - t.Fatalf("unmarshal manifest: %v", err) - } - - if len(m.Entries) != 1 { - t.Fatalf("expected one manifest entry, got %v", len(m.Entries)) - } - - e := m.Entries[0] - - ct := "application/bzz-manifest+json" - if e.ContentType != ct { - t.Errorf("expected %q content type, got %q", ct, e.ContentType) - } - - if e.Access == nil { - t.Fatal("manifest access is nil") - } - - a := e.Access - - if a.Type != "act" { - t.Fatalf(`got access type %q, expected "act"`, a.Type) - } - if len(a.Salt) < 32 { - t.Fatalf(`got salt with length %v, expected not less the 32 bytes`, len(a.Salt)) - } - - if a.Publisher != pkComp { - t.Fatal("publisher key did not match") - } - httpClient := &http.Client{} - - // all nodes except the skipped node should be able to decrypt the content - for i, node := range cluster.Nodes { - log.Debug("trying to fetch from node", "node index", i) - - url := node.URL + "/" + "bzz:/" + hash - response, err := httpClient.Get(url) - if err != nil { - t.Fatal(err) - } - log.Debug("got response from node", "response code", response.StatusCode) - - if i == nodeToSkip { - log.Debug("reached node to skip", "status code", response.StatusCode) - - if response.StatusCode != http.StatusUnauthorized { - t.Fatalf("should be a 401") - } - - // try downloading using a password instead, using the unauthorized node - passwordUrl := strings.Replace(url, "http://", "http://:smth@", -1) - response, err = httpClient.Get(passwordUrl) - if err != nil { - t.Fatal(err) - } - if response.StatusCode != http.StatusOK { - t.Fatal("should be a 200") - } - - // now try with the wrong password, expect 401 - passwordUrl = strings.Replace(url, "http://", "http://:smthWrong@", -1) - response, err = httpClient.Get(passwordUrl) - if err != nil { - t.Fatal(err) - } - if response.StatusCode != http.StatusUnauthorized { - t.Fatal("should be a 401") - } - continue - } - - if response.StatusCode != http.StatusOK { - t.Fatal("should be a 200") - } - d, err := ioutil.ReadAll(response.Body) - if err != nil { - t.Fatal(err) - } - if string(d) != data { - t.Errorf("expected decrypted data %q, got %q", data, string(d)) - } - } -} - -// TestKeypairSanity is a sanity test for the crypto scheme for ACT. it asserts the correct shared secret according to -// the specs at https://github.com/ethersphere/swarm-docs/blob/eb857afda906c6e7bb90d37f3f334ccce5eef230/act.md -func TestKeypairSanity(t *testing.T) { - salt := make([]byte, 32) - if _, err := io.ReadFull(rand.Reader, salt); err != nil { - t.Fatalf("reading from crypto/rand failed: %v", err.Error()) - } - sharedSecret := "a85586744a1ddd56a7ed9f33fa24f40dd745b3a941be296a0d60e329dbdb896d" - - for i, v := range []struct { - publisherPriv string - granteePub string - }{ - { - publisherPriv: "ec5541555f3bc6376788425e9d1a62f55a82901683fd7062c5eddcc373a73459", - granteePub: "0226f213613e843a413ad35b40f193910d26eb35f00154afcde9ded57479a6224a", - }, - { - publisherPriv: "70c7a73011aa56584a0009ab874794ee7e5652fd0c6911cd02f8b6267dd82d2d", - granteePub: "02e6f8d5e28faaa899744972bb847b6eb805a160494690c9ee7197ae9f619181db", - }, - } { - b, _ := hex.DecodeString(v.granteePub) - granteePub, _ := crypto.DecompressPubkey(b) - publisherPrivate, _ := crypto.HexToECDSA(v.publisherPriv) - - ssKey, err := api.NewSessionKeyPK(publisherPrivate, granteePub, salt) - if err != nil { - t.Fatal(err) - } - - hasher := sha3.NewLegacyKeccak256() - hasher.Write(salt) - shared, err := hex.DecodeString(sharedSecret) - if err != nil { - t.Fatal(err) - } - hasher.Write(shared) - sum := hasher.Sum(nil) - - if !bytes.Equal(ssKey, sum) { - t.Fatalf("%d: got a session key mismatch", i) - } - } -} diff --git a/cmd/swarm/bootnodes.go b/cmd/swarm/bootnodes.go deleted file mode 100644 index ce3cd5288..000000000 --- a/cmd/swarm/bootnodes.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -var SwarmBootnodes = []string{ - // EF Swarm Bootnode - AWS - eu-central-1 - "enode://4c113504601930bf2000c29bcd98d1716b6167749f58bad703bae338332fe93cc9d9204f08afb44100dc7bea479205f5d162df579f9a8f76f8b402d339709023@3.122.203.99:30301", - // EF Swarm Bootnode - AWS - us-west-2 - "enode://89f2ede3371bff1ad9f2088f2012984e280287a4e2b68007c2a6ad994909c51886b4a8e9e2ecc97f9910aca538398e0a5804b0ee80a187fde1ba4f32626322ba@52.35.212.179:30301", -} diff --git a/cmd/swarm/config.go b/cmd/swarm/config.go deleted file mode 100644 index e4b333549..000000000 --- a/cmd/swarm/config.go +++ /dev/null @@ -1,451 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "errors" - "fmt" - "io" - "os" - "reflect" - "strconv" - "strings" - "time" - "unicode" - - cli "gopkg.in/urfave/cli.v1" - - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/naoina/toml" - - bzzapi "github.com/ethereum/go-ethereum/swarm/api" -) - -var ( - //flag definition for the dumpconfig command - DumpConfigCommand = cli.Command{ - Action: utils.MigrateFlags(dumpConfig), - Name: "dumpconfig", - Usage: "Show configuration values", - ArgsUsage: "", - Flags: app.Flags, - Category: "MISCELLANEOUS COMMANDS", - Description: `The dumpconfig command shows configuration values.`, - } - - //flag definition for the config file command - SwarmTomlConfigPathFlag = cli.StringFlag{ - Name: "config", - Usage: "TOML configuration file", - } -) - -//constants for environment variables -const ( - SwarmEnvChequebookAddr = "SWARM_CHEQUEBOOK_ADDR" - SwarmEnvAccount = "SWARM_ACCOUNT" - SwarmEnvListenAddr = "SWARM_LISTEN_ADDR" - SwarmEnvPort = "SWARM_PORT" - SwarmEnvNetworkID = "SWARM_NETWORK_ID" - SwarmEnvSwapEnable = "SWARM_SWAP_ENABLE" - SwarmEnvSwapAPI = "SWARM_SWAP_API" - SwarmEnvSyncDisable = "SWARM_SYNC_DISABLE" - SwarmEnvSyncUpdateDelay = "SWARM_ENV_SYNC_UPDATE_DELAY" - SwarmEnvMaxStreamPeerServers = "SWARM_ENV_MAX_STREAM_PEER_SERVERS" - SwarmEnvLightNodeEnable = "SWARM_LIGHT_NODE_ENABLE" - SwarmEnvDeliverySkipCheck = "SWARM_DELIVERY_SKIP_CHECK" - SwarmEnvENSAPI = "SWARM_ENS_API" - SwarmEnvENSAddr = "SWARM_ENS_ADDR" - SwarmEnvCORS = "SWARM_CORS" - SwarmEnvBootnodes = "SWARM_BOOTNODES" - SwarmEnvPSSEnable = "SWARM_PSS_ENABLE" - SwarmEnvStorePath = "SWARM_STORE_PATH" - SwarmEnvStoreCapacity = "SWARM_STORE_CAPACITY" - SwarmEnvStoreCacheCapacity = "SWARM_STORE_CACHE_CAPACITY" - SwarmEnvBootnodeMode = "SWARM_BOOTNODE_MODE" - SwarmAccessPassword = "SWARM_ACCESS_PASSWORD" - SwarmAutoDefaultPath = "SWARM_AUTO_DEFAULTPATH" - SwarmGlobalstoreAPI = "SWARM_GLOBALSTORE_API" - GethEnvDataDir = "GETH_DATADIR" -) - -// These settings ensure that TOML keys use the same names as Go struct fields. -var tomlSettings = toml.Config{ - NormFieldName: func(rt reflect.Type, key string) string { - return key - }, - FieldToKey: func(rt reflect.Type, field string) string { - return field - }, - MissingField: func(rt reflect.Type, field string) error { - link := "" - if unicode.IsUpper(rune(rt.Name()[0])) && rt.PkgPath() != "main" { - link = fmt.Sprintf(", check github.com/ethereum/go-ethereum/swarm/api/config.go for available fields") - } - return fmt.Errorf("field '%s' is not defined in %s%s", field, rt.String(), link) - }, -} - -//before booting the swarm node, build the configuration -func buildConfig(ctx *cli.Context) (config *bzzapi.Config, err error) { - //start by creating a default config - config = bzzapi.NewConfig() - //first load settings from config file (if provided) - config, err = configFileOverride(config, ctx) - if err != nil { - return nil, err - } - //override settings provided by environment variables - config = envVarsOverride(config) - //override settings provided by command line - config = cmdLineOverride(config, ctx) - //validate configuration parameters - err = validateConfig(config) - - return -} - -//finally, after the configuration build phase is finished, initialize -func initSwarmNode(config *bzzapi.Config, stack *node.Node, ctx *cli.Context, nodeconfig *node.Config) error { - //at this point, all vars should be set in the Config - //get the account for the provided swarm account - prvkey := getAccount(config.BzzAccount, ctx, stack) - //set the resolved config path (geth --datadir) - config.Path = expandPath(stack.InstanceDir()) - //finally, initialize the configuration - err := config.Init(prvkey, nodeconfig.NodeKey()) - if err != nil { - return err - } - //configuration phase completed here - log.Debug("Starting Swarm with the following parameters:") - //after having created the config, print it to screen - log.Debug(printConfig(config)) - return nil -} - -//configFileOverride overrides the current config with the config file, if a config file has been provided -func configFileOverride(config *bzzapi.Config, ctx *cli.Context) (*bzzapi.Config, error) { - var err error - - //only do something if the -config flag has been set - if ctx.GlobalIsSet(SwarmTomlConfigPathFlag.Name) { - var filepath string - if filepath = ctx.GlobalString(SwarmTomlConfigPathFlag.Name); filepath == "" { - utils.Fatalf("Config file flag provided with invalid file path") - } - var f *os.File - f, err = os.Open(filepath) - if err != nil { - return nil, err - } - defer f.Close() - - //decode the TOML file into a Config struct - //note that we are decoding into the existing defaultConfig; - //if an entry is not present in the file, the default entry is kept - err = tomlSettings.NewDecoder(f).Decode(&config) - // Add file name to errors that have a line number. - if _, ok := err.(*toml.LineError); ok { - err = errors.New(filepath + ", " + err.Error()) - } - } - return config, err -} - -// cmdLineOverride overrides the current config with whatever is provided through the command line -// most values are not allowed a zero value (empty string), if not otherwise noted -func cmdLineOverride(currentConfig *bzzapi.Config, ctx *cli.Context) *bzzapi.Config { - if keyid := ctx.GlobalString(SwarmAccountFlag.Name); keyid != "" { - currentConfig.BzzAccount = keyid - } - - if chbookaddr := ctx.GlobalString(ChequebookAddrFlag.Name); chbookaddr != "" { - currentConfig.Contract = common.HexToAddress(chbookaddr) - } - - if networkid := ctx.GlobalString(SwarmNetworkIdFlag.Name); networkid != "" { - id, err := strconv.ParseUint(networkid, 10, 64) - if err != nil { - utils.Fatalf("invalid cli flag %s: %v", SwarmNetworkIdFlag.Name, err) - } - if id != 0 { - currentConfig.NetworkID = id - } - } - - if ctx.GlobalIsSet(utils.DataDirFlag.Name) { - if datadir := ctx.GlobalString(utils.DataDirFlag.Name); datadir != "" { - currentConfig.Path = expandPath(datadir) - } - } - - bzzport := ctx.GlobalString(SwarmPortFlag.Name) - if len(bzzport) > 0 { - currentConfig.Port = bzzport - } - - if bzzaddr := ctx.GlobalString(SwarmListenAddrFlag.Name); bzzaddr != "" { - currentConfig.ListenAddr = bzzaddr - } - - if ctx.GlobalIsSet(SwarmSwapEnabledFlag.Name) { - currentConfig.SwapEnabled = true - } - - if ctx.GlobalIsSet(SwarmSyncDisabledFlag.Name) { - currentConfig.SyncEnabled = false - } - - if d := ctx.GlobalDuration(SwarmSyncUpdateDelay.Name); d > 0 { - currentConfig.SyncUpdateDelay = d - } - - // any value including 0 is acceptable - currentConfig.MaxStreamPeerServers = ctx.GlobalInt(SwarmMaxStreamPeerServersFlag.Name) - - if ctx.GlobalIsSet(SwarmLightNodeEnabled.Name) { - currentConfig.LightNodeEnabled = true - } - - if ctx.GlobalIsSet(SwarmDeliverySkipCheckFlag.Name) { - currentConfig.DeliverySkipCheck = true - } - - currentConfig.SwapAPI = ctx.GlobalString(SwarmSwapAPIFlag.Name) - if currentConfig.SwapEnabled && currentConfig.SwapAPI == "" { - utils.Fatalf(SwarmErrSwapSetNoAPI) - } - - if ctx.GlobalIsSet(EnsAPIFlag.Name) { - ensAPIs := ctx.GlobalStringSlice(EnsAPIFlag.Name) - // preserve backward compatibility to disable ENS with --ens-api="" - if len(ensAPIs) == 1 && ensAPIs[0] == "" { - ensAPIs = nil - } - for i := range ensAPIs { - ensAPIs[i] = expandPath(ensAPIs[i]) - } - - currentConfig.EnsAPIs = ensAPIs - } - - if cors := ctx.GlobalString(CorsStringFlag.Name); cors != "" { - currentConfig.Cors = cors - } - - if storePath := ctx.GlobalString(SwarmStorePath.Name); storePath != "" { - currentConfig.ChunkDbPath = storePath - } - - if storeCapacity := ctx.GlobalUint64(SwarmStoreCapacity.Name); storeCapacity != 0 { - currentConfig.DbCapacity = storeCapacity - } - - if ctx.GlobalIsSet(SwarmStoreCacheCapacity.Name) { - currentConfig.CacheCapacity = ctx.GlobalUint(SwarmStoreCacheCapacity.Name) - } - - if ctx.GlobalIsSet(SwarmBootnodeModeFlag.Name) { - currentConfig.BootnodeMode = ctx.GlobalBool(SwarmBootnodeModeFlag.Name) - } - - if ctx.GlobalIsSet(SwarmGlobalStoreAPIFlag.Name) { - currentConfig.GlobalStoreAPI = ctx.GlobalString(SwarmGlobalStoreAPIFlag.Name) - } - - return currentConfig - -} - -// envVarsOverride overrides the current config with whatver is provided in environment variables -// most values are not allowed a zero value (empty string), if not otherwise noted -func envVarsOverride(currentConfig *bzzapi.Config) (config *bzzapi.Config) { - if keyid := os.Getenv(SwarmEnvAccount); keyid != "" { - currentConfig.BzzAccount = keyid - } - - if chbookaddr := os.Getenv(SwarmEnvChequebookAddr); chbookaddr != "" { - currentConfig.Contract = common.HexToAddress(chbookaddr) - } - - if networkid := os.Getenv(SwarmEnvNetworkID); networkid != "" { - id, err := strconv.ParseUint(networkid, 10, 64) - if err != nil { - utils.Fatalf("invalid environment variable %s: %v", SwarmEnvNetworkID, err) - } - if id != 0 { - currentConfig.NetworkID = id - } - } - - if datadir := os.Getenv(GethEnvDataDir); datadir != "" { - currentConfig.Path = expandPath(datadir) - } - - bzzport := os.Getenv(SwarmEnvPort) - if len(bzzport) > 0 { - currentConfig.Port = bzzport - } - - if bzzaddr := os.Getenv(SwarmEnvListenAddr); bzzaddr != "" { - currentConfig.ListenAddr = bzzaddr - } - - if swapenable := os.Getenv(SwarmEnvSwapEnable); swapenable != "" { - swap, err := strconv.ParseBool(swapenable) - if err != nil { - utils.Fatalf("invalid environment variable %s: %v", SwarmEnvSwapEnable, err) - } - currentConfig.SwapEnabled = swap - } - - if syncdisable := os.Getenv(SwarmEnvSyncDisable); syncdisable != "" { - sync, err := strconv.ParseBool(syncdisable) - if err != nil { - utils.Fatalf("invalid environment variable %s: %v", SwarmEnvSyncDisable, err) - } - currentConfig.SyncEnabled = !sync - } - - if v := os.Getenv(SwarmEnvDeliverySkipCheck); v != "" { - skipCheck, err := strconv.ParseBool(v) - if err != nil { - currentConfig.DeliverySkipCheck = skipCheck - } - } - - if v := os.Getenv(SwarmEnvSyncUpdateDelay); v != "" { - d, err := time.ParseDuration(v) - if err != nil { - utils.Fatalf("invalid environment variable %s: %v", SwarmEnvSyncUpdateDelay, err) - } - currentConfig.SyncUpdateDelay = d - } - - if max := os.Getenv(SwarmEnvMaxStreamPeerServers); max != "" { - m, err := strconv.Atoi(max) - if err != nil { - utils.Fatalf("invalid environment variable %s: %v", SwarmEnvMaxStreamPeerServers, err) - } - currentConfig.MaxStreamPeerServers = m - } - - if lne := os.Getenv(SwarmEnvLightNodeEnable); lne != "" { - lightnode, err := strconv.ParseBool(lne) - if err != nil { - utils.Fatalf("invalid environment variable %s: %v", SwarmEnvLightNodeEnable, err) - } - currentConfig.LightNodeEnabled = lightnode - } - - if swapapi := os.Getenv(SwarmEnvSwapAPI); swapapi != "" { - currentConfig.SwapAPI = swapapi - } - - if currentConfig.SwapEnabled && currentConfig.SwapAPI == "" { - utils.Fatalf(SwarmErrSwapSetNoAPI) - } - - if ensapi := os.Getenv(SwarmEnvENSAPI); ensapi != "" { - currentConfig.EnsAPIs = strings.Split(ensapi, ",") - } - - if ensaddr := os.Getenv(SwarmEnvENSAddr); ensaddr != "" { - currentConfig.EnsRoot = common.HexToAddress(ensaddr) - } - - if cors := os.Getenv(SwarmEnvCORS); cors != "" { - currentConfig.Cors = cors - } - - if bm := os.Getenv(SwarmEnvBootnodeMode); bm != "" { - bootnodeMode, err := strconv.ParseBool(bm) - if err != nil { - utils.Fatalf("invalid environment variable %s: %v", SwarmEnvBootnodeMode, err) - } - currentConfig.BootnodeMode = bootnodeMode - } - - if api := os.Getenv(SwarmGlobalstoreAPI); api != "" { - currentConfig.GlobalStoreAPI = api - } - - return currentConfig -} - -// dumpConfig is the dumpconfig command. -// writes a default config to STDOUT -func dumpConfig(ctx *cli.Context) error { - cfg, err := buildConfig(ctx) - if err != nil { - utils.Fatalf(fmt.Sprintf("Uh oh - dumpconfig triggered an error %v", err)) - } - comment := "" - out, err := tomlSettings.Marshal(&cfg) - if err != nil { - return err - } - io.WriteString(os.Stdout, comment) - os.Stdout.Write(out) - return nil -} - -//validate configuration parameters -func validateConfig(cfg *bzzapi.Config) (err error) { - for _, ensAPI := range cfg.EnsAPIs { - if ensAPI != "" { - if err := validateEnsAPIs(ensAPI); err != nil { - return fmt.Errorf("invalid format [tld:][contract-addr@]url for ENS API endpoint configuration %q: %v", ensAPI, err) - } - } - } - return nil -} - -//validate EnsAPIs configuration parameter -func validateEnsAPIs(s string) (err error) { - // missing contract address - if strings.HasPrefix(s, "@") { - return errors.New("missing contract address") - } - // missing url - if strings.HasSuffix(s, "@") { - return errors.New("missing url") - } - // missing tld - if strings.HasPrefix(s, ":") { - return errors.New("missing tld") - } - // missing url - if strings.HasSuffix(s, ":") { - return errors.New("missing url") - } - return nil -} - -//print a Config as string -func printConfig(config *bzzapi.Config) string { - out, err := tomlSettings.Marshal(&config) - if err != nil { - return fmt.Sprintf("Something is not right with the configuration: %v", err) - } - return string(out) -} diff --git a/cmd/swarm/config_test.go b/cmd/swarm/config_test.go deleted file mode 100644 index 484f6dec3..000000000 --- a/cmd/swarm/config_test.go +++ /dev/null @@ -1,575 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "fmt" - "io" - "io/ioutil" - "net" - "os" - "os/exec" - "testing" - "time" - - "github.com/docker/docker/pkg/reexec" - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/swarm" - "github.com/ethereum/go-ethereum/swarm/api" -) - -func TestConfigDump(t *testing.T) { - swarm := runSwarm(t, "dumpconfig") - defaultConf := api.NewConfig() - out, err := tomlSettings.Marshal(&defaultConf) - if err != nil { - t.Fatal(err) - } - swarm.Expect(string(out)) - swarm.ExpectExit() -} - -func TestConfigFailsSwapEnabledNoSwapApi(t *testing.T) { - flags := []string{ - fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", - fmt.Sprintf("--%s", SwarmPortFlag.Name), "54545", - fmt.Sprintf("--%s", SwarmSwapEnabledFlag.Name), - } - - swarm := runSwarm(t, flags...) - swarm.Expect("Fatal: " + SwarmErrSwapSetNoAPI + "\n") - swarm.ExpectExit() -} - -func TestConfigFailsNoBzzAccount(t *testing.T) { - flags := []string{ - fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", - fmt.Sprintf("--%s", SwarmPortFlag.Name), "54545", - } - - swarm := runSwarm(t, flags...) - swarm.Expect("Fatal: " + SwarmErrNoBZZAccount + "\n") - swarm.ExpectExit() -} - -func TestConfigCmdLineOverrides(t *testing.T) { - dir, err := ioutil.TempDir("", "bzztest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) - - conf, account := getTestAccount(t, dir) - node := &testNode{Dir: dir} - - // assign ports - httpPort, err := assignTCPPort() - if err != nil { - t.Fatal(err) - } - - flags := []string{ - fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", - fmt.Sprintf("--%s", SwarmPortFlag.Name), httpPort, - fmt.Sprintf("--%s", SwarmSyncDisabledFlag.Name), - fmt.Sprintf("--%s", CorsStringFlag.Name), "*", - fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(), - fmt.Sprintf("--%s", SwarmDeliverySkipCheckFlag.Name), - fmt.Sprintf("--%s", EnsAPIFlag.Name), "", - fmt.Sprintf("--%s", utils.DataDirFlag.Name), dir, - fmt.Sprintf("--%s", utils.IPCPathFlag.Name), conf.IPCPath, - } - node.Cmd = runSwarm(t, flags...) - node.Cmd.InputLine(testPassphrase) - defer func() { - if t.Failed() { - node.Shutdown() - } - }() - // wait for the node to start - for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { - node.Client, err = rpc.Dial(conf.IPCEndpoint()) - if err == nil { - break - } - } - if node.Client == nil { - t.Fatal(err) - } - - // load info - var info swarm.Info - if err := node.Client.Call(&info, "bzz_info"); err != nil { - t.Fatal(err) - } - - if info.Port != httpPort { - t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port) - } - - if info.NetworkID != 42 { - t.Fatalf("Expected network ID to be %d, got %d", 42, info.NetworkID) - } - - if info.SyncEnabled { - t.Fatal("Expected Sync to be disabled, but is true") - } - - if !info.DeliverySkipCheck { - t.Fatal("Expected DeliverySkipCheck to be enabled, but it is not") - } - - if info.Cors != "*" { - t.Fatalf("Expected Cors flag to be set to %s, got %s", "*", info.Cors) - } - - node.Shutdown() -} - -func TestConfigFileOverrides(t *testing.T) { - - // assign ports - httpPort, err := assignTCPPort() - if err != nil { - t.Fatal(err) - } - - //create a config file - //first, create a default conf - defaultConf := api.NewConfig() - //change some values in order to test if they have been loaded - defaultConf.SyncEnabled = false - defaultConf.DeliverySkipCheck = true - defaultConf.NetworkID = 54 - defaultConf.Port = httpPort - defaultConf.DbCapacity = 9000000 - defaultConf.HiveParams.KeepAliveInterval = 6000000000 - defaultConf.Swap.Params.Strategy.AutoCashInterval = 600 * time.Second - //defaultConf.SyncParams.KeyBufferSize = 512 - //create a TOML string - out, err := tomlSettings.Marshal(&defaultConf) - if err != nil { - t.Fatalf("Error creating TOML file in TestFileOverride: %v", err) - } - //create file - f, err := ioutil.TempFile("", "testconfig.toml") - if err != nil { - t.Fatalf("Error writing TOML file in TestFileOverride: %v", err) - } - //write file - _, err = f.WriteString(string(out)) - if err != nil { - t.Fatalf("Error writing TOML file in TestFileOverride: %v", err) - } - f.Sync() - - dir, err := ioutil.TempDir("", "bzztest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) - conf, account := getTestAccount(t, dir) - node := &testNode{Dir: dir} - - flags := []string{ - fmt.Sprintf("--%s", SwarmTomlConfigPathFlag.Name), f.Name(), - fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(), - fmt.Sprintf("--%s", EnsAPIFlag.Name), "", - fmt.Sprintf("--%s", utils.DataDirFlag.Name), dir, - fmt.Sprintf("--%s", utils.IPCPathFlag.Name), conf.IPCPath, - } - node.Cmd = runSwarm(t, flags...) - node.Cmd.InputLine(testPassphrase) - defer func() { - if t.Failed() { - node.Shutdown() - } - }() - // wait for the node to start - for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { - node.Client, err = rpc.Dial(conf.IPCEndpoint()) - if err == nil { - break - } - } - if node.Client == nil { - t.Fatal(err) - } - - // load info - var info swarm.Info - if err := node.Client.Call(&info, "bzz_info"); err != nil { - t.Fatal(err) - } - - if info.Port != httpPort { - t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port) - } - - if info.NetworkID != 54 { - t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkID) - } - - if info.SyncEnabled { - t.Fatal("Expected Sync to be disabled, but is true") - } - - if !info.DeliverySkipCheck { - t.Fatal("Expected DeliverySkipCheck to be enabled, but it is not") - } - - if info.DbCapacity != 9000000 { - t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkID) - } - - if info.HiveParams.KeepAliveInterval != 6000000000 { - t.Fatalf("Expected HiveParams KeepAliveInterval to be %d, got %d", uint64(6000000000), uint64(info.HiveParams.KeepAliveInterval)) - } - - if info.Swap.Params.Strategy.AutoCashInterval != 600*time.Second { - t.Fatalf("Expected SwapParams AutoCashInterval to be %ds, got %d", 600, info.Swap.Params.Strategy.AutoCashInterval) - } - - // if info.SyncParams.KeyBufferSize != 512 { - // t.Fatalf("Expected info.SyncParams.KeyBufferSize to be %d, got %d", 512, info.SyncParams.KeyBufferSize) - // } - - node.Shutdown() -} - -func TestConfigEnvVars(t *testing.T) { - // assign ports - httpPort, err := assignTCPPort() - if err != nil { - t.Fatal(err) - } - - envVars := os.Environ() - envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmPortFlag.EnvVar, httpPort)) - envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmNetworkIdFlag.EnvVar, "999")) - envVars = append(envVars, fmt.Sprintf("%s=%s", CorsStringFlag.EnvVar, "*")) - envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmSyncDisabledFlag.EnvVar, "true")) - envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmDeliverySkipCheckFlag.EnvVar, "true")) - - dir, err := ioutil.TempDir("", "bzztest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) - conf, account := getTestAccount(t, dir) - node := &testNode{Dir: dir} - flags := []string{ - fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(), - "--ens-api", "", - "--datadir", dir, - "--ipcpath", conf.IPCPath, - } - - //node.Cmd = runSwarm(t,flags...) - //node.Cmd.cmd.Env = envVars - //the above assignment does not work, so we need a custom Cmd here in order to pass envVars: - cmd := &exec.Cmd{ - Path: reexec.Self(), - Args: append([]string{"swarm-test"}, flags...), - Stderr: os.Stderr, - Stdout: os.Stdout, - } - cmd.Env = envVars - //stdout, err := cmd.StdoutPipe() - //if err != nil { - // t.Fatal(err) - //} - //stdout = bufio.NewReader(stdout) - var stdin io.WriteCloser - if stdin, err = cmd.StdinPipe(); err != nil { - t.Fatal(err) - } - if err := cmd.Start(); err != nil { - t.Fatal(err) - } - - //cmd.InputLine(testPassphrase) - io.WriteString(stdin, testPassphrase+"\n") - defer func() { - if t.Failed() { - node.Shutdown() - cmd.Process.Kill() - } - }() - // wait for the node to start - for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { - node.Client, err = rpc.Dial(conf.IPCEndpoint()) - if err == nil { - break - } - } - - if node.Client == nil { - t.Fatal(err) - } - - // load info - var info swarm.Info - if err := node.Client.Call(&info, "bzz_info"); err != nil { - t.Fatal(err) - } - - if info.Port != httpPort { - t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port) - } - - if info.NetworkID != 999 { - t.Fatalf("Expected network ID to be %d, got %d", 999, info.NetworkID) - } - - if info.Cors != "*" { - t.Fatalf("Expected Cors flag to be set to %s, got %s", "*", info.Cors) - } - - if info.SyncEnabled { - t.Fatal("Expected Sync to be disabled, but is true") - } - - if !info.DeliverySkipCheck { - t.Fatal("Expected DeliverySkipCheck to be enabled, but it is not") - } - - node.Shutdown() - cmd.Process.Kill() -} - -func TestConfigCmdLineOverridesFile(t *testing.T) { - - // assign ports - httpPort, err := assignTCPPort() - if err != nil { - t.Fatal(err) - } - - //create a config file - //first, create a default conf - defaultConf := api.NewConfig() - //change some values in order to test if they have been loaded - defaultConf.SyncEnabled = true - defaultConf.NetworkID = 54 - defaultConf.Port = "8588" - defaultConf.DbCapacity = 9000000 - defaultConf.HiveParams.KeepAliveInterval = 6000000000 - defaultConf.Swap.Params.Strategy.AutoCashInterval = 600 * time.Second - //defaultConf.SyncParams.KeyBufferSize = 512 - //create a TOML file - out, err := tomlSettings.Marshal(&defaultConf) - if err != nil { - t.Fatalf("Error creating TOML file in TestFileOverride: %v", err) - } - //write file - fname := "testconfig.toml" - f, err := ioutil.TempFile("", fname) - if err != nil { - t.Fatalf("Error writing TOML file in TestFileOverride: %v", err) - } - defer os.Remove(fname) - //write file - _, err = f.WriteString(string(out)) - if err != nil { - t.Fatalf("Error writing TOML file in TestFileOverride: %v", err) - } - f.Sync() - - dir, err := ioutil.TempDir("", "bzztest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) - conf, account := getTestAccount(t, dir) - node := &testNode{Dir: dir} - - expectNetworkId := uint64(77) - - flags := []string{ - fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "77", - fmt.Sprintf("--%s", SwarmPortFlag.Name), httpPort, - fmt.Sprintf("--%s", SwarmSyncDisabledFlag.Name), - fmt.Sprintf("--%s", SwarmTomlConfigPathFlag.Name), f.Name(), - fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(), - fmt.Sprintf("--%s", EnsAPIFlag.Name), "", - fmt.Sprintf("--%s", utils.DataDirFlag.Name), dir, - fmt.Sprintf("--%s", utils.IPCPathFlag.Name), conf.IPCPath, - } - node.Cmd = runSwarm(t, flags...) - node.Cmd.InputLine(testPassphrase) - defer func() { - if t.Failed() { - node.Shutdown() - } - }() - // wait for the node to start - for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { - node.Client, err = rpc.Dial(conf.IPCEndpoint()) - if err == nil { - break - } - } - if node.Client == nil { - t.Fatal(err) - } - - // load info - var info swarm.Info - if err := node.Client.Call(&info, "bzz_info"); err != nil { - t.Fatal(err) - } - - if info.Port != httpPort { - t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port) - } - - if info.NetworkID != expectNetworkId { - t.Fatalf("Expected network ID to be %d, got %d", expectNetworkId, info.NetworkID) - } - - if info.SyncEnabled { - t.Fatal("Expected Sync to be disabled, but is true") - } - - if info.DbCapacity != 9000000 { - t.Fatalf("Expected Capacity to be %d, got %d", 9000000, info.DbCapacity) - } - - if info.HiveParams.KeepAliveInterval != 6000000000 { - t.Fatalf("Expected HiveParams KeepAliveInterval to be %d, got %d", uint64(6000000000), uint64(info.HiveParams.KeepAliveInterval)) - } - - if info.Swap.Params.Strategy.AutoCashInterval != 600*time.Second { - t.Fatalf("Expected SwapParams AutoCashInterval to be %ds, got %d", 600, info.Swap.Params.Strategy.AutoCashInterval) - } - - // if info.SyncParams.KeyBufferSize != 512 { - // t.Fatalf("Expected info.SyncParams.KeyBufferSize to be %d, got %d", 512, info.SyncParams.KeyBufferSize) - // } - - node.Shutdown() -} - -func TestConfigValidate(t *testing.T) { - for _, c := range []struct { - cfg *api.Config - err string - }{ - { - cfg: &api.Config{EnsAPIs: []string{ - "/data/testnet/geth.ipc", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "http://127.0.0.1:1234", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "ws://127.0.0.1:1234", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "test:/data/testnet/geth.ipc", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "test:ws://127.0.0.1:1234", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "314159265dD8dbb310642f98f50C066173C1259b@/data/testnet/geth.ipc", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "314159265dD8dbb310642f98f50C066173C1259b@http://127.0.0.1:1234", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "314159265dD8dbb310642f98f50C066173C1259b@ws://127.0.0.1:1234", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "test:314159265dD8dbb310642f98f50C066173C1259b@/data/testnet/geth.ipc", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "eth:314159265dD8dbb310642f98f50C066173C1259b@http://127.0.0.1:1234", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "eth:314159265dD8dbb310642f98f50C066173C1259b@ws://127.0.0.1:12344", - }}, - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "eth:", - }}, - err: "invalid format [tld:][contract-addr@]url for ENS API endpoint configuration \"eth:\": missing url", - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "314159265dD8dbb310642f98f50C066173C1259b@", - }}, - err: "invalid format [tld:][contract-addr@]url for ENS API endpoint configuration \"314159265dD8dbb310642f98f50C066173C1259b@\": missing url", - }, - { - cfg: &api.Config{EnsAPIs: []string{ - ":314159265dD8dbb310642f98f50C066173C1259", - }}, - err: "invalid format [tld:][contract-addr@]url for ENS API endpoint configuration \":314159265dD8dbb310642f98f50C066173C1259\": missing tld", - }, - { - cfg: &api.Config{EnsAPIs: []string{ - "@/data/testnet/geth.ipc", - }}, - err: "invalid format [tld:][contract-addr@]url for ENS API endpoint configuration \"@/data/testnet/geth.ipc\": missing contract address", - }, - } { - err := validateConfig(c.cfg) - if c.err != "" && err.Error() != c.err { - t.Errorf("expected error %q, got %q", c.err, err) - } - if c.err == "" && err != nil { - t.Errorf("unexpected error %q", err) - } - } -} - -func assignTCPPort() (string, error) { - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return "", err - } - l.Close() - _, port, err := net.SplitHostPort(l.Addr().String()) - if err != nil { - return "", err - } - return port, nil -} diff --git a/cmd/swarm/db.go b/cmd/swarm/db.go deleted file mode 100644 index b0e9f367f..000000000 --- a/cmd/swarm/db.go +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "archive/tar" - "bytes" - "encoding/binary" - "encoding/hex" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/swarm/chunk" - "github.com/ethereum/go-ethereum/swarm/storage/localstore" - "github.com/syndtr/goleveldb/leveldb" - "github.com/syndtr/goleveldb/leveldb/opt" - "gopkg.in/urfave/cli.v1" -) - -var legacyKeyIndex = byte(0) -var keyData = byte(6) - -type dpaDBIndex struct { - Idx uint64 - Access uint64 -} - -var dbCommand = cli.Command{ - Name: "db", - CustomHelpTemplate: helpTemplate, - Usage: "manage the local chunk database", - ArgsUsage: "db COMMAND", - Description: "Manage the local chunk database", - Subcommands: []cli.Command{ - { - Action: dbExport, - CustomHelpTemplate: helpTemplate, - Name: "export", - Usage: "export a local chunk database as a tar archive (use - to send to stdout)", - ArgsUsage: "<chunkdb> <file>", - Description: ` -Export a local chunk database as a tar archive (use - to send to stdout). - - swarm db export ~/.ethereum/swarm/bzz-KEY/chunks chunks.tar - -The export may be quite large, consider piping the output through the Unix -pv(1) tool to get a progress bar: - - swarm db export ~/.ethereum/swarm/bzz-KEY/chunks - | pv > chunks.tar -`, - }, - { - Action: dbImport, - CustomHelpTemplate: helpTemplate, - Name: "import", - Usage: "import chunks from a tar archive into a local chunk database (use - to read from stdin)", - ArgsUsage: "<chunkdb> <file>", - Description: `Import chunks from a tar archive into a local chunk database (use - to read from stdin). - - swarm db import ~/.ethereum/swarm/bzz-KEY/chunks chunks.tar - -The import may be quite large, consider piping the input through the Unix -pv(1) tool to get a progress bar: - - pv chunks.tar | swarm db import ~/.ethereum/swarm/bzz-KEY/chunks -`, - Flags: []cli.Flag{ - SwarmLegacyFlag, - }, - }, - }, -} - -func dbExport(ctx *cli.Context) { - args := ctx.Args() - if len(args) != 3 { - utils.Fatalf("invalid arguments, please specify both <chunkdb> (path to a local chunk database), <file> (path to write the tar archive to, - for stdout) and the base key") - } - - var out io.Writer - if args[1] == "-" { - out = os.Stdout - } else { - f, err := os.Create(args[1]) - if err != nil { - utils.Fatalf("error opening output file: %s", err) - } - defer f.Close() - out = f - } - - isLegacy := localstore.IsLegacyDatabase(args[0]) - if isLegacy { - count, err := exportLegacy(args[0], common.Hex2Bytes(args[2]), out) - if err != nil { - utils.Fatalf("error exporting legacy local chunk database: %s", err) - } - - log.Info(fmt.Sprintf("successfully exported %d chunks from legacy db", count)) - return - } - - store, err := openLDBStore(args[0], common.Hex2Bytes(args[2])) - if err != nil { - utils.Fatalf("error opening local chunk database: %s", err) - } - defer store.Close() - - count, err := store.Export(out) - if err != nil { - utils.Fatalf("error exporting local chunk database: %s", err) - } - - log.Info(fmt.Sprintf("successfully exported %d chunks", count)) -} - -func dbImport(ctx *cli.Context) { - args := ctx.Args() - if len(args) != 3 { - utils.Fatalf("invalid arguments, please specify both <chunkdb> (path to a local chunk database), <file> (path to read the tar archive from, - for stdin) and the base key") - } - - legacy := ctx.IsSet(SwarmLegacyFlag.Name) - - store, err := openLDBStore(args[0], common.Hex2Bytes(args[2])) - if err != nil { - utils.Fatalf("error opening local chunk database: %s", err) - } - defer store.Close() - - var in io.Reader - if args[1] == "-" { - in = os.Stdin - } else { - f, err := os.Open(args[1]) - if err != nil { - utils.Fatalf("error opening input file: %s", err) - } - defer f.Close() - in = f - } - - count, err := store.Import(in, legacy) - if err != nil { - utils.Fatalf("error importing local chunk database: %s", err) - } - - log.Info(fmt.Sprintf("successfully imported %d chunks", count)) -} - -func openLDBStore(path string, basekey []byte) (*localstore.DB, error) { - if _, err := os.Stat(filepath.Join(path, "CURRENT")); err != nil { - return nil, fmt.Errorf("invalid chunkdb path: %s", err) - } - - return localstore.New(path, basekey, nil) -} - -func decodeIndex(data []byte, index *dpaDBIndex) error { - dec := rlp.NewStream(bytes.NewReader(data), 0) - return dec.Decode(index) -} - -func getDataKey(idx uint64, po uint8) []byte { - key := make([]byte, 10) - key[0] = keyData - key[1] = po - binary.BigEndian.PutUint64(key[2:], idx) - - return key -} - -func exportLegacy(path string, basekey []byte, out io.Writer) (int64, error) { - tw := tar.NewWriter(out) - defer tw.Close() - db, err := leveldb.OpenFile(path, &opt.Options{OpenFilesCacheCapacity: 128}) - if err != nil { - return 0, err - } - defer db.Close() - - it := db.NewIterator(nil, nil) - defer it.Release() - var count int64 - for ok := it.Seek([]byte{legacyKeyIndex}); ok; ok = it.Next() { - key := it.Key() - if (key == nil) || (key[0] != legacyKeyIndex) { - break - } - - var index dpaDBIndex - - hash := key[1:] - decodeIndex(it.Value(), &index) - - po := uint8(chunk.Proximity(basekey, hash)) - - datakey := getDataKey(index.Idx, po) - data, err := db.Get(datakey, nil) - if err != nil { - log.Crit(fmt.Sprintf("Chunk %x found but could not be accessed: %v, %x", key, err, datakey)) - continue - } - - hdr := &tar.Header{ - Name: hex.EncodeToString(hash), - Mode: 0644, - Size: int64(len(data)), - } - if err := tw.WriteHeader(hdr); err != nil { - return count, err - } - if _, err := tw.Write(data); err != nil { - return count, err - } - count++ - } - - return count, nil -} diff --git a/cmd/swarm/download.go b/cmd/swarm/download.go deleted file mode 100644 index fcbefa020..000000000 --- a/cmd/swarm/download.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. -package main - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/swarm/api" - swarm "github.com/ethereum/go-ethereum/swarm/api/client" - "gopkg.in/urfave/cli.v1" -) - -var downloadCommand = cli.Command{ - Action: download, - Name: "down", - Flags: []cli.Flag{SwarmRecursiveFlag, SwarmAccessPasswordFlag}, - Usage: "downloads a swarm manifest or a file inside a manifest", - ArgsUsage: " <uri> [<dir>]", - Description: `Downloads a swarm bzz uri to the given dir. When no dir is provided, working directory is assumed. --recursive flag is expected when downloading a manifest with multiple entries.`, -} - -func download(ctx *cli.Context) { - log.Debug("downloading content using swarm down") - args := ctx.Args() - dest := "." - - switch len(args) { - case 0: - utils.Fatalf("Usage: swarm down [options] <bzz locator> [<destination path>]") - case 1: - log.Trace(fmt.Sprintf("swarm down: no destination path - assuming working dir")) - default: - log.Trace(fmt.Sprintf("destination path arg: %s", args[1])) - if absDest, err := filepath.Abs(args[1]); err == nil { - dest = absDest - } else { - utils.Fatalf("could not get download path: %v", err) - } - } - - var ( - bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") - isRecursive = ctx.Bool(SwarmRecursiveFlag.Name) - client = swarm.NewClient(bzzapi) - ) - - if fi, err := os.Stat(dest); err == nil { - if isRecursive && !fi.Mode().IsDir() { - utils.Fatalf("destination path is not a directory!") - } - } else { - if !os.IsNotExist(err) { - utils.Fatalf("could not stat path: %v", err) - } - } - - uri, err := api.Parse(args[0]) - if err != nil { - utils.Fatalf("could not parse uri argument: %v", err) - } - - dl := func(credentials string) error { - // assume behaviour according to --recursive switch - if isRecursive { - if err := client.DownloadDirectory(uri.Addr, uri.Path, dest, credentials); err != nil { - if err == swarm.ErrUnauthorized { - return err - } - return fmt.Errorf("directory %s: %v", uri.Path, err) - } - } else { - // we are downloading a file - log.Debug("downloading file/path from a manifest", "uri.Addr", uri.Addr, "uri.Path", uri.Path) - - err := client.DownloadFile(uri.Addr, uri.Path, dest, credentials) - if err != nil { - if err == swarm.ErrUnauthorized { - return err - } - return fmt.Errorf("file %s from address: %s: %v", uri.Path, uri.Addr, err) - } - } - return nil - } - if passwords := makePasswordList(ctx); passwords != nil { - password := getPassPhrase(fmt.Sprintf("Downloading %s is restricted", uri), 0, passwords) - err = dl(password) - } else { - err = dl("") - } - if err != nil { - utils.Fatalf("download: %v", err) - } -} diff --git a/cmd/swarm/explore.go b/cmd/swarm/explore.go deleted file mode 100644 index 9566213e4..000000000 --- a/cmd/swarm/explore.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2019 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -// Command bzzhash computes a swarm tree hash. -package main - -import ( - "context" - "fmt" - "os" - - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/swarm/chunk" - "github.com/ethereum/go-ethereum/swarm/storage" - "gopkg.in/urfave/cli.v1" -) - -var hashesCommand = cli.Command{ - Action: hashes, - CustomHelpTemplate: helpTemplate, - Name: "hashes", - Usage: "print all hashes of a file to STDOUT", - ArgsUsage: "<file>", - Description: "Prints all hashes of a file to STDOUT", -} - -func hashes(ctx *cli.Context) { - args := ctx.Args() - if len(args) < 1 { - utils.Fatalf("Usage: swarm hashes <file name>") - } - f, err := os.Open(args[0]) - if err != nil { - utils.Fatalf("Error opening file " + args[1]) - } - defer f.Close() - - fileStore := storage.NewFileStore(&storage.FakeChunkStore{}, storage.NewFileStoreParams(), chunk.NewTags()) - refs, err := fileStore.GetAllReferences(context.TODO(), f, false) - if err != nil { - utils.Fatalf("%v\n", err) - } else { - for _, r := range refs { - fmt.Println(r.String()) - } - } -} diff --git a/cmd/swarm/export_test.go b/cmd/swarm/export_test.go deleted file mode 100644 index ca82cfd4c..000000000 --- a/cmd/swarm/export_test.go +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "crypto/md5" - "encoding/base64" - "encoding/hex" - "io" - "io/ioutil" - "net/http" - "os" - "path" - "runtime" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/cmd/swarm/testdata" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/swarm" - "github.com/ethereum/go-ethereum/swarm/testutil" -) - -const ( - DATABASE_FIXTURE_BZZ_ACCOUNT = "0aa159029fa13ffa8fa1c6fff6ebceface99d6a4" - DATABASE_FIXTURE_PASSWORD = "pass" - FIXTURE_DATADIR_PREFIX = "swarm/bzz-0aa159029fa13ffa8fa1c6fff6ebceface99d6a4" - FixtureBaseKey = "a9f22b3d77b4bdf5f3eefce995d6c8e7cecf2636f20956f08a0d1ed95adb52ad" -) - -// TestCLISwarmExportImport perform the following test: -// 1. runs swarm node -// 2. uploads a random file -// 3. runs an export of the local datastore -// 4. runs a second swarm node -// 5. imports the exported datastore -// 6. fetches the uploaded random file from the second node -func TestCLISwarmExportImport(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip() - } - cluster := newTestCluster(t, 1) - - // generate random 1mb file - content := testutil.RandomBytes(1, 1000000) - fileName := testutil.TempFileWithContent(t, string(content)) - defer os.Remove(fileName) - - // upload the file with 'swarm up' and expect a hash - up := runSwarm(t, "--bzzapi", cluster.Nodes[0].URL, "up", fileName) - _, matches := up.ExpectRegexp(`[a-f\d]{64}`) - up.ExpectExit() - hash := matches[0] - - var info swarm.Info - if err := cluster.Nodes[0].Client.Call(&info, "bzz_info"); err != nil { - t.Fatal(err) - } - - cluster.Stop() - defer cluster.Cleanup() - - // generate an export.tar - exportCmd := runSwarm(t, "db", "export", info.Path+"/chunks", info.Path+"/export.tar", strings.TrimPrefix(info.BzzKey, "0x")) - exportCmd.ExpectExit() - - // start second cluster - cluster2 := newTestCluster(t, 1) - - var info2 swarm.Info - if err := cluster2.Nodes[0].Client.Call(&info2, "bzz_info"); err != nil { - t.Fatal(err) - } - - // stop second cluster, so that we close LevelDB - cluster2.Stop() - defer cluster2.Cleanup() - - // import the export.tar - importCmd := runSwarm(t, "db", "import", info2.Path+"/chunks", info.Path+"/export.tar", strings.TrimPrefix(info2.BzzKey, "0x")) - importCmd.ExpectExit() - - // spin second cluster back up - cluster2.StartExistingNodes(t, 1, strings.TrimPrefix(info2.BzzAccount, "0x")) - - // try to fetch imported file - res, err := http.Get(cluster2.Nodes[0].URL + "/bzz:/" + hash) - if err != nil { - t.Fatal(err) - } - - if res.StatusCode != 200 { - t.Fatalf("expected HTTP status %d, got %s", 200, res.Status) - } - - // compare downloaded file with the generated random file - mustEqualFiles(t, bytes.NewReader(content), res.Body) -} - -// TestExportLegacyToNew checks that an old database gets imported correctly into the new localstore structure -// The test sequence is as follows: -// 1. unpack database fixture to tmp dir -// 2. try to open with new swarm binary that should complain about old database -// 3. export from old database -// 4. remove the chunks folder -// 5. import the dump -// 6. file should be accessible -func TestExportLegacyToNew(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip() // this should be reenabled once the appveyor tests underlying issue is fixed - } - /* - fixture bzz account 0aa159029fa13ffa8fa1c6fff6ebceface99d6a4 - */ - const UPLOADED_FILE_MD5_HASH = "a001fdae53ba50cae584b8b02b06f821" - const UPLOADED_HASH = "67a86082ee0ea1bc7dd8d955bb1e14d04f61d55ae6a4b37b3d0296a3a95e454a" - tmpdir, err := ioutil.TempDir("", "swarm-test") - log.Trace("running legacy datastore migration test", "temp dir", tmpdir) - defer os.RemoveAll(tmpdir) - if err != nil { - t.Fatal(err) - } - inflateBase64Gzip(t, testdata.DATADIR_MIGRATION_FIXTURE, tmpdir) - - tmpPassword := testutil.TempFileWithContent(t, DATABASE_FIXTURE_PASSWORD) - defer os.Remove(tmpPassword) - - flags := []string{ - "--datadir", tmpdir, - "--bzzaccount", DATABASE_FIXTURE_BZZ_ACCOUNT, - "--password", tmpPassword, - } - - newSwarmOldDb := runSwarm(t, flags...) - _, matches := newSwarmOldDb.ExpectRegexp(".+") - newSwarmOldDb.ExpectExit() - - if len(matches) == 0 { - t.Fatalf("stdout not matched") - } - - if newSwarmOldDb.ExitStatus() == 0 { - t.Fatal("should error") - } - t.Log("exporting legacy database") - actualDataDir := path.Join(tmpdir, FIXTURE_DATADIR_PREFIX) - exportCmd := runSwarm(t, "--verbosity", "5", "db", "export", actualDataDir+"/chunks", tmpdir+"/export.tar", FixtureBaseKey) - exportCmd.ExpectExit() - - stat, err := os.Stat(tmpdir + "/export.tar") - if err != nil { - t.Fatal(err) - } - - // make some silly size assumption - if stat.Size() < 90000 { - t.Fatal("export size too small") - } - log.Info("removing chunk datadir") - err = os.RemoveAll(path.Join(actualDataDir, "chunks")) - if err != nil { - t.Fatal(err) - } - - // start second cluster - cluster2 := newTestCluster(t, 1) - var info2 swarm.Info - if err := cluster2.Nodes[0].Client.Call(&info2, "bzz_info"); err != nil { - t.Fatal(err) - } - - // stop second cluster, so that we close LevelDB - cluster2.Stop() - defer cluster2.Cleanup() - - // import the export.tar - importCmd := runSwarm(t, "db", "import", "--legacy", info2.Path+"/chunks", tmpdir+"/export.tar", strings.TrimPrefix(info2.BzzKey, "0x")) - importCmd.ExpectExit() - - // spin second cluster back up - cluster2.StartExistingNodes(t, 1, strings.TrimPrefix(info2.BzzAccount, "0x")) - t.Log("trying to http get the file") - // try to fetch imported file - res, err := http.Get(cluster2.Nodes[0].URL + "/bzz:/" + UPLOADED_HASH) - if err != nil { - t.Fatal(err) - } - - if res.StatusCode != 200 { - t.Fatalf("expected HTTP status %d, got %s", 200, res.Status) - } - h := md5.New() - if _, err := io.Copy(h, res.Body); err != nil { - t.Fatal(err) - } - - sum := h.Sum(nil) - - b, err := hex.DecodeString(UPLOADED_FILE_MD5_HASH) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(sum, b) { - t.Fatal("should be equal") - } -} - -func mustEqualFiles(t *testing.T, up io.Reader, down io.Reader) { - h := md5.New() - upLen, err := io.Copy(h, up) - if err != nil { - t.Fatal(err) - } - upHash := h.Sum(nil) - h.Reset() - downLen, err := io.Copy(h, down) - if err != nil { - t.Fatal(err) - } - downHash := h.Sum(nil) - - if !bytes.Equal(upHash, downHash) || upLen != downLen { - t.Fatalf("downloaded imported file md5=%x (length %v) is not the same as the generated one mp5=%x (length %v)", downHash, downLen, upHash, upLen) - } -} - -func inflateBase64Gzip(t *testing.T, base64File, directory string) { - t.Helper() - - f := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64File)) - gzf, err := gzip.NewReader(f) - if err != nil { - t.Fatal(err) - } - - tarReader := tar.NewReader(gzf) - - for { - header, err := tarReader.Next() - if err == io.EOF { - break - } - - if err != nil { - t.Fatal(err) - } - - name := header.Name - - switch header.Typeflag { - case tar.TypeDir: - err := os.Mkdir(path.Join(directory, name), os.ModePerm) - if err != nil { - t.Fatal(err) - } - case tar.TypeReg: - file, err := os.Create(path.Join(directory, name)) - if err != nil { - t.Fatal(err) - } - if _, err := io.Copy(file, tarReader); err != nil { - t.Fatal(err) - } - file.Close() - default: - t.Fatal("shouldn't happen") - } - } -} diff --git a/cmd/swarm/feeds.go b/cmd/swarm/feeds.go deleted file mode 100644 index 6cd971a92..000000000 --- a/cmd/swarm/feeds.go +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -// Command feed allows the user to create and update signed Swarm feeds -package main - -import ( - "fmt" - "strings" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" - - "github.com/ethereum/go-ethereum/cmd/utils" - swarm "github.com/ethereum/go-ethereum/swarm/api/client" - "github.com/ethereum/go-ethereum/swarm/storage/feed" - "gopkg.in/urfave/cli.v1" -) - -var feedCommand = cli.Command{ - CustomHelpTemplate: helpTemplate, - Name: "feed", - Usage: "(Advanced) Create and update Swarm Feeds", - ArgsUsage: "<create|update|info>", - Description: "Works with Swarm Feeds", - Subcommands: []cli.Command{ - { - Action: feedCreateManifest, - CustomHelpTemplate: helpTemplate, - Name: "create", - Usage: "creates and publishes a new feed manifest", - Description: `creates and publishes a new feed manifest pointing to a specified user's updates about a particular topic. - The feed topic can be built in the following ways: - * use --topic to set the topic to an arbitrary binary hex string. - * use --name to set the topic to a human-readable name. - For example --name could be set to "profile-picture", meaning this feed allows to get this user's current profile picture. - * use both --topic and --name to create named subtopics. - For example, --topic could be set to an Ethereum contract address and --name could be set to "comments", meaning - this feed tracks a discussion about that contract. - The --user flag allows to have this manifest refer to a user other than yourself. If not specified, - it will then default to your local account (--bzzaccount)`, - Flags: []cli.Flag{SwarmFeedNameFlag, SwarmFeedTopicFlag, SwarmFeedUserFlag}, - }, - { - Action: feedUpdate, - CustomHelpTemplate: helpTemplate, - Name: "update", - Usage: "updates the content of an existing Swarm Feed", - ArgsUsage: "<0x Hex data>", - Description: `publishes a new update on the specified topic - The feed topic can be built in the following ways: - * use --topic to set the topic to an arbitrary binary hex string. - * use --name to set the topic to a human-readable name. - For example --name could be set to "profile-picture", meaning this feed allows to get this user's current profile picture. - * use both --topic and --name to create named subtopics. - For example, --topic could be set to an Ethereum contract address and --name could be set to "comments", meaning - this feed tracks a discussion about that contract. - - If you have a manifest, you can specify it with --manifest to refer to the feed, - instead of using --topic / --name - `, - Flags: []cli.Flag{SwarmFeedManifestFlag, SwarmFeedNameFlag, SwarmFeedTopicFlag}, - }, - { - Action: feedInfo, - CustomHelpTemplate: helpTemplate, - Name: "info", - Usage: "obtains information about an existing Swarm feed", - Description: `obtains information about an existing Swarm feed - The topic can be specified directly with the --topic flag as an hex string - If no topic is specified, the default topic (zero) will be used - The --name flag can be used to specify subtopics with a specific name. - The --user flag allows to refer to a user other than yourself. If not specified, - it will then default to your local account (--bzzaccount) - If you have a manifest, you can specify it with --manifest instead of --topic / --name / ---user - to refer to the feed`, - Flags: []cli.Flag{SwarmFeedManifestFlag, SwarmFeedNameFlag, SwarmFeedTopicFlag, SwarmFeedUserFlag}, - }, - }, -} - -func NewGenericSigner(ctx *cli.Context) feed.Signer { - return feed.NewGenericSigner(getPrivKey(ctx)) -} - -func getTopic(ctx *cli.Context) (topic feed.Topic) { - var name = ctx.String(SwarmFeedNameFlag.Name) - var relatedTopic = ctx.String(SwarmFeedTopicFlag.Name) - var relatedTopicBytes []byte - var err error - - if relatedTopic != "" { - relatedTopicBytes, err = hexutil.Decode(relatedTopic) - if err != nil { - utils.Fatalf("Error parsing topic: %s", err) - } - } - - topic, err = feed.NewTopic(name, relatedTopicBytes) - if err != nil { - utils.Fatalf("Error parsing topic: %s", err) - } - return topic -} - -// swarm feed create <frequency> [--name <name>] [--data <0x Hexdata> [--multihash=false]] -// swarm feed update <Manifest Address or ENS domain> <0x Hexdata> [--multihash=false] -// swarm feed info <Manifest Address or ENS domain> - -func feedCreateManifest(ctx *cli.Context) { - var ( - bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") - client = swarm.NewClient(bzzapi) - ) - - newFeedUpdateRequest := feed.NewFirstRequest(getTopic(ctx)) - newFeedUpdateRequest.Feed.User = feedGetUser(ctx) - - manifestAddress, err := client.CreateFeedWithManifest(newFeedUpdateRequest) - if err != nil { - utils.Fatalf("Error creating feed manifest: %s", err.Error()) - return - } - fmt.Println(manifestAddress) // output manifest address to the user in a single line (useful for other commands to pick up) - -} - -func feedUpdate(ctx *cli.Context) { - args := ctx.Args() - - var ( - bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") - client = swarm.NewClient(bzzapi) - manifestAddressOrDomain = ctx.String(SwarmFeedManifestFlag.Name) - ) - - if len(args) < 1 { - fmt.Println("Incorrect number of arguments") - cli.ShowCommandHelpAndExit(ctx, "update", 1) - return - } - - signer := NewGenericSigner(ctx) - - data, err := hexutil.Decode(args[0]) - if err != nil { - utils.Fatalf("Error parsing data: %s", err.Error()) - return - } - - var updateRequest *feed.Request - var query *feed.Query - - if manifestAddressOrDomain == "" { - query = new(feed.Query) - query.User = signer.Address() - query.Topic = getTopic(ctx) - } - - // Retrieve a feed update request - updateRequest, err = client.GetFeedRequest(query, manifestAddressOrDomain) - if err != nil { - utils.Fatalf("Error retrieving feed status: %s", err.Error()) - } - - // Check that the provided signer matches the request to sign - if updateRequest.User != signer.Address() { - utils.Fatalf("Signer address does not match the update request") - } - - // set the new data - updateRequest.SetData(data) - - // sign update - if err = updateRequest.Sign(signer); err != nil { - utils.Fatalf("Error signing feed update: %s", err.Error()) - } - - // post update - err = client.UpdateFeed(updateRequest) - if err != nil { - utils.Fatalf("Error updating feed: %s", err.Error()) - return - } -} - -func feedInfo(ctx *cli.Context) { - var ( - bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") - client = swarm.NewClient(bzzapi) - manifestAddressOrDomain = ctx.String(SwarmFeedManifestFlag.Name) - ) - - var query *feed.Query - if manifestAddressOrDomain == "" { - query = new(feed.Query) - query.Topic = getTopic(ctx) - query.User = feedGetUser(ctx) - } - - metadata, err := client.GetFeedRequest(query, manifestAddressOrDomain) - if err != nil { - utils.Fatalf("Error retrieving feed metadata: %s", err.Error()) - return - } - encodedMetadata, err := metadata.MarshalJSON() - if err != nil { - utils.Fatalf("Error encoding metadata to JSON for display:%s", err) - } - fmt.Println(string(encodedMetadata)) -} - -func feedGetUser(ctx *cli.Context) common.Address { - var user = ctx.String(SwarmFeedUserFlag.Name) - if user != "" { - return common.HexToAddress(user) - } - pk := getPrivKey(ctx) - if pk == nil { - utils.Fatalf("Cannot read private key. Must specify --user or --bzzaccount") - } - return crypto.PubkeyToAddress(pk.PublicKey) - -} diff --git a/cmd/swarm/feeds_test.go b/cmd/swarm/feeds_test.go deleted file mode 100644 index 4c40f62a8..000000000 --- a/cmd/swarm/feeds_test.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "os" - "testing" - - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/swarm/api" - swarm "github.com/ethereum/go-ethereum/swarm/api/client" - swarmhttp "github.com/ethereum/go-ethereum/swarm/api/http" - "github.com/ethereum/go-ethereum/swarm/storage/feed" - "github.com/ethereum/go-ethereum/swarm/storage/feed/lookup" - "github.com/ethereum/go-ethereum/swarm/testutil" -) - -func TestCLIFeedUpdate(t *testing.T) { - srv := swarmhttp.NewTestSwarmServer(t, func(api *api.API) swarmhttp.TestServer { - return swarmhttp.NewServer(api, "") - }, nil) - log.Info("starting a test swarm server") - defer srv.Close() - - // create a private key file for signing - privkeyHex := "0000000000000000000000000000000000000000000000000000000000001979" - privKey, _ := crypto.HexToECDSA(privkeyHex) - address := crypto.PubkeyToAddress(privKey.PublicKey) - - pkFileName := testutil.TempFileWithContent(t, privkeyHex) - defer os.Remove(pkFileName) - - // compose a topic. We'll be doing quotes about Miguel de Cervantes - var topic feed.Topic - subject := []byte("Miguel de Cervantes") - copy(topic[:], subject[:]) - name := "quotes" - - // prepare some data for the update - data := []byte("En boca cerrada no entran moscas") - hexData := hexutil.Encode(data) - - flags := []string{ - "--bzzapi", srv.URL, - "--bzzaccount", pkFileName, - "feed", "update", - "--topic", topic.Hex(), - "--name", name, - hexData} - - // create an update and expect an exit without errors - log.Info("updating a feed with 'swarm feed update'") - cmd := runSwarm(t, flags...) - cmd.ExpectExit() - - // now try to get the update using the client - client := swarm.NewClient(srv.URL) - - // build the same topic as before, this time - // we use NewTopic to create a topic automatically. - topic, err := feed.NewTopic(name, subject) - if err != nil { - t.Fatal(err) - } - - // Feed configures whose updates we will be looking up. - fd := feed.Feed{ - Topic: topic, - User: address, - } - - // Build a query to get the latest update - query := feed.NewQueryLatest(&fd, lookup.NoClue) - - // retrieve content! - reader, err := client.QueryFeed(query, "") - if err != nil { - t.Fatal(err) - } - - retrieved, err := ioutil.ReadAll(reader) - if err != nil { - t.Fatal(err) - } - - // check we retrieved the sent information - if !bytes.Equal(data, retrieved) { - t.Fatalf("Received %s, expected %s", retrieved, data) - } - - // Now retrieve info for the next update - flags = []string{ - "--bzzapi", srv.URL, - "feed", "info", - "--topic", topic.Hex(), - "--user", address.Hex(), - } - - log.Info("getting feed info with 'swarm feed info'") - cmd = runSwarm(t, flags...) - _, matches := cmd.ExpectRegexp(`.*`) // regex hack to extract stdout - cmd.ExpectExit() - - // verify we can deserialize the result as a valid JSON - var request feed.Request - err = json.Unmarshal([]byte(matches[0]), &request) - if err != nil { - t.Fatal(err) - } - - // make sure the retrieved feed is the same - if request.Feed != fd { - t.Fatalf("Expected feed to be: %s, got %s", fd, request.Feed) - } - - // test publishing a manifest - flags = []string{ - "--bzzapi", srv.URL, - "--bzzaccount", pkFileName, - "feed", "create", - "--topic", topic.Hex(), - } - - log.Info("Publishing manifest with 'swarm feed create'") - cmd = runSwarm(t, flags...) - _, matches = cmd.ExpectRegexp(`[a-f\d]{64}`) - cmd.ExpectExit() - - manifestAddress := matches[0] // read the received feed manifest - - // now attempt to lookup the latest update using a manifest instead - reader, err = client.QueryFeed(nil, manifestAddress) - if err != nil { - t.Fatal(err) - } - - retrieved, err = ioutil.ReadAll(reader) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(data, retrieved) { - t.Fatalf("Received %s, expected %s", retrieved, data) - } - - // test publishing a manifest for a different user - flags = []string{ - "--bzzapi", srv.URL, - "feed", "create", - "--topic", topic.Hex(), - "--user", "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // different user - } - - log.Info("Publishing manifest with 'swarm feed create' for a different user") - cmd = runSwarm(t, flags...) - _, matches = cmd.ExpectRegexp(`[a-f\d]{64}`) - cmd.ExpectExit() - - manifestAddress = matches[0] // read the received feed manifest - - // now let's try to update that user's manifest which we don't have the private key for - flags = []string{ - "--bzzapi", srv.URL, - "--bzzaccount", pkFileName, - "feed", "update", - "--manifest", manifestAddress, - hexData} - - // create an update and expect an error given there is a user mismatch - log.Info("updating a feed with 'swarm feed update'") - cmd = runSwarm(t, flags...) - cmd.ExpectRegexp("Fatal:.*") // best way so far to detect a failure. - cmd.ExpectExit() - if cmd.ExitStatus() == 0 { - t.Fatal("Expected nonzero exit code when updating a manifest with the wrong user. Got 0.") - } -} diff --git a/cmd/swarm/flags.go b/cmd/swarm/flags.go deleted file mode 100644 index 6093149e3..000000000 --- a/cmd/swarm/flags.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -// Command feed allows the user to create and update signed Swarm feeds -package main - -import cli "gopkg.in/urfave/cli.v1" - -var ( - ChequebookAddrFlag = cli.StringFlag{ - Name: "chequebook", - Usage: "chequebook contract address", - EnvVar: SwarmEnvChequebookAddr, - } - SwarmAccountFlag = cli.StringFlag{ - Name: "bzzaccount", - Usage: "Swarm account key file", - EnvVar: SwarmEnvAccount, - } - SwarmListenAddrFlag = cli.StringFlag{ - Name: "httpaddr", - Usage: "Swarm HTTP API listening interface", - EnvVar: SwarmEnvListenAddr, - } - SwarmPortFlag = cli.StringFlag{ - Name: "bzzport", - Usage: "Swarm local http api port", - EnvVar: SwarmEnvPort, - } - SwarmNetworkIdFlag = cli.IntFlag{ - Name: "bzznetworkid", - Usage: "Network identifier (integer, default 3=swarm testnet)", - EnvVar: SwarmEnvNetworkID, - } - SwarmSwapEnabledFlag = cli.BoolFlag{ - Name: "swap", - Usage: "Swarm SWAP enabled (default false)", - EnvVar: SwarmEnvSwapEnable, - } - SwarmSwapAPIFlag = cli.StringFlag{ - Name: "swap-api", - Usage: "URL of the Ethereum API provider to use to settle SWAP payments", - EnvVar: SwarmEnvSwapAPI, - } - SwarmSyncDisabledFlag = cli.BoolTFlag{ - Name: "nosync", - Usage: "Disable swarm syncing", - EnvVar: SwarmEnvSyncDisable, - } - SwarmSyncUpdateDelay = cli.DurationFlag{ - Name: "sync-update-delay", - Usage: "Duration for sync subscriptions update after no new peers are added (default 15s)", - EnvVar: SwarmEnvSyncUpdateDelay, - } - SwarmMaxStreamPeerServersFlag = cli.IntFlag{ - Name: "max-stream-peer-servers", - Usage: "Limit of Stream peer servers, 0 denotes unlimited", - EnvVar: SwarmEnvMaxStreamPeerServers, - Value: 10000, // A very large default value is possible as stream servers have very small memory footprint - } - SwarmLightNodeEnabled = cli.BoolFlag{ - Name: "lightnode", - Usage: "Enable Swarm LightNode (default false)", - EnvVar: SwarmEnvLightNodeEnable, - } - SwarmDeliverySkipCheckFlag = cli.BoolFlag{ - Name: "delivery-skip-check", - Usage: "Skip chunk delivery check (default false)", - EnvVar: SwarmEnvDeliverySkipCheck, - } - EnsAPIFlag = cli.StringSliceFlag{ - Name: "ens-api", - Usage: "ENS API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url", - EnvVar: SwarmEnvENSAPI, - } - SwarmApiFlag = cli.StringFlag{ - Name: "bzzapi", - Usage: "Specifies the Swarm HTTP endpoint to connect to", - Value: "http://127.0.0.1:8500", - } - SwarmRecursiveFlag = cli.BoolFlag{ - Name: "recursive", - Usage: "Upload directories recursively", - } - SwarmWantManifestFlag = cli.BoolTFlag{ - Name: "manifest", - Usage: "Automatic manifest upload (default true)", - } - SwarmUploadDefaultPath = cli.StringFlag{ - Name: "defaultpath", - Usage: "path to file served for empty url path (none)", - } - SwarmAccessGrantKeyFlag = cli.StringFlag{ - Name: "grant-key", - Usage: "grants a given public key access to an ACT", - } - SwarmAccessGrantKeysFlag = cli.StringFlag{ - Name: "grant-keys", - Usage: "grants a given list of public keys in the following file (separated by line breaks) access to an ACT", - } - SwarmUpFromStdinFlag = cli.BoolFlag{ - Name: "stdin", - Usage: "reads data to be uploaded from stdin", - } - SwarmUploadMimeType = cli.StringFlag{ - Name: "mime", - Usage: "Manually specify MIME type", - } - SwarmEncryptedFlag = cli.BoolFlag{ - Name: "encrypt", - Usage: "use encrypted upload", - } - SwarmAccessPasswordFlag = cli.StringFlag{ - Name: "password", - Usage: "Password", - EnvVar: SwarmAccessPassword, - } - SwarmDryRunFlag = cli.BoolFlag{ - Name: "dry-run", - Usage: "dry-run", - } - CorsStringFlag = cli.StringFlag{ - Name: "corsdomain", - Usage: "Domain on which to send Access-Control-Allow-Origin header (multiple domains can be supplied separated by a ',')", - EnvVar: SwarmEnvCORS, - } - SwarmStorePath = cli.StringFlag{ - Name: "store.path", - Usage: "Path to leveldb chunk DB (default <$GETH_ENV_DIR>/swarm/bzz-<$BZZ_KEY>/chunks)", - EnvVar: SwarmEnvStorePath, - } - SwarmStoreCapacity = cli.Uint64Flag{ - Name: "store.size", - Usage: "Number of chunks (5M is roughly 20-25GB) (default 5000000)", - EnvVar: SwarmEnvStoreCapacity, - } - SwarmStoreCacheCapacity = cli.UintFlag{ - Name: "store.cache.size", - Usage: "Number of recent chunks cached in memory", - EnvVar: SwarmEnvStoreCacheCapacity, - Value: 10000, - } - SwarmCompressedFlag = cli.BoolFlag{ - Name: "compressed", - Usage: "Prints encryption keys in compressed form", - } - SwarmBootnodeModeFlag = cli.BoolFlag{ - Name: "bootnode-mode", - Usage: "Run Swarm in Bootnode mode", - } - SwarmFeedNameFlag = cli.StringFlag{ - Name: "name", - Usage: "User-defined name for the new feed, limited to 32 characters. If combined with topic, it will refer to a subtopic with this name", - } - SwarmFeedTopicFlag = cli.StringFlag{ - Name: "topic", - Usage: "User-defined topic this feed is tracking, hex encoded. Limited to 64 hexadecimal characters", - } - SwarmFeedManifestFlag = cli.StringFlag{ - Name: "manifest", - Usage: "Refers to the feed through a manifest", - } - SwarmFeedUserFlag = cli.StringFlag{ - Name: "user", - Usage: "Indicates the user who updates the feed", - } - SwarmGlobalStoreAPIFlag = cli.StringFlag{ - Name: "globalstore-api", - Usage: "URL of the Global Store API provider (only for testing)", - EnvVar: SwarmGlobalstoreAPI, - } - SwarmLegacyFlag = cli.BoolFlag{ - Name: "legacy", - Usage: "Use this flag when importing a db export from a legacy local store database dump (for schemas older than 'sanctuary')", - } -) diff --git a/cmd/swarm/fs.go b/cmd/swarm/fs.go deleted file mode 100644 index 7f156523b..000000000 --- a/cmd/swarm/fs.go +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "context" - "fmt" - "path/filepath" - "strings" - "time" - - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/swarm/fuse" - "gopkg.in/urfave/cli.v1" -) - -var fsCommand = cli.Command{ - Name: "fs", - CustomHelpTemplate: helpTemplate, - Usage: "perform FUSE operations", - ArgsUsage: "fs COMMAND", - Description: "Performs FUSE operations by mounting/unmounting/listing mount points. This assumes you already have a Swarm node running locally. For all operation you must reference the correct path to bzzd.ipc in order to communicate with the node", - Subcommands: []cli.Command{ - { - Action: mount, - CustomHelpTemplate: helpTemplate, - Name: "mount", - Usage: "mount a swarm hash to a mount point", - ArgsUsage: "swarm fs mount <manifest hash> <mount point>", - Description: "Mounts a Swarm manifest hash to a given mount point. This assumes you already have a Swarm node running locally. You must reference the correct path to your bzzd.ipc file", - }, - { - Action: unmount, - CustomHelpTemplate: helpTemplate, - Name: "unmount", - Usage: "unmount a swarmfs mount", - ArgsUsage: "swarm fs unmount <mount point>", - Description: "Unmounts a swarmfs mount residing at <mount point>. This assumes you already have a Swarm node running locally. You must reference the correct path to your bzzd.ipc file", - }, - { - Action: listMounts, - CustomHelpTemplate: helpTemplate, - Name: "list", - Usage: "list swarmfs mounts", - ArgsUsage: "swarm fs list", - Description: "Lists all mounted swarmfs volumes. This assumes you already have a Swarm node running locally. You must reference the correct path to your bzzd.ipc file", - }, - }, -} - -func mount(cliContext *cli.Context) { - args := cliContext.Args() - if len(args) < 2 { - utils.Fatalf("Usage: swarm fs mount <manifestHash> <file name>") - } - - client, err := dialRPC(cliContext) - if err != nil { - utils.Fatalf("had an error dailing to RPC endpoint: %v", err) - } - defer client.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - mf := &fuse.MountInfo{} - mountPoint, err := filepath.Abs(filepath.Clean(args[1])) - if err != nil { - utils.Fatalf("error expanding path for mount point: %v", err) - } - err = client.CallContext(ctx, mf, "swarmfs_mount", args[0], mountPoint) - if err != nil { - utils.Fatalf("had an error calling the RPC endpoint while mounting: %v", err) - } -} - -func unmount(cliContext *cli.Context) { - args := cliContext.Args() - - if len(args) < 1 { - utils.Fatalf("Usage: swarm fs unmount <mount path>") - } - client, err := dialRPC(cliContext) - if err != nil { - utils.Fatalf("had an error dailing to RPC endpoint: %v", err) - } - defer client.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - mf := fuse.MountInfo{} - err = client.CallContext(ctx, &mf, "swarmfs_unmount", args[0]) - if err != nil { - utils.Fatalf("encountered an error calling the RPC endpoint while unmounting: %v", err) - } - fmt.Printf("%s\n", mf.LatestManifest) //print the latest manifest hash for user reference -} - -func listMounts(cliContext *cli.Context) { - client, err := dialRPC(cliContext) - if err != nil { - utils.Fatalf("had an error dailing to RPC endpoint: %v", err) - } - defer client.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - var mf []fuse.MountInfo - err = client.CallContext(ctx, &mf, "swarmfs_listmounts") - if err != nil { - utils.Fatalf("encountered an error calling the RPC endpoint while listing mounts: %v", err) - } - if len(mf) == 0 { - fmt.Print("Could not found any swarmfs mounts. Please make sure you've specified the correct RPC endpoint\n") - } else { - fmt.Printf("Found %d swarmfs mount(s):\n", len(mf)) - for i, mountInfo := range mf { - fmt.Printf("%d:\n", i) - fmt.Printf("\tMount point: %s\n", mountInfo.MountPoint) - fmt.Printf("\tLatest Manifest: %s\n", mountInfo.LatestManifest) - fmt.Printf("\tStart Manifest: %s\n", mountInfo.StartManifest) - } - } -} - -func dialRPC(ctx *cli.Context) (*rpc.Client, error) { - endpoint := getIPCEndpoint(ctx) - log.Info("IPC endpoint", "path", endpoint) - return rpc.Dial(endpoint) -} - -func getIPCEndpoint(ctx *cli.Context) string { - cfg := defaultNodeConfig - utils.SetNodeConfig(ctx, &cfg) - - endpoint := cfg.IPCEndpoint() - - if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") { - // Backwards compatibility with geth < 1.5 which required - // these prefixes. - endpoint = endpoint[4:] - } - return endpoint -} diff --git a/cmd/swarm/fs_test.go b/cmd/swarm/fs_test.go deleted file mode 100644 index 5f58d6c0d..000000000 --- a/cmd/swarm/fs_test.go +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -// +build linux freebsd - -package main - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/log" -) - -type testFile struct { - filePath string - content string -} - -// TestCLISwarmFsDefaultIPCPath tests if the most basic fs command, i.e., list -// can find and correctly connect to a running Swarm node on the default -// IPCPath. -func TestCLISwarmFsDefaultIPCPath(t *testing.T) { - cluster := newTestCluster(t, 1) - defer cluster.Shutdown() - - handlingNode := cluster.Nodes[0] - list := runSwarm(t, []string{ - "--datadir", handlingNode.Dir, - "fs", - "list", - }...) - - list.WaitExit() - if list.Err != nil { - t.Fatal(list.Err) - } -} - -// TestCLISwarmFs is a high-level test of swarmfs -// -// This test fails on travis for macOS as this executable exits with code 1 -// and without any log messages in the log: -// /Library/Filesystems/osxfuse.fs/Contents/Resources/load_osxfuse. -// This is the reason for this file not being built on darwin architecture. -func TestCLISwarmFs(t *testing.T) { - cluster := newTestCluster(t, 3) - defer cluster.Shutdown() - - // create a tmp dir - mountPoint, err := ioutil.TempDir("", "swarm-test") - log.Debug("swarmfs cli test", "1st mount", mountPoint) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(mountPoint) - - handlingNode := cluster.Nodes[0] - mhash := doUploadEmptyDir(t, handlingNode) - log.Debug("swarmfs cli test: mounting first run", "ipc path", filepath.Join(handlingNode.Dir, handlingNode.IpcPath)) - - mount := runSwarm(t, []string{ - fmt.Sprintf("--%s", utils.IPCPathFlag.Name), filepath.Join(handlingNode.Dir, handlingNode.IpcPath), - "fs", - "mount", - mhash, - mountPoint, - }...) - mount.ExpectExit() - - filesToAssert := []*testFile{} - - dirPath, err := createDirInDir(mountPoint, "testSubDir") - if err != nil { - t.Fatal(err) - } - dirPath2, err := createDirInDir(dirPath, "AnotherTestSubDir") - if err != nil { - t.Fatal(err) - } - - dummyContent := "somerandomtestcontentthatshouldbeasserted" - dirs := []string{ - mountPoint, - dirPath, - dirPath2, - } - files := []string{"f1.tmp", "f2.tmp"} - for _, d := range dirs { - for _, entry := range files { - tFile, err := createTestFileInPath(d, entry, dummyContent) - if err != nil { - t.Fatal(err) - } - filesToAssert = append(filesToAssert, tFile) - } - } - if len(filesToAssert) != len(dirs)*len(files) { - t.Fatalf("should have %d files to assert now, got %d", len(dirs)*len(files), len(filesToAssert)) - } - hashRegexp := `[a-f\d]{64}` - log.Debug("swarmfs cli test: unmounting first run...", "ipc path", filepath.Join(handlingNode.Dir, handlingNode.IpcPath)) - - unmount := runSwarm(t, []string{ - fmt.Sprintf("--%s", utils.IPCPathFlag.Name), filepath.Join(handlingNode.Dir, handlingNode.IpcPath), - "fs", - "unmount", - mountPoint, - }...) - _, matches := unmount.ExpectRegexp(hashRegexp) - unmount.ExpectExit() - - hash := matches[0] - if hash == mhash { - t.Fatal("this should not be equal") - } - log.Debug("swarmfs cli test: asserting no files in mount point") - - //check that there's nothing in the mount folder - filesInDir, err := ioutil.ReadDir(mountPoint) - if err != nil { - t.Fatalf("had an error reading the directory: %v", err) - } - - if len(filesInDir) != 0 { - t.Fatal("there shouldn't be anything here") - } - - secondMountPoint, err := ioutil.TempDir("", "swarm-test") - log.Debug("swarmfs cli test", "2nd mount point at", secondMountPoint) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(secondMountPoint) - - log.Debug("swarmfs cli test: remounting at second mount point", "ipc path", filepath.Join(handlingNode.Dir, handlingNode.IpcPath)) - - //remount, check files - newMount := runSwarm(t, []string{ - fmt.Sprintf("--%s", utils.IPCPathFlag.Name), filepath.Join(handlingNode.Dir, handlingNode.IpcPath), - "fs", - "mount", - hash, // the latest hash - secondMountPoint, - }...) - - newMount.ExpectExit() - time.Sleep(1 * time.Second) - - filesInDir, err = ioutil.ReadDir(secondMountPoint) - if err != nil { - t.Fatal(err) - } - - if len(filesInDir) == 0 { - t.Fatal("there should be something here") - } - - log.Debug("swarmfs cli test: traversing file tree to see it matches previous mount") - - for _, file := range filesToAssert { - file.filePath = strings.Replace(file.filePath, mountPoint, secondMountPoint, -1) - fileBytes, err := ioutil.ReadFile(file.filePath) - - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(fileBytes, bytes.NewBufferString(file.content).Bytes()) { - t.Fatal("this should be equal") - } - } - - log.Debug("swarmfs cli test: unmounting second run", "ipc path", filepath.Join(handlingNode.Dir, handlingNode.IpcPath)) - - unmountSec := runSwarm(t, []string{ - fmt.Sprintf("--%s", utils.IPCPathFlag.Name), filepath.Join(handlingNode.Dir, handlingNode.IpcPath), - "fs", - "unmount", - secondMountPoint, - }...) - - _, matches = unmountSec.ExpectRegexp(hashRegexp) - unmountSec.ExpectExit() - - if matches[0] != hash { - t.Fatal("these should be equal - no changes made") - } -} - -func doUploadEmptyDir(t *testing.T, node *testNode) string { - // create a tmp dir - tmpDir, err := ioutil.TempDir("", "swarm-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - hashRegexp := `[a-f\d]{64}` - - flags := []string{ - "--bzzapi", node.URL, - "--recursive", - "up", - tmpDir} - - log.Info("swarmfs cli test: uploading dir with 'swarm up'") - up := runSwarm(t, flags...) - _, matches := up.ExpectRegexp(hashRegexp) - up.ExpectExit() - hash := matches[0] - log.Info("swarmfs cli test: dir uploaded", "hash", hash) - return hash -} - -func createDirInDir(createInDir string, dirToCreate string) (string, error) { - fullpath := filepath.Join(createInDir, dirToCreate) - err := os.MkdirAll(fullpath, 0777) - if err != nil { - return "", err - } - return fullpath, nil -} - -func createTestFileInPath(dir, filename, content string) (*testFile, error) { - tFile := &testFile{} - filePath := filepath.Join(dir, filename) - if file, err := os.Create(filePath); err == nil { - tFile.content = content - tFile.filePath = filePath - - _, err = io.WriteString(file, content) - if err != nil { - return nil, err - } - file.Close() - } - - return tFile, nil -} diff --git a/cmd/swarm/global-store/explorer.go b/cmd/swarm/global-store/explorer.go deleted file mode 100644 index 634ff1ebb..000000000 --- a/cmd/swarm/global-store/explorer.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2019 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "context" - "fmt" - "net" - "net/http" - "time" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/swarm/storage/mock" - "github.com/ethereum/go-ethereum/swarm/storage/mock/explorer" - cli "gopkg.in/urfave/cli.v1" -) - -// serveChunkExplorer starts an http server in background with chunk explorer handler -// using the provided global store. Server is started if the returned shutdown function -// is not nil. -func serveChunkExplorer(ctx *cli.Context, globalStore mock.GlobalStorer) (shutdown func(), err error) { - if !ctx.IsSet("explorer-address") { - return nil, nil - } - - corsOrigins := ctx.StringSlice("explorer-cors-origin") - server := &http.Server{ - Handler: explorer.NewHandler(globalStore, corsOrigins), - IdleTimeout: 30 * time.Minute, - ReadTimeout: 2 * time.Minute, - WriteTimeout: 2 * time.Minute, - } - listener, err := net.Listen("tcp", ctx.String("explorer-address")) - if err != nil { - return nil, fmt.Errorf("explorer: %v", err) - } - log.Info("chunk explorer http", "address", listener.Addr().String(), "origins", corsOrigins) - - go func() { - if err := server.Serve(listener); err != nil { - log.Error("chunk explorer", "err", err) - } - }() - - return func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := server.Shutdown(ctx); err != nil { - log.Error("chunk explorer: shutdown", "err", err) - } - }, nil -} diff --git a/cmd/swarm/global-store/explorer_test.go b/cmd/swarm/global-store/explorer_test.go deleted file mode 100644 index 2e4928c8f..000000000 --- a/cmd/swarm/global-store/explorer_test.go +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright 2019 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "encoding/json" - "fmt" - "net/http" - "sort" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/swarm/storage/mock/explorer" - mockRPC "github.com/ethereum/go-ethereum/swarm/storage/mock/rpc" -) - -// TestExplorer validates basic chunk explorer functionality by storing -// a small set of chunk and making http requests on exposed endpoint. -// Full chunk explorer validation is done in mock/explorer package. -func TestExplorer(t *testing.T) { - addr := findFreeTCPAddress(t) - explorerAddr := findFreeTCPAddress(t) - testCmd := runGlobalStore(t, "ws", "--addr", addr, "--explorer-address", explorerAddr) - defer testCmd.Kill() - - client := websocketClient(t, addr) - - store := mockRPC.NewGlobalStore(client) - defer store.Close() - - nodeKeys := map[string][]string{ - "a1": {"b1", "b2", "b3"}, - "a2": {"b3", "b4", "b5"}, - } - - keyNodes := make(map[string][]string) - - for addr, keys := range nodeKeys { - for _, key := range keys { - keyNodes[key] = append(keyNodes[key], addr) - } - } - - invalidAddr := "c1" - invalidKey := "d1" - - for addr, keys := range nodeKeys { - for _, key := range keys { - err := store.Put(common.HexToAddress(addr), common.Hex2Bytes(key), []byte("data")) - if err != nil { - t.Fatal(err) - } - } - } - - endpoint := "http://" + explorerAddr - - t.Run("has key", func(t *testing.T) { - for addr, keys := range nodeKeys { - for _, key := range keys { - testStatusResponse(t, endpoint+"/api/has-key/"+addr+"/"+key, http.StatusOK) - testStatusResponse(t, endpoint+"/api/has-key/"+invalidAddr+"/"+key, http.StatusNotFound) - } - testStatusResponse(t, endpoint+"/api/has-key/"+addr+"/"+invalidKey, http.StatusNotFound) - } - testStatusResponse(t, endpoint+"/api/has-key/"+invalidAddr+"/"+invalidKey, http.StatusNotFound) - }) - - t.Run("keys", func(t *testing.T) { - var keys []string - for key := range keyNodes { - keys = append(keys, key) - } - sort.Strings(keys) - testKeysResponse(t, endpoint+"/api/keys", explorer.KeysResponse{ - Keys: keys, - }) - }) - - t.Run("nodes", func(t *testing.T) { - var nodes []string - for addr := range nodeKeys { - nodes = append(nodes, common.HexToAddress(addr).Hex()) - } - sort.Strings(nodes) - testNodesResponse(t, endpoint+"/api/nodes", explorer.NodesResponse{ - Nodes: nodes, - }) - }) - - t.Run("node keys", func(t *testing.T) { - for addr, keys := range nodeKeys { - testKeysResponse(t, endpoint+"/api/keys?node="+addr, explorer.KeysResponse{ - Keys: keys, - }) - } - testKeysResponse(t, endpoint+"/api/keys?node="+invalidAddr, explorer.KeysResponse{}) - }) - - t.Run("key nodes", func(t *testing.T) { - for key, addrs := range keyNodes { - var nodes []string - for _, addr := range addrs { - nodes = append(nodes, common.HexToAddress(addr).Hex()) - } - sort.Strings(nodes) - testNodesResponse(t, endpoint+"/api/nodes?key="+key, explorer.NodesResponse{ - Nodes: nodes, - }) - } - testNodesResponse(t, endpoint+"/api/nodes?key="+invalidKey, explorer.NodesResponse{}) - }) -} - -// TestExplorer_CORSOrigin validates if chunk explorer returns -// correct CORS origin header in GET and OPTIONS requests. -func TestExplorer_CORSOrigin(t *testing.T) { - origin := "http://localhost/" - addr := findFreeTCPAddress(t) - explorerAddr := findFreeTCPAddress(t) - testCmd := runGlobalStore(t, "ws", - "--addr", addr, - "--explorer-address", explorerAddr, - "--explorer-cors-origin", origin, - ) - defer testCmd.Kill() - - // wait until the server is started - waitHTTPEndpoint(t, explorerAddr) - - url := "http://" + explorerAddr + "/api/keys" - - t.Run("get", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - t.Fatal(err) - } - req.Header.Set("Origin", origin) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - header := resp.Header.Get("Access-Control-Allow-Origin") - if header != origin { - t.Errorf("got Access-Control-Allow-Origin header %q, want %q", header, origin) - } - }) - - t.Run("preflight", func(t *testing.T) { - req, err := http.NewRequest(http.MethodOptions, url, nil) - if err != nil { - t.Fatal(err) - } - req.Header.Set("Origin", origin) - req.Header.Set("Access-Control-Request-Method", "GET") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - header := resp.Header.Get("Access-Control-Allow-Origin") - if header != origin { - t.Errorf("got Access-Control-Allow-Origin header %q, want %q", header, origin) - } - }) -} - -// testStatusResponse makes an http request to provided url -// and validates if response is explorer.StatusResponse for -// the expected status code. -func testStatusResponse(t *testing.T, url string, code int) { - t.Helper() - - resp, err := http.Get(url) - if err != nil { - t.Fatal(err) - } - if resp.StatusCode != code { - t.Errorf("got status code %v, want %v", resp.StatusCode, code) - } - var r explorer.StatusResponse - if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { - t.Fatal(err) - } - if r.Code != code { - t.Errorf("got response code %v, want %v", r.Code, code) - } - if r.Message != http.StatusText(code) { - t.Errorf("got response message %q, want %q", r.Message, http.StatusText(code)) - } -} - -// testKeysResponse makes an http request to provided url -// and validates if response machhes expected explorer.KeysResponse. -func testKeysResponse(t *testing.T, url string, want explorer.KeysResponse) { - t.Helper() - - resp, err := http.Get(url) - if err != nil { - t.Fatal(err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("got status code %v, want %v", resp.StatusCode, http.StatusOK) - } - var r explorer.KeysResponse - if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { - t.Fatal(err) - } - if fmt.Sprint(r.Keys) != fmt.Sprint(want.Keys) { - t.Errorf("got keys %v, want %v", r.Keys, want.Keys) - } - if r.Next != want.Next { - t.Errorf("got next %s, want %s", r.Next, want.Next) - } -} - -// testNodeResponse makes an http request to provided url -// and validates if response machhes expected explorer.NodeResponse. -func testNodesResponse(t *testing.T, url string, want explorer.NodesResponse) { - t.Helper() - - resp, err := http.Get(url) - if err != nil { - t.Fatal(err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("got status code %v, want %v", resp.StatusCode, http.StatusOK) - } - var r explorer.NodesResponse - if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { - t.Fatal(err) - } - if fmt.Sprint(r.Nodes) != fmt.Sprint(want.Nodes) { - t.Errorf("got nodes %v, want %v", r.Nodes, want.Nodes) - } - if r.Next != want.Next { - t.Errorf("got next %s, want %s", r.Next, want.Next) - } -} diff --git a/cmd/swarm/global-store/global_store.go b/cmd/swarm/global-store/global_store.go deleted file mode 100644 index f93b464db..000000000 --- a/cmd/swarm/global-store/global_store.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2019 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "io" - "net" - "net/http" - "os" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/swarm/storage/mock" - "github.com/ethereum/go-ethereum/swarm/storage/mock/db" - "github.com/ethereum/go-ethereum/swarm/storage/mock/mem" - cli "gopkg.in/urfave/cli.v1" -) - -// startHTTP starts a global store with HTTP RPC server. -// It is used for "http" cli command. -func startHTTP(ctx *cli.Context) (err error) { - server, cleanup, err := newServer(ctx) - if err != nil { - return err - } - defer cleanup() - - listener, err := net.Listen("tcp", ctx.String("addr")) - if err != nil { - return err - } - log.Info("http", "address", listener.Addr().String()) - - return http.Serve(listener, server) -} - -// startWS starts a global store with WebSocket RPC server. -// It is used for "websocket" cli command. -func startWS(ctx *cli.Context) (err error) { - server, cleanup, err := newServer(ctx) - if err != nil { - return err - } - defer cleanup() - - listener, err := net.Listen("tcp", ctx.String("addr")) - if err != nil { - return err - } - origins := ctx.StringSlice("origins") - log.Info("websocket", "address", listener.Addr().String(), "origins", origins) - - return http.Serve(listener, server.WebsocketHandler(origins)) -} - -// newServer creates a global store and starts a chunk explorer server if configured. -// Returned cleanup function should be called only if err is nil. -func newServer(ctx *cli.Context) (server *rpc.Server, cleanup func(), err error) { - log.PrintOrigins(true) - log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(ctx.Int("verbosity")), log.StreamHandler(os.Stdout, log.TerminalFormat(false)))) - - cleanup = func() {} - var globalStore mock.GlobalStorer - dir := ctx.String("dir") - if dir != "" { - dbStore, err := db.NewGlobalStore(dir) - if err != nil { - return nil, nil, err - } - cleanup = func() { - if err := dbStore.Close(); err != nil { - log.Error("global store: close", "err", err) - } - } - globalStore = dbStore - log.Info("database global store", "dir", dir) - } else { - globalStore = mem.NewGlobalStore() - log.Info("in-memory global store") - } - - server = rpc.NewServer() - if err := server.RegisterName("mockStore", globalStore); err != nil { - cleanup() - return nil, nil, err - } - - shutdown, err := serveChunkExplorer(ctx, globalStore) - if err != nil { - cleanup() - return nil, nil, err - } - if shutdown != nil { - cleanup = func() { - shutdown() - - if c, ok := globalStore.(io.Closer); ok { - if err := c.Close(); err != nil { - log.Error("global store: close", "err", err) - } - } - } - } - - return server, cleanup, nil -} diff --git a/cmd/swarm/global-store/global_store_test.go b/cmd/swarm/global-store/global_store_test.go deleted file mode 100644 index c437c9d45..000000000 --- a/cmd/swarm/global-store/global_store_test.go +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2019 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "context" - "io/ioutil" - "net" - "net/http" - "os" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/rpc" - mockRPC "github.com/ethereum/go-ethereum/swarm/storage/mock/rpc" -) - -// TestHTTP_InMemory tests in-memory global store that exposes -// HTTP server. -func TestHTTP_InMemory(t *testing.T) { - testHTTP(t, true) -} - -// TestHTTP_Database tests global store with persisted database -// that exposes HTTP server. -func TestHTTP_Database(t *testing.T) { - dir, err := ioutil.TempDir("", "swarm-global-store-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) - - // create a fresh global store - testHTTP(t, true, "--dir", dir) - - // check if data saved by the previous global store instance - testHTTP(t, false, "--dir", dir) -} - -// testWebsocket starts global store binary with HTTP server -// and validates that it can store and retrieve data. -// If put is false, no data will be stored, only retrieved, -// giving the possibility to check if data is present in the -// storage directory. -func testHTTP(t *testing.T, put bool, args ...string) { - addr := findFreeTCPAddress(t) - testCmd := runGlobalStore(t, append([]string{"http", "--addr", addr}, args...)...) - defer testCmd.Kill() - - client, err := rpc.DialHTTP("http://" + addr) - if err != nil { - t.Fatal(err) - } - - // wait until global store process is started as - // rpc.DialHTTP is actually not connecting - waitHTTPEndpoint(t, addr) - - store := mockRPC.NewGlobalStore(client) - defer store.Close() - - node := store.NewNodeStore(common.HexToAddress("123abc")) - - wantKey := "key" - wantValue := "value" - - if put { - err = node.Put([]byte(wantKey), []byte(wantValue)) - if err != nil { - t.Fatal(err) - } - } - - gotValue, err := node.Get([]byte(wantKey)) - if err != nil { - t.Fatal(err) - } - - if string(gotValue) != wantValue { - t.Errorf("got value %s for key %s, want %s", string(gotValue), wantKey, wantValue) - } -} - -// TestWebsocket_InMemory tests in-memory global store that exposes -// WebSocket server. -func TestWebsocket_InMemory(t *testing.T) { - testWebsocket(t, true) -} - -// TestWebsocket_Database tests global store with persisted database -// that exposes HTTP server. -func TestWebsocket_Database(t *testing.T) { - dir, err := ioutil.TempDir("", "swarm-global-store-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) - - // create a fresh global store - testWebsocket(t, true, "--dir", dir) - - // check if data saved by the previous global store instance - testWebsocket(t, false, "--dir", dir) -} - -// testWebsocket starts global store binary with WebSocket server -// and validates that it can store and retrieve data. -// If put is false, no data will be stored, only retrieved, -// giving the possibility to check if data is present in the -// storage directory. -func testWebsocket(t *testing.T, put bool, args ...string) { - addr := findFreeTCPAddress(t) - testCmd := runGlobalStore(t, append([]string{"ws", "--addr", addr}, args...)...) - defer testCmd.Kill() - - client := websocketClient(t, addr) - - store := mockRPC.NewGlobalStore(client) - defer store.Close() - - node := store.NewNodeStore(common.HexToAddress("123abc")) - - wantKey := "key" - wantValue := "value" - - if put { - err := node.Put([]byte(wantKey), []byte(wantValue)) - if err != nil { - t.Fatal(err) - } - } - - gotValue, err := node.Get([]byte(wantKey)) - if err != nil { - t.Fatal(err) - } - - if string(gotValue) != wantValue { - t.Errorf("got value %s for key %s, want %s", string(gotValue), wantKey, wantValue) - } -} - -// findFreeTCPAddress returns a local address (IP:Port) to which -// global store can listen on. -func findFreeTCPAddress(t *testing.T) (addr string) { - t.Helper() - - listener, err := net.Listen("tcp", "") - if err != nil { - t.Fatal(err) - } - defer listener.Close() - - return listener.Addr().String() -} - -// websocketClient waits until global store process is started -// and returns rpc client. -func websocketClient(t *testing.T, addr string) (client *rpc.Client) { - t.Helper() - - var err error - for i := 0; i < 1000; i++ { - client, err = rpc.DialWebsocket(context.Background(), "ws://"+addr, "") - if err == nil { - break - } - time.Sleep(10 * time.Millisecond) - } - if err != nil { - t.Fatal(err) - } - return client -} - -// waitHTTPEndpoint retries http requests to a provided -// address until the connection is established. -func waitHTTPEndpoint(t *testing.T, addr string) { - t.Helper() - - var err error - for i := 0; i < 1000; i++ { - _, err = http.Get("http://" + addr) - if err == nil { - break - } - time.Sleep(10 * time.Millisecond) - } - if err != nil { - t.Fatal(err) - } -} diff --git a/cmd/swarm/global-store/main.go b/cmd/swarm/global-store/main.go deleted file mode 100644 index 58505f988..000000000 --- a/cmd/swarm/global-store/main.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2019 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "os" - - "github.com/ethereum/go-ethereum/log" - cli "gopkg.in/urfave/cli.v1" -) - -var ( - version = "0.1" - gitCommit string // Git SHA1 commit hash of the release (set via linker flags) - gitDate string -) - -func main() { - err := newApp().Run(os.Args) - if err != nil { - log.Error(err.Error()) - os.Exit(1) - } -} - -// newApp construct a new instance of Swarm Global Store. -// Method Run is called on it in the main function and in tests. -func newApp() (app *cli.App) { - app = cli.NewApp() - app.Name = "global-store" - app.Version = version - if len(gitCommit) >= 8 { - app.Version += "-" + gitCommit[:8] - } - if gitDate != "" { - app.Version += "-" + gitDate - } - app.Usage = "Swarm Global Store" - - // app flags (for all commands) - app.Flags = []cli.Flag{ - cli.IntFlag{ - Name: "verbosity", - Value: 3, - Usage: "Verbosity level.", - }, - cli.StringFlag{ - Name: "explorer-address", - Value: "", - Usage: "Chunk explorer HTTP listener address.", - }, - cli.StringSliceFlag{ - Name: "explorer-cors-origin", - Value: nil, - Usage: "Chunk explorer CORS origin (can be specified multiple times).", - }, - } - - app.Commands = []cli.Command{ - { - Name: "http", - Aliases: []string{"h"}, - Usage: "Start swarm global store with HTTP server.", - Action: startHTTP, - // Flags only for "start" command. - // Allow app flags to be specified after the - // command argument. - Flags: append(app.Flags, - cli.StringFlag{ - Name: "dir", - Value: "", - Usage: "Data directory.", - }, - cli.StringFlag{ - Name: "addr", - Value: "0.0.0.0:3033", - Usage: "Address to listen for HTTP connections.", - }, - ), - }, - { - Name: "websocket", - Aliases: []string{"ws"}, - Usage: "Start swarm global store with WebSocket server.", - Action: startWS, - // Flags only for "start" command. - // Allow app flags to be specified after the - // command argument. - Flags: append(app.Flags, - cli.StringFlag{ - Name: "dir", - Value: "", - Usage: "Data directory.", - }, - cli.StringFlag{ - Name: "addr", - Value: "0.0.0.0:3033", - Usage: "Address to listen for WebSocket connections.", - }, - cli.StringSliceFlag{ - Name: "origin", - Value: nil, - Usage: "WebSocket CORS origin (can be specified multiple times).", - }, - ), - }, - } - - return app -} diff --git a/cmd/swarm/global-store/run_test.go b/cmd/swarm/global-store/run_test.go deleted file mode 100644 index d7ef626e5..000000000 --- a/cmd/swarm/global-store/run_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2019 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "fmt" - "os" - "testing" - - "github.com/docker/docker/pkg/reexec" - "github.com/ethereum/go-ethereum/internal/cmdtest" -) - -func init() { - reexec.Register("swarm-global-store", func() { - if err := newApp().Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - os.Exit(0) - }) -} - -func runGlobalStore(t *testing.T, args ...string) *cmdtest.TestCmd { - tt := cmdtest.NewTestCmd(t, nil) - tt.Run("swarm-global-store", args...) - return tt -} - -func TestMain(m *testing.M) { - if reexec.Init() { - return - } - os.Exit(m.Run()) -} diff --git a/cmd/swarm/hash.go b/cmd/swarm/hash.go deleted file mode 100644 index ff786fa10..000000000 --- a/cmd/swarm/hash.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -// Command bzzhash computes a swarm tree hash. -package main - -import ( - "context" - "encoding/hex" - "fmt" - "os" - - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/contracts/ens" - "github.com/ethereum/go-ethereum/swarm/chunk" - "github.com/ethereum/go-ethereum/swarm/storage" - "gopkg.in/urfave/cli.v1" -) - -var hashCommand = cli.Command{ - Action: hash, - CustomHelpTemplate: helpTemplate, - Name: "hash", - Usage: "print the swarm hash of a file or directory", - ArgsUsage: "<file>", - Description: "Prints the swarm hash of file or directory", - Subcommands: []cli.Command{ - { - CustomHelpTemplate: helpTemplate, - Name: "ens", - Usage: "converts a swarm hash to an ens EIP1577 compatible CIDv1 hash", - ArgsUsage: "<ref>", - Description: "", - Subcommands: []cli.Command{ - { - Action: encodeEipHash, - CustomHelpTemplate: helpTemplate, - Name: "contenthash", - Usage: "converts a swarm hash to an ens EIP1577 compatible CIDv1 hash", - ArgsUsage: "<ref>", - Description: "", - }, - { - Action: ensNodeHash, - CustomHelpTemplate: helpTemplate, - Name: "node", - Usage: "converts an ens name to an ENS node hash", - ArgsUsage: "<ref>", - Description: "", - }, - }, - }, - }} - -func hash(ctx *cli.Context) { - args := ctx.Args() - if len(args) < 1 { - utils.Fatalf("Usage: swarm hash <file name>") - } - f, err := os.Open(args[0]) - if err != nil { - utils.Fatalf("Error opening file " + args[1]) - } - defer f.Close() - - stat, _ := f.Stat() - fileStore := storage.NewFileStore(&storage.FakeChunkStore{}, storage.NewFileStoreParams(), chunk.NewTags()) - addr, _, err := fileStore.Store(context.TODO(), f, stat.Size(), false) - if err != nil { - utils.Fatalf("%v\n", err) - } else { - fmt.Printf("%v\n", addr) - } -} -func ensNodeHash(ctx *cli.Context) { - args := ctx.Args() - if len(args) < 1 { - utils.Fatalf("Usage: swarm hash ens node <ens name>") - } - ensName := args[0] - - hash := ens.EnsNode(ensName) - - stringHex := hex.EncodeToString(hash[:]) - fmt.Println(stringHex) -} -func encodeEipHash(ctx *cli.Context) { - args := ctx.Args() - if len(args) < 1 { - utils.Fatalf("Usage: swarm hash ens <swarm hash>") - } - swarmHash := args[0] - - hash := common.HexToHash(swarmHash) - ensHash, err := ens.EncodeSwarmHash(hash) - if err != nil { - utils.Fatalf("error converting swarm hash", err) - } - - stringHex := hex.EncodeToString(ensHash) - fmt.Println(stringHex) -} diff --git a/cmd/swarm/list.go b/cmd/swarm/list.go deleted file mode 100644 index 5d35154a5..000000000 --- a/cmd/swarm/list.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "fmt" - "os" - "strings" - "text/tabwriter" - - "github.com/ethereum/go-ethereum/cmd/utils" - swarm "github.com/ethereum/go-ethereum/swarm/api/client" - "gopkg.in/urfave/cli.v1" -) - -var listCommand = cli.Command{ - Action: list, - CustomHelpTemplate: helpTemplate, - Name: "ls", - Usage: "list files and directories contained in a manifest", - ArgsUsage: "<manifest> [<prefix>]", - Description: "Lists files and directories contained in a manifest", -} - -func list(ctx *cli.Context) { - args := ctx.Args() - - if len(args) < 1 { - utils.Fatalf("Please supply a manifest reference as the first argument") - } else if len(args) > 2 { - utils.Fatalf("Too many arguments - usage 'swarm ls manifest [prefix]'") - } - manifest := args[0] - - var prefix string - if len(args) == 2 { - prefix = args[1] - } - - bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") - client := swarm.NewClient(bzzapi) - list, err := client.List(manifest, prefix, "") - if err != nil { - utils.Fatalf("Failed to generate file and directory list: %s", err) - } - - w := tabwriter.NewWriter(os.Stdout, 1, 2, 2, ' ', 0) - defer w.Flush() - fmt.Fprintln(w, "HASH\tCONTENT TYPE\tPATH") - for _, prefix := range list.CommonPrefixes { - fmt.Fprintf(w, "%s\t%s\t%s\n", "", "DIR", prefix) - } - for _, entry := range list.Entries { - fmt.Fprintf(w, "%s\t%s\t%s\n", entry.Hash, entry.ContentType, entry.Path) - } -} diff --git a/cmd/swarm/main.go b/cmd/swarm/main.go deleted file mode 100644 index f4c1dd688..000000000 --- a/cmd/swarm/main.go +++ /dev/null @@ -1,475 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "crypto/ecdsa" - "encoding/hex" - "fmt" - "io/ioutil" - "os" - "os/signal" - "runtime" - "sort" - "strconv" - "strings" - "syscall" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/console" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/internal/debug" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/swarm" - bzzapi "github.com/ethereum/go-ethereum/swarm/api" - swarmmetrics "github.com/ethereum/go-ethereum/swarm/metrics" - "github.com/ethereum/go-ethereum/swarm/storage/mock" - mockrpc "github.com/ethereum/go-ethereum/swarm/storage/mock/rpc" - "github.com/ethereum/go-ethereum/swarm/tracing" - sv "github.com/ethereum/go-ethereum/swarm/version" - - cli "gopkg.in/urfave/cli.v1" -) - -const clientIdentifier = "swarm" -const helpTemplate = `NAME: -{{.HelpName}} - {{.Usage}} - -USAGE: -{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} - -CATEGORY: -{{.Category}}{{end}}{{if .Description}} - -DESCRIPTION: -{{.Description}}{{end}}{{if .VisibleFlags}} - -OPTIONS: -{{range .VisibleFlags}}{{.}} -{{end}}{{end}} -` - -// Git SHA1 commit hash of the release (set via linker flags) -// this variable will be assigned if corresponding parameter is passed with install, but not with test -// e.g.: go install -ldflags "-X main.gitCommit=ed1312d01b19e04ef578946226e5d8069d5dfd5a" ./cmd/swarm -var gitCommit string - -//declare a few constant error messages, useful for later error check comparisons in test -var ( - SwarmErrNoBZZAccount = "bzzaccount option is required but not set; check your config file, command line or environment variables" - SwarmErrSwapSetNoAPI = "SWAP is enabled but --swap-api is not set" -) - -// this help command gets added to any subcommand that does not define it explicitly -var defaultSubcommandHelp = cli.Command{ - Action: func(ctx *cli.Context) { cli.ShowCommandHelpAndExit(ctx, "", 1) }, - CustomHelpTemplate: helpTemplate, - Name: "help", - Usage: "shows this help", - Hidden: true, -} - -var defaultNodeConfig = node.DefaultConfig - -// This init function sets defaults so cmd/swarm can run alongside geth. -func init() { - sv.GitCommit = gitCommit - defaultNodeConfig.Name = clientIdentifier - defaultNodeConfig.Version = sv.VersionWithCommit(gitCommit) - defaultNodeConfig.P2P.ListenAddr = ":30399" - defaultNodeConfig.IPCPath = "bzzd.ipc" - // Set flag defaults for --help display. - utils.ListenPortFlag.Value = 30399 -} - -var app = utils.NewApp("", "", "Ethereum Swarm") - -// This init function creates the cli.App. -func init() { - app.Action = bzzd - app.Version = sv.ArchiveVersion(gitCommit) - app.Copyright = "Copyright 2013-2016 The go-ethereum Authors" - app.Commands = []cli.Command{ - { - Action: version, - CustomHelpTemplate: helpTemplate, - Name: "version", - Usage: "Print version numbers", - Description: "The output of this command is supposed to be machine-readable", - }, - { - Action: keys, - CustomHelpTemplate: helpTemplate, - Name: "print-keys", - Flags: []cli.Flag{SwarmCompressedFlag}, - Usage: "Print public key information", - Description: "The output of this command is supposed to be machine-readable", - }, - // See upload.go - upCommand, - // See access.go - accessCommand, - // See feeds.go - feedCommand, - // See list.go - listCommand, - // See hash.go - hashCommand, - // See download.go - downloadCommand, - // See manifest.go - manifestCommand, - // See fs.go - fsCommand, - // See db.go - dbCommand, - // See config.go - DumpConfigCommand, - // hashesCommand - hashesCommand, - } - - // append a hidden help subcommand to all commands that have subcommands - // if a help command was already defined above, that one will take precedence. - addDefaultHelpSubcommands(app.Commands) - - sort.Sort(cli.CommandsByName(app.Commands)) - - app.Flags = []cli.Flag{ - utils.IdentityFlag, - utils.DataDirFlag, - utils.BootnodesFlag, - utils.KeyStoreDirFlag, - utils.ListenPortFlag, - utils.DiscoveryV5Flag, - utils.NetrestrictFlag, - utils.NodeKeyFileFlag, - utils.NodeKeyHexFlag, - utils.MaxPeersFlag, - utils.NATFlag, - utils.IPCDisabledFlag, - utils.IPCPathFlag, - utils.PasswordFileFlag, - // bzzd-specific flags - CorsStringFlag, - EnsAPIFlag, - SwarmTomlConfigPathFlag, - SwarmSwapEnabledFlag, - SwarmSwapAPIFlag, - SwarmSyncDisabledFlag, - SwarmSyncUpdateDelay, - SwarmMaxStreamPeerServersFlag, - SwarmLightNodeEnabled, - SwarmDeliverySkipCheckFlag, - SwarmListenAddrFlag, - SwarmPortFlag, - SwarmAccountFlag, - SwarmNetworkIdFlag, - ChequebookAddrFlag, - // upload flags - SwarmApiFlag, - SwarmRecursiveFlag, - SwarmWantManifestFlag, - SwarmUploadDefaultPath, - SwarmUpFromStdinFlag, - SwarmUploadMimeType, - // bootnode mode - SwarmBootnodeModeFlag, - // storage flags - SwarmStorePath, - SwarmStoreCapacity, - SwarmStoreCacheCapacity, - SwarmGlobalStoreAPIFlag, - } - rpcFlags := []cli.Flag{ - utils.WSEnabledFlag, - utils.WSListenAddrFlag, - utils.WSPortFlag, - utils.WSApiFlag, - utils.WSAllowedOriginsFlag, - } - app.Flags = append(app.Flags, rpcFlags...) - app.Flags = append(app.Flags, debug.Flags...) - app.Flags = append(app.Flags, swarmmetrics.Flags...) - app.Flags = append(app.Flags, tracing.Flags...) - app.Before = func(ctx *cli.Context) error { - runtime.GOMAXPROCS(runtime.NumCPU()) - if err := debug.Setup(ctx, ""); err != nil { - return err - } - swarmmetrics.Setup(ctx) - tracing.Setup(ctx) - return nil - } - app.After = func(ctx *cli.Context) error { - debug.Exit() - return nil - } -} - -func main() { - if err := app.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func keys(ctx *cli.Context) error { - privateKey := getPrivKey(ctx) - pubkey := crypto.FromECDSAPub(&privateKey.PublicKey) - pubkeyhex := hex.EncodeToString(pubkey) - pubCompressed := hex.EncodeToString(crypto.CompressPubkey(&privateKey.PublicKey)) - bzzkey := crypto.Keccak256Hash(pubkey).Hex() - - if !ctx.Bool(SwarmCompressedFlag.Name) { - fmt.Println(fmt.Sprintf("bzzkey=%s", bzzkey[2:])) - fmt.Println(fmt.Sprintf("publicKey=%s", pubkeyhex)) - } - fmt.Println(fmt.Sprintf("publicKeyCompressed=%s", pubCompressed)) - - return nil -} - -func version(ctx *cli.Context) error { - fmt.Println(strings.Title(clientIdentifier)) - fmt.Println("Version:", sv.VersionWithMeta) - if gitCommit != "" { - fmt.Println("Git Commit:", gitCommit) - } - fmt.Println("Go Version:", runtime.Version()) - fmt.Println("OS:", runtime.GOOS) - return nil -} - -func bzzd(ctx *cli.Context) error { - //build a valid bzzapi.Config from all available sources: - //default config, file config, command line and env vars - - bzzconfig, err := buildConfig(ctx) - if err != nil { - utils.Fatalf("unable to configure swarm: %v", err) - } - - cfg := defaultNodeConfig - - //pss operates on ws - cfg.WSModules = append(cfg.WSModules, "pss") - - //geth only supports --datadir via command line - //in order to be consistent within swarm, if we pass --datadir via environment variable - //or via config file, we get the same directory for geth and swarm - if _, err := os.Stat(bzzconfig.Path); err == nil { - cfg.DataDir = bzzconfig.Path - } - - //optionally set the bootnodes before configuring the node - setSwarmBootstrapNodes(ctx, &cfg) - //setup the ethereum node - utils.SetNodeConfig(ctx, &cfg) - - //disable dynamic dialing from p2p/discovery - cfg.P2P.NoDial = true - - stack, err := node.New(&cfg) - if err != nil { - utils.Fatalf("can't create node: %v", err) - } - defer stack.Close() - - //a few steps need to be done after the config phase is completed, - //due to overriding behavior - err = initSwarmNode(bzzconfig, stack, ctx, &cfg) - if err != nil { - return err - } - //register BZZ as node.Service in the ethereum node - registerBzzService(bzzconfig, stack) - //start the node - utils.StartNode(stack) - - go func() { - sigc := make(chan os.Signal, 1) - signal.Notify(sigc, syscall.SIGTERM) - defer signal.Stop(sigc) - <-sigc - log.Info("Got sigterm, shutting swarm down...") - stack.Stop() - }() - - // add swarm bootnodes, because swarm doesn't use p2p package's discovery discv5 - go func() { - s := stack.Server() - - for _, n := range cfg.P2P.BootstrapNodes { - s.AddPeer(n) - } - }() - - stack.Wait() - return nil -} - -func registerBzzService(bzzconfig *bzzapi.Config, stack *node.Node) { - //define the swarm service boot function - boot := func(_ *node.ServiceContext) (node.Service, error) { - var nodeStore *mock.NodeStore - if bzzconfig.GlobalStoreAPI != "" { - // connect to global store - client, err := rpc.Dial(bzzconfig.GlobalStoreAPI) - if err != nil { - return nil, fmt.Errorf("global store: %v", err) - } - globalStore := mockrpc.NewGlobalStore(client) - // create a node store for this swarm key on global store - nodeStore = globalStore.NewNodeStore(common.HexToAddress(bzzconfig.BzzKey)) - } - return swarm.NewSwarm(bzzconfig, nodeStore) - } - //register within the ethereum node - if err := stack.Register(boot); err != nil { - utils.Fatalf("Failed to register the Swarm service: %v", err) - } -} - -func getAccount(bzzaccount string, ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey { - //an account is mandatory - if bzzaccount == "" { - utils.Fatalf(SwarmErrNoBZZAccount) - } - // Try to load the arg as a hex key file. - if key, err := crypto.LoadECDSA(bzzaccount); err == nil { - log.Info("Swarm account key loaded", "address", crypto.PubkeyToAddress(key.PublicKey)) - return key - } - // Otherwise try getting it from the keystore. - am := stack.AccountManager() - ks := am.Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) - - return decryptStoreAccount(ks, bzzaccount, utils.MakePasswordList(ctx)) -} - -// getPrivKey returns the private key of the specified bzzaccount -// Used only by client commands, such as `feed` -func getPrivKey(ctx *cli.Context) *ecdsa.PrivateKey { - // booting up the swarm node just as we do in bzzd action - bzzconfig, err := buildConfig(ctx) - if err != nil { - utils.Fatalf("unable to configure swarm: %v", err) - } - cfg := defaultNodeConfig - if _, err := os.Stat(bzzconfig.Path); err == nil { - cfg.DataDir = bzzconfig.Path - } - utils.SetNodeConfig(ctx, &cfg) - stack, err := node.New(&cfg) - if err != nil { - utils.Fatalf("can't create node: %v", err) - } - defer stack.Close() - - return getAccount(bzzconfig.BzzAccount, ctx, stack) -} - -func decryptStoreAccount(ks *keystore.KeyStore, account string, passwords []string) *ecdsa.PrivateKey { - var a accounts.Account - var err error - if common.IsHexAddress(account) { - a, err = ks.Find(accounts.Account{Address: common.HexToAddress(account)}) - } else if ix, ixerr := strconv.Atoi(account); ixerr == nil && ix > 0 { - if accounts := ks.Accounts(); len(accounts) > ix { - a = accounts[ix] - } else { - err = fmt.Errorf("index %d higher than number of accounts %d", ix, len(accounts)) - } - } else { - utils.Fatalf("Can't find swarm account key %s", account) - } - if err != nil { - utils.Fatalf("Can't find swarm account key: %v - Is the provided bzzaccount(%s) from the right datadir/Path?", err, account) - } - keyjson, err := ioutil.ReadFile(a.URL.Path) - if err != nil { - utils.Fatalf("Can't load swarm account key: %v", err) - } - for i := 0; i < 3; i++ { - password := getPassPhrase(fmt.Sprintf("Unlocking swarm account %s [%d/3]", a.Address.Hex(), i+1), i, passwords) - key, err := keystore.DecryptKey(keyjson, password) - if err == nil { - return key.PrivateKey - } - } - utils.Fatalf("Can't decrypt swarm account key") - return nil -} - -// getPassPhrase retrieves the password associated with bzz account, either by fetching -// from a list of pre-loaded passwords, or by requesting it interactively from user. -func getPassPhrase(prompt string, i int, passwords []string) string { - // non-interactive - if len(passwords) > 0 { - if i < len(passwords) { - return passwords[i] - } - return passwords[len(passwords)-1] - } - - // fallback to interactive mode - if prompt != "" { - fmt.Println(prompt) - } - password, err := console.Stdin.PromptPassword("Passphrase: ") - if err != nil { - utils.Fatalf("Failed to read passphrase: %v", err) - } - return password -} - -// addDefaultHelpSubcommand scans through defined CLI commands and adds -// a basic help subcommand to each -// if a help command is already defined, it will take precedence over the default. -func addDefaultHelpSubcommands(commands []cli.Command) { - for i := range commands { - cmd := &commands[i] - if cmd.Subcommands != nil { - cmd.Subcommands = append(cmd.Subcommands, defaultSubcommandHelp) - addDefaultHelpSubcommands(cmd.Subcommands) - } - } -} - -func setSwarmBootstrapNodes(ctx *cli.Context, cfg *node.Config) { - if ctx.GlobalIsSet(utils.BootnodesFlag.Name) || ctx.GlobalIsSet(utils.BootnodesV4Flag.Name) { - return - } - - cfg.P2P.BootstrapNodes = []*enode.Node{} - - for _, url := range SwarmBootnodes { - node, err := enode.ParseV4(url) - if err != nil { - log.Error("Bootstrap URL invalid", "enode", url, "err", err) - } - cfg.P2P.BootstrapNodes = append(cfg.P2P.BootstrapNodes, node) - } - -} diff --git a/cmd/swarm/manifest.go b/cmd/swarm/manifest.go deleted file mode 100644 index 312c72fa2..000000000 --- a/cmd/swarm/manifest.go +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -// Command MANIFEST update -package main - -import ( - "fmt" - "os" - "strings" - - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/swarm/api" - swarm "github.com/ethereum/go-ethereum/swarm/api/client" - "gopkg.in/urfave/cli.v1" -) - -var manifestCommand = cli.Command{ - Name: "manifest", - CustomHelpTemplate: helpTemplate, - Usage: "perform operations on swarm manifests", - ArgsUsage: "COMMAND", - Description: "Updates a MANIFEST by adding/removing/updating the hash of a path.\nCOMMAND could be: add, update, remove", - Subcommands: []cli.Command{ - { - Action: manifestAdd, - CustomHelpTemplate: helpTemplate, - Name: "add", - Usage: "add a new path to the manifest", - ArgsUsage: "<MANIFEST> <path> <hash>", - Description: "Adds a new path to the manifest", - }, - { - Action: manifestUpdate, - CustomHelpTemplate: helpTemplate, - Name: "update", - Usage: "update the hash for an already existing path in the manifest", - ArgsUsage: "<MANIFEST> <path> <newhash>", - Description: "Update the hash for an already existing path in the manifest", - }, - { - Action: manifestRemove, - CustomHelpTemplate: helpTemplate, - Name: "remove", - Usage: "removes a path from the manifest", - ArgsUsage: "<MANIFEST> <path>", - Description: "Removes a path from the manifest", - }, - }, -} - -// manifestAdd adds a new entry to the manifest at the given path. -// New entry hash, the last argument, must be the hash of a manifest -// with only one entry, which meta-data will be added to the original manifest. -// On success, this function will print new (updated) manifest's hash. -func manifestAdd(ctx *cli.Context) { - args := ctx.Args() - if len(args) != 3 { - utils.Fatalf("Need exactly three arguments <MHASH> <path> <HASH>") - } - - var ( - mhash = args[0] - path = args[1] - hash = args[2] - ) - - bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") - client := swarm.NewClient(bzzapi) - - m, _, err := client.DownloadManifest(hash) - if err != nil { - utils.Fatalf("Error downloading manifest to add: %v", err) - } - l := len(m.Entries) - if l == 0 { - utils.Fatalf("No entries in manifest %s", hash) - } else if l > 1 { - utils.Fatalf("Too many entries in manifest %s", hash) - } - - newManifest := addEntryToManifest(client, mhash, path, m.Entries[0]) - fmt.Println(newManifest) -} - -// manifestUpdate replaces an existing entry of the manifest at the given path. -// New entry hash, the last argument, must be the hash of a manifest -// with only one entry, which meta-data will be added to the original manifest. -// On success, this function will print hash of the updated manifest. -func manifestUpdate(ctx *cli.Context) { - args := ctx.Args() - if len(args) != 3 { - utils.Fatalf("Need exactly three arguments <MHASH> <path> <HASH>") - } - - var ( - mhash = args[0] - path = args[1] - hash = args[2] - ) - - bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") - client := swarm.NewClient(bzzapi) - - m, _, err := client.DownloadManifest(hash) - if err != nil { - utils.Fatalf("Error downloading manifest to update: %v", err) - } - l := len(m.Entries) - if l == 0 { - utils.Fatalf("No entries in manifest %s", hash) - } else if l > 1 { - utils.Fatalf("Too many entries in manifest %s", hash) - } - - newManifest, _, defaultEntryUpdated := updateEntryInManifest(client, mhash, path, m.Entries[0], true) - if defaultEntryUpdated { - // Print informational message to stderr - // allowing the user to get the new manifest hash from stdout - // without the need to parse the complete output. - fmt.Fprintln(os.Stderr, "Manifest default entry is updated, too") - } - fmt.Println(newManifest) -} - -// manifestRemove removes an existing entry of the manifest at the given path. -// On success, this function will print hash of the manifest which does not -// contain the path. -func manifestRemove(ctx *cli.Context) { - args := ctx.Args() - if len(args) != 2 { - utils.Fatalf("Need exactly two arguments <MHASH> <path>") - } - - var ( - mhash = args[0] - path = args[1] - ) - - bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") - client := swarm.NewClient(bzzapi) - - newManifest := removeEntryFromManifest(client, mhash, path) - fmt.Println(newManifest) -} - -func addEntryToManifest(client *swarm.Client, mhash, path string, entry api.ManifestEntry) string { - var longestPathEntry = api.ManifestEntry{} - - mroot, isEncrypted, err := client.DownloadManifest(mhash) - if err != nil { - utils.Fatalf("Manifest download failed: %v", err) - } - - // See if we path is in this Manifest or do we have to dig deeper - for _, e := range mroot.Entries { - if path == e.Path { - utils.Fatalf("Path %s already present, not adding anything", path) - } else { - if e.ContentType == api.ManifestType { - prfxlen := strings.HasPrefix(path, e.Path) - if prfxlen && len(path) > len(longestPathEntry.Path) { - longestPathEntry = e - } - } - } - } - - if longestPathEntry.Path != "" { - // Load the child Manifest add the entry there - newPath := path[len(longestPathEntry.Path):] - newHash := addEntryToManifest(client, longestPathEntry.Hash, newPath, entry) - - // Replace the hash for parent Manifests - newMRoot := &api.Manifest{} - for _, e := range mroot.Entries { - if longestPathEntry.Path == e.Path { - e.Hash = newHash - } - newMRoot.Entries = append(newMRoot.Entries, e) - } - mroot = newMRoot - } else { - // Add the entry in the leaf Manifest - entry.Path = path - mroot.Entries = append(mroot.Entries, entry) - } - - newManifestHash, err := client.UploadManifest(mroot, isEncrypted) - if err != nil { - utils.Fatalf("Manifest upload failed: %v", err) - } - return newManifestHash -} - -// updateEntryInManifest updates an existing entry o path with a new one in the manifest with provided mhash -// finding the path recursively through all nested manifests. Argument isRoot is used for default -// entry update detection. If the updated entry has the same hash as the default entry, then the -// default entry in root manifest will be updated too. -// Returned values are the new manifest hash, hash of the entry that was replaced by the new entry and -// a a bool that is true if default entry is updated. -func updateEntryInManifest(client *swarm.Client, mhash, path string, entry api.ManifestEntry, isRoot bool) (newManifestHash, oldHash string, defaultEntryUpdated bool) { - var ( - newEntry = api.ManifestEntry{} - longestPathEntry = api.ManifestEntry{} - ) - - mroot, isEncrypted, err := client.DownloadManifest(mhash) - if err != nil { - utils.Fatalf("Manifest download failed: %v", err) - } - - // See if we path is in this Manifest or do we have to dig deeper - for _, e := range mroot.Entries { - if path == e.Path { - newEntry = e - // keep the reference of the hash of the entry that should be replaced - // for default entry detection - oldHash = e.Hash - } else { - if e.ContentType == api.ManifestType { - prfxlen := strings.HasPrefix(path, e.Path) - if prfxlen && len(path) > len(longestPathEntry.Path) { - longestPathEntry = e - } - } - } - } - - if longestPathEntry.Path == "" && newEntry.Path == "" { - utils.Fatalf("Path %s not present in the Manifest, not setting anything", path) - } - - if longestPathEntry.Path != "" { - // Load the child Manifest add the entry there - newPath := path[len(longestPathEntry.Path):] - var newHash string - newHash, oldHash, _ = updateEntryInManifest(client, longestPathEntry.Hash, newPath, entry, false) - - // Replace the hash for parent Manifests - newMRoot := &api.Manifest{} - for _, e := range mroot.Entries { - if longestPathEntry.Path == e.Path { - e.Hash = newHash - } - newMRoot.Entries = append(newMRoot.Entries, e) - - } - mroot = newMRoot - } - - // update the manifest if the new entry is found and - // check if default entry should be updated - if newEntry.Path != "" || isRoot { - // Replace the hash for leaf Manifest - newMRoot := &api.Manifest{} - for _, e := range mroot.Entries { - if newEntry.Path == e.Path { - entry.Path = e.Path - newMRoot.Entries = append(newMRoot.Entries, entry) - } else if isRoot && e.Path == "" && e.Hash == oldHash { - entry.Path = e.Path - newMRoot.Entries = append(newMRoot.Entries, entry) - defaultEntryUpdated = true - } else { - newMRoot.Entries = append(newMRoot.Entries, e) - } - } - mroot = newMRoot - } - - newManifestHash, err = client.UploadManifest(mroot, isEncrypted) - if err != nil { - utils.Fatalf("Manifest upload failed: %v", err) - } - return newManifestHash, oldHash, defaultEntryUpdated -} - -func removeEntryFromManifest(client *swarm.Client, mhash, path string) string { - var ( - entryToRemove = api.ManifestEntry{} - longestPathEntry = api.ManifestEntry{} - ) - - mroot, isEncrypted, err := client.DownloadManifest(mhash) - if err != nil { - utils.Fatalf("Manifest download failed: %v", err) - } - - // See if we path is in this Manifest or do we have to dig deeper - for _, entry := range mroot.Entries { - if path == entry.Path { - entryToRemove = entry - } else { - if entry.ContentType == api.ManifestType { - prfxlen := strings.HasPrefix(path, entry.Path) - if prfxlen && len(path) > len(longestPathEntry.Path) { - longestPathEntry = entry - } - } - } - } - - if longestPathEntry.Path == "" && entryToRemove.Path == "" { - utils.Fatalf("Path %s not present in the Manifest, not removing anything", path) - } - - if longestPathEntry.Path != "" { - // Load the child Manifest remove the entry there - newPath := path[len(longestPathEntry.Path):] - newHash := removeEntryFromManifest(client, longestPathEntry.Hash, newPath) - - // Replace the hash for parent Manifests - newMRoot := &api.Manifest{} - for _, entry := range mroot.Entries { - if longestPathEntry.Path == entry.Path { - entry.Hash = newHash - } - newMRoot.Entries = append(newMRoot.Entries, entry) - } - mroot = newMRoot - } - - if entryToRemove.Path != "" { - // remove the entry in this Manifest - newMRoot := &api.Manifest{} - for _, entry := range mroot.Entries { - if entryToRemove.Path != entry.Path { - newMRoot.Entries = append(newMRoot.Entries, entry) - } - } - mroot = newMRoot - } - - newManifestHash, err := client.UploadManifest(mroot, isEncrypted) - if err != nil { - utils.Fatalf("Manifest upload failed: %v", err) - } - return newManifestHash -} diff --git a/cmd/swarm/manifest_test.go b/cmd/swarm/manifest_test.go deleted file mode 100644 index 01d982aa7..000000000 --- a/cmd/swarm/manifest_test.go +++ /dev/null @@ -1,597 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "bytes" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/ethereum/go-ethereum/swarm/api" - swarm "github.com/ethereum/go-ethereum/swarm/api/client" - swarmhttp "github.com/ethereum/go-ethereum/swarm/api/http" -) - -// TestManifestChange tests manifest add, update and remove -// cli commands without encryption. -func TestManifestChange(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip() - } - - testManifestChange(t, false) -} - -// TestManifestChange tests manifest add, update and remove -// cli commands with encryption enabled. -func TestManifestChangeEncrypted(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip() - } - - testManifestChange(t, true) -} - -// testManifestChange performs cli commands: -// - manifest add -// - manifest update -// - manifest remove -// on a manifest, testing the functionality of this -// comands on paths that are in root manifest or a nested one. -// Argument encrypt controls whether to use encryption or not. -func testManifestChange(t *testing.T, encrypt bool) { - t.Parallel() - srv := swarmhttp.NewTestSwarmServer(t, serverFunc, nil) - defer srv.Close() - - tmp, err := ioutil.TempDir("", "swarm-manifest-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmp) - - origDir := filepath.Join(tmp, "orig") - if err := os.Mkdir(origDir, 0777); err != nil { - t.Fatal(err) - } - - indexDataFilename := filepath.Join(origDir, "index.html") - err = ioutil.WriteFile(indexDataFilename, []byte("<h1>Test</h1>"), 0666) - if err != nil { - t.Fatal(err) - } - // Files paths robots.txt and robots.html share the same prefix "robots." - // which will result a manifest with a nested manifest under path "robots.". - // This will allow testing manifest changes on both root and nested manifest. - err = ioutil.WriteFile(filepath.Join(origDir, "robots.txt"), []byte("Disallow: /"), 0666) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(origDir, "robots.html"), []byte("<strong>No Robots Allowed</strong>"), 0666) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(origDir, "mutants.txt"), []byte("Frank\nMarcus"), 0666) - if err != nil { - t.Fatal(err) - } - - args := []string{ - "--bzzapi", - srv.URL, - "--recursive", - "--defaultpath", - indexDataFilename, - "up", - origDir, - } - if encrypt { - args = append(args, "--encrypt") - } - - origManifestHash := runSwarmExpectHash(t, args...) - - checkHashLength(t, origManifestHash, encrypt) - - client := swarm.NewClient(srv.URL) - - // upload a new file and use its manifest to add it the original manifest. - t.Run("add", func(t *testing.T) { - humansData := []byte("Ann\nBob") - humansDataFilename := filepath.Join(tmp, "humans.txt") - err = ioutil.WriteFile(humansDataFilename, humansData, 0666) - if err != nil { - t.Fatal(err) - } - - humansManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "up", - humansDataFilename, - ) - - newManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "manifest", - "add", - origManifestHash, - "humans.txt", - humansManifestHash, - ) - - checkHashLength(t, newManifestHash, encrypt) - - newManifest := downloadManifest(t, client, newManifestHash, encrypt) - - var found bool - for _, e := range newManifest.Entries { - if e.Path == "humans.txt" { - found = true - if e.Size != int64(len(humansData)) { - t.Errorf("expected humans.txt size %v, got %v", len(humansData), e.Size) - } - if e.ModTime.IsZero() { - t.Errorf("got zero mod time for humans.txt") - } - ct := "text/plain; charset=utf-8" - if e.ContentType != ct { - t.Errorf("expected content type %q, got %q", ct, e.ContentType) - } - break - } - } - if !found { - t.Fatal("no humans.txt in new manifest") - } - - checkFile(t, client, newManifestHash, "humans.txt", humansData) - }) - - // upload a new file and use its manifest to add it the original manifest, - // but ensure that the file will be in the nested manifest of the original one. - t.Run("add nested", func(t *testing.T) { - robotsData := []byte(`{"disallow": "/"}`) - robotsDataFilename := filepath.Join(tmp, "robots.json") - err = ioutil.WriteFile(robotsDataFilename, robotsData, 0666) - if err != nil { - t.Fatal(err) - } - - robotsManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "up", - robotsDataFilename, - ) - - newManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "manifest", - "add", - origManifestHash, - "robots.json", - robotsManifestHash, - ) - - checkHashLength(t, newManifestHash, encrypt) - - newManifest := downloadManifest(t, client, newManifestHash, encrypt) - - var found bool - loop: - for _, e := range newManifest.Entries { - if e.Path == "robots." { - nestedManifest := downloadManifest(t, client, e.Hash, encrypt) - for _, e := range nestedManifest.Entries { - if e.Path == "json" { - found = true - if e.Size != int64(len(robotsData)) { - t.Errorf("expected robots.json size %v, got %v", len(robotsData), e.Size) - } - if e.ModTime.IsZero() { - t.Errorf("got zero mod time for robots.json") - } - ct := "application/json" - if e.ContentType != ct { - t.Errorf("expected content type %q, got %q", ct, e.ContentType) - } - break loop - } - } - } - } - if !found { - t.Fatal("no robots.json in new manifest") - } - - checkFile(t, client, newManifestHash, "robots.json", robotsData) - }) - - // upload a new file and use its manifest to change the file it the original manifest. - t.Run("update", func(t *testing.T) { - indexData := []byte("<h1>Ethereum Swarm</h1>") - indexDataFilename := filepath.Join(tmp, "index.html") - err = ioutil.WriteFile(indexDataFilename, indexData, 0666) - if err != nil { - t.Fatal(err) - } - - indexManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "up", - indexDataFilename, - ) - - newManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "manifest", - "update", - origManifestHash, - "index.html", - indexManifestHash, - ) - - checkHashLength(t, newManifestHash, encrypt) - - newManifest := downloadManifest(t, client, newManifestHash, encrypt) - - var found bool - for _, e := range newManifest.Entries { - if e.Path == "index.html" { - found = true - if e.Size != int64(len(indexData)) { - t.Errorf("expected index.html size %v, got %v", len(indexData), e.Size) - } - if e.ModTime.IsZero() { - t.Errorf("got zero mod time for index.html") - } - ct := "text/html; charset=utf-8" - if e.ContentType != ct { - t.Errorf("expected content type %q, got %q", ct, e.ContentType) - } - break - } - } - if !found { - t.Fatal("no index.html in new manifest") - } - - checkFile(t, client, newManifestHash, "index.html", indexData) - - // check default entry change - checkFile(t, client, newManifestHash, "", indexData) - }) - - // upload a new file and use its manifest to change the file it the original manifest, - // but ensure that the file is in the nested manifest of the original one. - t.Run("update nested", func(t *testing.T) { - robotsData := []byte(`<string>Only humans allowed!!!</strong>`) - robotsDataFilename := filepath.Join(tmp, "robots.html") - err = ioutil.WriteFile(robotsDataFilename, robotsData, 0666) - if err != nil { - t.Fatal(err) - } - - humansManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "up", - robotsDataFilename, - ) - - newManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "manifest", - "update", - origManifestHash, - "robots.html", - humansManifestHash, - ) - - checkHashLength(t, newManifestHash, encrypt) - - newManifest := downloadManifest(t, client, newManifestHash, encrypt) - - var found bool - loop: - for _, e := range newManifest.Entries { - if e.Path == "robots." { - nestedManifest := downloadManifest(t, client, e.Hash, encrypt) - for _, e := range nestedManifest.Entries { - if e.Path == "html" { - found = true - if e.Size != int64(len(robotsData)) { - t.Errorf("expected robots.html size %v, got %v", len(robotsData), e.Size) - } - if e.ModTime.IsZero() { - t.Errorf("got zero mod time for robots.html") - } - ct := "text/html; charset=utf-8" - if e.ContentType != ct { - t.Errorf("expected content type %q, got %q", ct, e.ContentType) - } - break loop - } - } - } - } - if !found { - t.Fatal("no robots.html in new manifest") - } - - checkFile(t, client, newManifestHash, "robots.html", robotsData) - }) - - // remove a file from the manifest. - t.Run("remove", func(t *testing.T) { - newManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "manifest", - "remove", - origManifestHash, - "mutants.txt", - ) - - checkHashLength(t, newManifestHash, encrypt) - - newManifest := downloadManifest(t, client, newManifestHash, encrypt) - - var found bool - for _, e := range newManifest.Entries { - if e.Path == "mutants.txt" { - found = true - break - } - } - if found { - t.Fatal("mutants.txt is not removed") - } - }) - - // remove a file from the manifest, but ensure that the file is in - // the nested manifest of the original one. - t.Run("remove nested", func(t *testing.T) { - newManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "manifest", - "remove", - origManifestHash, - "robots.html", - ) - - checkHashLength(t, newManifestHash, encrypt) - - newManifest := downloadManifest(t, client, newManifestHash, encrypt) - - var found bool - loop: - for _, e := range newManifest.Entries { - if e.Path == "robots." { - nestedManifest := downloadManifest(t, client, e.Hash, encrypt) - for _, e := range nestedManifest.Entries { - if e.Path == "html" { - found = true - break loop - } - } - } - } - if found { - t.Fatal("robots.html in not removed") - } - }) -} - -// TestNestedDefaultEntryUpdate tests if the default entry is updated -// if the file in nested manifest used for it is also updated. -func TestNestedDefaultEntryUpdate(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip() - } - - testNestedDefaultEntryUpdate(t, false) -} - -// TestNestedDefaultEntryUpdateEncrypted tests if the default entry -// of encrypted upload is updated if the file in nested manifest -// used for it is also updated. -func TestNestedDefaultEntryUpdateEncrypted(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip() - } - - testNestedDefaultEntryUpdate(t, true) -} - -func testNestedDefaultEntryUpdate(t *testing.T, encrypt bool) { - t.Parallel() - srv := swarmhttp.NewTestSwarmServer(t, serverFunc, nil) - defer srv.Close() - - tmp, err := ioutil.TempDir("", "swarm-manifest-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmp) - - origDir := filepath.Join(tmp, "orig") - if err := os.Mkdir(origDir, 0777); err != nil { - t.Fatal(err) - } - - indexData := []byte("<h1>Test</h1>") - indexDataFilename := filepath.Join(origDir, "index.html") - err = ioutil.WriteFile(indexDataFilename, indexData, 0666) - if err != nil { - t.Fatal(err) - } - // Add another file with common prefix as the default entry to test updates of - // default entry with nested manifests. - err = ioutil.WriteFile(filepath.Join(origDir, "index.txt"), []byte("Test"), 0666) - if err != nil { - t.Fatal(err) - } - - args := []string{ - "--bzzapi", - srv.URL, - "--recursive", - "--defaultpath", - indexDataFilename, - "up", - origDir, - } - if encrypt { - args = append(args, "--encrypt") - } - - origManifestHash := runSwarmExpectHash(t, args...) - - checkHashLength(t, origManifestHash, encrypt) - - client := swarm.NewClient(srv.URL) - - newIndexData := []byte("<h1>Ethereum Swarm</h1>") - newIndexDataFilename := filepath.Join(tmp, "index.html") - err = ioutil.WriteFile(newIndexDataFilename, newIndexData, 0666) - if err != nil { - t.Fatal(err) - } - - newIndexManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "up", - newIndexDataFilename, - ) - - newManifestHash := runSwarmExpectHash(t, - "--bzzapi", - srv.URL, - "manifest", - "update", - origManifestHash, - "index.html", - newIndexManifestHash, - ) - - checkHashLength(t, newManifestHash, encrypt) - - newManifest := downloadManifest(t, client, newManifestHash, encrypt) - - var found bool - for _, e := range newManifest.Entries { - if e.Path == "index." { - found = true - newManifest = downloadManifest(t, client, e.Hash, encrypt) - break - } - } - if !found { - t.Fatal("no index. path in new manifest") - } - - found = false - for _, e := range newManifest.Entries { - if e.Path == "html" { - found = true - if e.Size != int64(len(newIndexData)) { - t.Errorf("expected index.html size %v, got %v", len(newIndexData), e.Size) - } - if e.ModTime.IsZero() { - t.Errorf("got zero mod time for index.html") - } - ct := "text/html; charset=utf-8" - if e.ContentType != ct { - t.Errorf("expected content type %q, got %q", ct, e.ContentType) - } - break - } - } - if !found { - t.Fatal("no html in new manifest") - } - - checkFile(t, client, newManifestHash, "index.html", newIndexData) - - // check default entry change - checkFile(t, client, newManifestHash, "", newIndexData) -} - -func runSwarmExpectHash(t *testing.T, args ...string) (hash string) { - t.Helper() - hashRegexp := `[a-f\d]{64,128}` - up := runSwarm(t, args...) - _, matches := up.ExpectRegexp(hashRegexp) - up.ExpectExit() - - if len(matches) < 1 { - t.Fatal("no matches found") - } - return matches[0] -} - -func checkHashLength(t *testing.T, hash string, encrypted bool) { - t.Helper() - l := len(hash) - if encrypted && l != 128 { - t.Errorf("expected hash length 128, got %v", l) - } - if !encrypted && l != 64 { - t.Errorf("expected hash length 64, got %v", l) - } -} - -func downloadManifest(t *testing.T, client *swarm.Client, hash string, encrypted bool) (manifest *api.Manifest) { - t.Helper() - m, isEncrypted, err := client.DownloadManifest(hash) - if err != nil { - t.Fatal(err) - } - - if encrypted != isEncrypted { - t.Error("new manifest encryption flag is not correct") - } - return m -} - -func checkFile(t *testing.T, client *swarm.Client, hash, path string, expected []byte) { - t.Helper() - f, err := client.Download(hash, path) - if err != nil { - t.Fatal(err) - } - - got, err := ioutil.ReadAll(f) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(got, expected) { - t.Errorf("expected file content %q, got %q", expected, got) - } -} diff --git a/cmd/swarm/mimegen/generator.go b/cmd/swarm/mimegen/generator.go deleted file mode 100644 index 68f9e306e..000000000 --- a/cmd/swarm/mimegen/generator.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. -package main - -// Standard "mime" package rely on system-settings, see mime.osInitMime -// Swarm will run on many OS/Platform/Docker and must behave similar -// This command generates code to add common mime types based on mime.types file -// -// 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" - -import ( - "bufio" - "bytes" - "flag" - "html/template" - "io/ioutil" - "strings" - - "log" -) - -var ( - typesFlag = flag.String("types", "", "Input mime.types file") - packageFlag = flag.String("package", "", "Golang package in output file") - outFlag = flag.String("out", "", "Output file name for the generated mime types") -) - -type mime struct { - Name string - Exts []string -} - -type templateParams struct { - PackageName string - Mimes []mime -} - -func main() { - // Parse and ensure all needed inputs are specified - flag.Parse() - if *typesFlag == "" { - log.Fatalf("--types is required") - } - if *packageFlag == "" { - log.Fatalf("--types is required") - } - if *outFlag == "" { - log.Fatalf("--out is required") - } - - params := templateParams{ - PackageName: *packageFlag, - } - - types, err := ioutil.ReadFile(*typesFlag) - if err != nil { - log.Fatal(err) - } - - scanner := bufio.NewScanner(bytes.NewReader(types)) - for scanner.Scan() { - txt := scanner.Text() - if strings.HasPrefix(txt, "#") || len(txt) == 0 { - continue - } - parts := strings.Fields(txt) - if len(parts) == 1 { - continue - } - params.Mimes = append(params.Mimes, mime{parts[0], parts[1:]}) - } - - if err = scanner.Err(); err != nil { - log.Fatal(err) - } - - result := bytes.NewBuffer([]byte{}) - - if err := template.Must(template.New("_").Parse(tpl)).Execute(result, params); err != nil { - log.Fatal(err) - } - - if err := ioutil.WriteFile(*outFlag, result.Bytes(), 0600); err != nil { - log.Fatal(err) - } -} - -var tpl = `// Code generated by github.com/ethereum/go-ethereum/cmd/swarm/mimegen. DO NOT EDIT. - -package {{ .PackageName }} - -import "mime" -func init() { - var mimeTypes = map[string]string{ -{{- range .Mimes -}} - {{ $name := .Name -}} - {{- range .Exts }} - ".{{ . }}": "{{ $name | html }}", - {{- end }} -{{- end }} - } - for ext, name := range mimeTypes { - if err := mime.AddExtensionType(ext, name); err != nil { - panic(err) - } - } -} -` diff --git a/cmd/swarm/mimegen/mime.types b/cmd/swarm/mimegen/mime.types deleted file mode 100644 index 1bdf21149..000000000 --- a/cmd/swarm/mimegen/mime.types +++ /dev/null @@ -1,1828 +0,0 @@ -# This is a comment. I love comments. -*- indent-tabs-mode: t -*- - -# This file controls what Internet media types are sent to the client for -# given file extension(s). Sending the correct media type to the client -# is important so they know how to handle the content of the file. -# Extra types can either be added here or by using an AddType directive -# in your config files. For more information about Internet media types, -# please read RFC 2045, 2046, 2047, 2048, and 2077. The Internet media type -# registry is at <http://www.iana.org/assignments/media-types/>. - -# IANA types - -# MIME type Extensions -application/1d-interleaved-parityfec -application/3gpdash-qoe-report+xml -application/3gpp-ims+xml -application/A2L a2l -application/activemessage -application/alto-costmap+json -application/alto-costmapfilter+json -application/alto-directory+json -application/alto-endpointcost+json -application/alto-endpointcostparams+json -application/alto-endpointprop+json -application/alto-endpointpropparams+json -application/alto-error+json -application/alto-networkmap+json -application/alto-networkmapfilter+json -application/AML aml -application/andrew-inset ez -application/applefile -application/ATF atf -application/ATFX atfx -application/ATXML atxml -application/atom+xml atom -application/atomcat+xml atomcat -application/atomdeleted+xml atomdeleted -application/atomicmail -application/atomsvc+xml atomsvc -application/auth-policy+xml apxml -application/bacnet-xdd+zip xdd -application/batch-SMTP -application/beep+xml -application/calendar+json -application/calendar+xml xcs -application/call-completion -application/cals-1840 -application/cbor cbor -application/ccmp+xml ccmp -application/ccxml+xml ccxml -application/CDFX+XML cdfx -application/cdmi-capability cdmia -application/cdmi-container cdmic -application/cdmi-domain cdmid -application/cdmi-object cdmio -application/cdmi-queue cdmiq -application/cdni -application/CEA cea -application/cea-2018+xml -application/cellml+xml cellml cml -application/cfw -application/clue_info+xml clue -application/cms cmsc -application/cnrp+xml -application/coap-group+json -application/coap-payload -application/commonground -application/conference-info+xml -application/cpl+xml cpl -application/cose -application/cose-key -application/cose-key-set -application/csrattrs csrattrs -application/csta+xml -application/CSTAdata+xml -application/csvm+json -application/cybercash -application/dash+xml mpd -application/dashdelta mpdd -application/davmount+xml davmount -application/dca-rft -application/DCD dcd -application/dec-dx -application/dialog-info+xml -application/dicom dcm -application/dicom+json -application/dicom+xml -application/DII dii -application/DIT dit -application/dns -application/dskpp+xml xmls -application/dssc+der dssc -application/dssc+xml xdssc -application/dvcs dvc -application/ecmascript es -application/EDI-Consent -application/EDI-X12 -application/EDIFACT -application/efi efi -application/EmergencyCallData.Comment+xml -application/EmergencyCallData.Control+xml -application/EmergencyCallData.DeviceInfo+xml -application/EmergencyCallData.eCall.MSD -application/EmergencyCallData.ProviderInfo+xml -application/EmergencyCallData.ServiceInfo+xml -application/EmergencyCallData.SubscriberInfo+xml -application/EmergencyCallData.VEDS+xml -application/emma+xml emma -application/emotionml+xml emotionml -application/encaprtp -application/epp+xml -application/epub+zip epub -application/eshop -application/exi exi -application/fastinfoset finf -application/fastsoap -application/fdt+xml fdt -# fits, fit, fts: image/fits -application/fits -# application/font-sfnt deprecated in favor of font/sfnt -application/font-tdpfr pfr -# application/font-woff deprecated in favor of font/woff -application/framework-attributes+xml -application/geo+json geojson -application/geo+json-seq -application/gml+xml gml -application/gzip gz tgz -application/H224 -application/held+xml -application/http -application/hyperstudio stk -application/ibe-key-request+xml -application/ibe-pkg-reply+xml -application/ibe-pp-data -application/iges -application/im-iscomposing+xml -application/index -application/index.cmd -application/index.obj -application/index.response -application/index.vnd -application/inkml+xml ink inkml -application/iotp -application/ipfix ipfix -application/ipp -application/isup -application/its+xml its -application/javascript js -application/jose -application/jose+json -application/jrd+json jrd -application/json json -application/json-patch+json json-patch -application/json-seq -application/jwk+json -application/jwk-set+json -application/jwt -application/kpml-request+xml -application/kpml-response+xml -application/ld+json jsonld -application/lgr+xml lgr -application/link-format wlnk -application/load-control+xml -application/lost+xml lostxml -application/lostsync+xml lostsyncxml -application/LXF lxf -application/mac-binhex40 hqx -application/macwriteii -application/mads+xml mads -application/marc mrc -application/marcxml+xml mrcx -application/mathematica nb ma mb -application/mathml-content+xml -application/mathml-presentation+xml -application/mathml+xml mml -application/mbms-associated-procedure-description+xml -application/mbms-deregister+xml -application/mbms-envelope+xml -application/mbms-msk-response+xml -application/mbms-msk+xml -application/mbms-protection-description+xml -application/mbms-reception-report+xml -application/mbms-register-response+xml -application/mbms-register+xml -application/mbms-schedule+xml -application/mbms-user-service-description+xml -application/mbox mbox -application/media_control+xml -# mpf: text/vnd.ms-mediapackage -application/media-policy-dataset+xml -application/mediaservercontrol+xml -application/merge-patch+json -application/metalink4+xml meta4 -application/mets+xml mets -application/MF4 mf4 -application/mikey -application/mods+xml mods -application/moss-keys -application/moss-signature -application/mosskey-data -application/mosskey-request -application/mp21 m21 mp21 -# mp4, mpg4: video/mp4, see RFC 4337 -application/mp4 -application/mpeg4-generic -application/mpeg4-iod -application/mpeg4-iod-xmt -# xdf: application/xcap-diff+xml -application/mrb-consumer+xml -application/mrb-publish+xml -application/msc-ivr+xml -application/msc-mixer+xml -application/msword doc -application/mud+json -application/mxf mxf -application/n-quads nq -application/n-triples nt -application/nasdata -application/news-checkgroups -application/news-groupinfo -application/news-transmission -application/nlsml+xml -application/nss -application/ocsp-request orq -application/ocsp-response ors -application/octet-stream bin lha lzh exe class so dll img iso -application/oda oda -application/ODX odx -application/oebps-package+xml opf -application/ogg ogx -application/oxps oxps -application/p2p-overlay+xml relo -application/parityfec -# xer: application/xcap-error+xml -application/patch-ops-error+xml -application/pdf pdf -application/PDX pdx -application/pgp-encrypted pgp -application/pgp-keys -application/pgp-signature sig -application/pidf-diff+xml -application/pidf+xml -application/pkcs10 p10 -application/pkcs12 p12 pfx -application/pkcs7-mime p7m p7c -application/pkcs7-signature p7s -application/pkcs8 p8 -# ac: application/vnd.nokia.n-gage.ac+xml -application/pkix-attr-cert -application/pkix-cert cer -application/pkix-crl crl -application/pkix-pkipath pkipath -application/pkixcmp pki -application/pls+xml pls -application/poc-settings+xml -application/postscript ps eps ai -application/ppsp-tracker+json -application/problem+json -application/problem+xml -application/provenance+xml provx -application/prs.alvestrand.titrax-sheet -application/prs.cww cw cww -application/prs.hpub+zip hpub -application/prs.nprend rnd rct -application/prs.plucker -application/prs.rdf-xml-crypt rdf-crypt -application/prs.xsf+xml xsf -application/pskc+xml pskcxml -application/qsig -application/raptorfec -application/rdap+json -application/rdf+xml rdf -application/reginfo+xml rif -application/relax-ng-compact-syntax rnc -application/remote-printing -application/reputon+json -application/resource-lists-diff+xml rld -application/resource-lists+xml rl -application/rfc+xml rfcxml -application/riscos -application/rlmi+xml -application/rls-services+xml rs -application/rpki-ghostbusters gbr -application/rpki-manifest mft -application/rpki-publication -application/rpki-roa roa -application/rpki-updown -application/rtf rtf -application/rtploopback -application/rtx -application/samlassertion+xml -application/samlmetadata+xml -application/sbml+xml -application/scaip+xml -# scm: application/vnd.lotus-screencam -application/scim+json scim -application/scvp-cv-request scq -application/scvp-cv-response scs -application/scvp-vp-request spq -application/scvp-vp-response spp -application/sdp sdp -application/sep+xml -application/sep-exi -application/session-info -application/set-payment -application/set-payment-initiation -application/set-registration -application/set-registration-initiation -application/sgml -application/sgml-open-catalog soc -application/shf+xml shf -application/sieve siv sieve -application/simple-filter+xml cl -application/simple-message-summary -application/simpleSymbolContainer -application/slate -# application/smil obsoleted by application/smil+xml -application/smil+xml smil smi sml -application/smpte336m -application/soap+fastinfoset -application/soap+xml -application/sparql-query rq -application/sparql-results+xml srx -application/spirits-event+xml -application/sql sql -application/srgs gram -application/srgs+xml grxml -application/sru+xml sru -application/ssml+xml ssml -application/tamp-apex-update tau -application/tamp-apex-update-confirm auc -application/tamp-community-update tcu -application/tamp-community-update-confirm cuc -application/tamp-error ter -application/tamp-sequence-adjust tsa -application/tamp-sequence-adjust-confirm sac -# tsq: application/timestamp-query -application/tamp-status-query -# tsr: application/timestamp-reply -application/tamp-status-response -application/tamp-update tur -application/tamp-update-confirm tuc -application/tei+xml tei teiCorpus odd -application/thraud+xml tfi -application/timestamp-query tsq -application/timestamp-reply tsr -application/timestamped-data tsd -application/trig trig -application/ttml+xml ttml -application/tve-trigger -application/ulpfec -application/urc-grpsheet+xml gsheet -application/urc-ressheet+xml rsheet -application/urc-targetdesc+xml td -application/urc-uisocketdesc+xml uis -application/vcard+json -application/vcard+xml -application/vemmi -application/vnd.3gpp.access-transfer-events+xml -application/vnd.3gpp.bsf+xml -application/vnd.3gpp.mid-call+xml -application/vnd.3gpp.pic-bw-large plb -application/vnd.3gpp.pic-bw-small psb -application/vnd.3gpp.pic-bw-var pvb -application/vnd.3gpp-prose+xml -application/vnd.3gpp-prose-pc3ch+xml -# sms: application/vnd.3gpp2.sms -application/vnd.3gpp.sms -application/vnd.3gpp.sms+xml -application/vnd.3gpp.srvcc-ext+xml -application/vnd.3gpp.SRVCC-info+xml -application/vnd.3gpp.state-and-event-info+xml -application/vnd.3gpp.ussd+xml -application/vnd.3gpp2.bcmcsinfo+xml -application/vnd.3gpp2.sms sms -application/vnd.3gpp2.tcap tcap -application/vnd.3lightssoftware.imagescal imgcal -application/vnd.3M.Post-it-Notes pwn -application/vnd.accpac.simply.aso aso -application/vnd.accpac.simply.imp imp -application/vnd.acucobol acu -application/vnd.acucorp atc acutc -application/vnd.adobe.flash.movie swf -application/vnd.adobe.formscentral.fcdt fcdt -application/vnd.adobe.fxp fxp fxpl -application/vnd.adobe.partial-upload -application/vnd.adobe.xdp+xml xdp -application/vnd.adobe.xfdf xfdf -application/vnd.aether.imp -application/vnd.ah-barcode -application/vnd.ahead.space ahead -application/vnd.airzip.filesecure.azf azf -application/vnd.airzip.filesecure.azs azs -application/vnd.amazon.mobi8-ebook azw3 -application/vnd.americandynamics.acc acc -application/vnd.amiga.ami ami -application/vnd.amundsen.maze+xml -application/vnd.anki apkg -application/vnd.anser-web-certificate-issue-initiation cii -# Not in IANA listing, but is on FTP site? -application/vnd.anser-web-funds-transfer-initiation fti -# atx: audio/ATRAC-X -application/vnd.antix.game-component -application/vnd.apache.thrift.binary -application/vnd.apache.thrift.compact -application/vnd.apache.thrift.json -application/vnd.api+json -application/vnd.apothekende.reservation+json -application/vnd.apple.installer+xml dist distz pkg mpkg -# m3u: audio/x-mpegurl for now -application/vnd.apple.mpegurl m3u8 -# application/vnd.arastra.swi obsoleted by application/vnd.aristanetworks.swi -application/vnd.aristanetworks.swi swi -application/vnd.artsquare -application/vnd.astraea-software.iota iota -application/vnd.audiograph aep -application/vnd.autopackage package -application/vnd.avistar+xml -application/vnd.balsamiq.bmml+xml bmml -application/vnd.balsamiq.bmpr bmpr -application/vnd.bekitzur-stech+json -application/vnd.bint.med-content -application/vnd.biopax.rdf+xml -application/vnd.blueice.multipass mpm -application/vnd.bluetooth.ep.oob ep -application/vnd.bluetooth.le.oob le -application/vnd.bmi bmi -application/vnd.businessobjects rep -application/vnd.cab-jscript -application/vnd.canon-cpdl -application/vnd.canon-lips -application/vnd.capasystems-pg+json -application/vnd.cendio.thinlinc.clientconf tlclient -application/vnd.century-systems.tcp_stream -application/vnd.chemdraw+xml cdxml -application/vnd.chess-pgn pgn -application/vnd.chipnuts.karaoke-mmd mmd -application/vnd.cinderella cdy -application/vnd.cirpack.isdn-ext -application/vnd.citationstyles.style+xml csl -application/vnd.claymore cla -application/vnd.cloanto.rp9 rp9 -application/vnd.clonk.c4group c4g c4d c4f c4p c4u -application/vnd.cluetrust.cartomobile-config c11amc -application/vnd.cluetrust.cartomobile-config-pkg c11amz -application/vnd.coffeescript coffee -application/vnd.collection+json -application/vnd.collection.doc+json -application/vnd.collection.next+json -application/vnd.comicbook+zip cbz -# icc: application/vnd.iccprofile -application/vnd.commerce-battelle ica icf icd ic0 ic1 ic2 ic3 ic4 ic5 ic6 ic7 ic8 -application/vnd.commonspace csp cst -application/vnd.contact.cmsg cdbcmsg -application/vnd.coreos.ignition+json ign ignition -application/vnd.cosmocaller cmc -application/vnd.crick.clicker clkx -application/vnd.crick.clicker.keyboard clkk -application/vnd.crick.clicker.palette clkp -application/vnd.crick.clicker.template clkt -application/vnd.crick.clicker.wordbank clkw -application/vnd.criticaltools.wbs+xml wbs -application/vnd.ctc-posml pml -application/vnd.ctct.ws+xml -application/vnd.cups-pdf -application/vnd.cups-postscript -application/vnd.cups-ppd ppd -application/vnd.cups-raster -application/vnd.cups-raw -application/vnd.curl curl -application/vnd.cyan.dean.root+xml -application/vnd.cybank -application/vnd.d2l.coursepackage1p0+zip -application/vnd.dart dart -application/vnd.data-vision.rdz rdz -application/vnd.datapackage+json -application/vnd.dataresource+json -application/vnd.debian.binary-package deb udeb -application/vnd.dece.data uvf uvvf uvd uvvd -application/vnd.dece.ttml+xml uvt uvvt -application/vnd.dece.unspecified uvx uvvx -application/vnd.dece.zip uvz uvvz -application/vnd.denovo.fcselayout-link fe_launch -application/vnd.desmume.movie dsm -application/vnd.dir-bi.plate-dl-nosuffix -application/vnd.dm.delegation+xml -application/vnd.dna dna -application/vnd.document+json docjson -application/vnd.dolby.mobile.1 -application/vnd.dolby.mobile.2 -application/vnd.doremir.scorecloud-binary-document scld -application/vnd.dpgraph dpg mwc dpgraph -application/vnd.dreamfactory dfac -application/vnd.drive+json -application/vnd.dtg.local -application/vnd.dtg.local.flash fla -application/vnd.dtg.local.html -application/vnd.dvb.ait ait -# class: application/octet-stream -application/vnd.dvb.dvbj -application/vnd.dvb.esgcontainer -application/vnd.dvb.ipdcdftnotifaccess -application/vnd.dvb.ipdcesgaccess -application/vnd.dvb.ipdcesgaccess2 -application/vnd.dvb.ipdcesgpdd -application/vnd.dvb.ipdcroaming -application/vnd.dvb.iptv.alfec-base -application/vnd.dvb.iptv.alfec-enhancement -application/vnd.dvb.notif-aggregate-root+xml -application/vnd.dvb.notif-container+xml -application/vnd.dvb.notif-generic+xml -application/vnd.dvb.notif-ia-msglist+xml -application/vnd.dvb.notif-ia-registration-request+xml -application/vnd.dvb.notif-ia-registration-response+xml -application/vnd.dvb.notif-init+xml -# pfr: application/font-tdpfr -application/vnd.dvb.pfr -application/vnd.dvb.service svc -# dxr: application/x-director -application/vnd.dxr -application/vnd.dynageo geo -application/vnd.dzr dzr -application/vnd.easykaraoke.cdgdownload -application/vnd.ecdis-update -application/vnd.ecowin.chart mag -application/vnd.ecowin.filerequest -application/vnd.ecowin.fileupdate -application/vnd.ecowin.series -application/vnd.ecowin.seriesrequest -application/vnd.ecowin.seriesupdate -# img: application/octet-stream -application/vnd.efi-img -# iso: application/octet-stream -application/vnd.efi-iso -application/vnd.enliven nml -application/vnd.enphase.envoy -application/vnd.eprints.data+xml -application/vnd.epson.esf esf -application/vnd.epson.msf msf -application/vnd.epson.quickanime qam -application/vnd.epson.salt slt -application/vnd.epson.ssf ssf -application/vnd.ericsson.quickcall qcall qca -application/vnd.espass-espass+zip espass -application/vnd.eszigno3+xml es3 et3 -application/vnd.etsi.aoc+xml -application/vnd.etsi.asic-e+zip asice sce -# scs: application/scvp-cv-response -application/vnd.etsi.asic-s+zip asics -application/vnd.etsi.cug+xml -application/vnd.etsi.iptvcommand+xml -application/vnd.etsi.iptvdiscovery+xml -application/vnd.etsi.iptvprofile+xml -application/vnd.etsi.iptvsad-bc+xml -application/vnd.etsi.iptvsad-cod+xml -application/vnd.etsi.iptvsad-npvr+xml -application/vnd.etsi.iptvservice+xml -application/vnd.etsi.iptvsync+xml -application/vnd.etsi.iptvueprofile+xml -application/vnd.etsi.mcid+xml -application/vnd.etsi.mheg5 -application/vnd.etsi.overload-control-policy-dataset+xml -application/vnd.etsi.pstn+xml -application/vnd.etsi.sci+xml -application/vnd.etsi.simservs+xml -application/vnd.etsi.timestamp-token tst -application/vnd.etsi.tsl.der -application/vnd.etsi.tsl+xml -application/vnd.eudora.data -application/vnd.ezpix-album ez2 -application/vnd.ezpix-package ez3 -application/vnd.f-secure.mobile -application/vnd.fastcopy-disk-image dim -application/vnd.fdf fdf -application/vnd.fdsn.mseed msd mseed -application/vnd.fdsn.seed seed dataless -application/vnd.ffsns -application/vnd.filmit.zfc zfc -# all extensions: application/vnd.hbci -application/vnd.fints -application/vnd.firemonkeys.cloudcell -application/vnd.FloGraphIt gph -application/vnd.fluxtime.clip ftc -application/vnd.font-fontforge-sfd sfd -application/vnd.framemaker fm -application/vnd.frogans.fnc fnc -application/vnd.frogans.ltf ltf -application/vnd.fsc.weblaunch fsc -application/vnd.fujitsu.oasys oas -application/vnd.fujitsu.oasys2 oa2 -application/vnd.fujitsu.oasys3 oa3 -application/vnd.fujitsu.oasysgp fg5 -application/vnd.fujitsu.oasysprs bh2 -application/vnd.fujixerox.ART-EX -application/vnd.fujixerox.ART4 -application/vnd.fujixerox.ddd ddd -application/vnd.fujixerox.docuworks xdw -application/vnd.fujixerox.docuworks.binder xbd -application/vnd.fujixerox.docuworks.container xct -application/vnd.fujixerox.HBPL -application/vnd.fut-misnet -application/vnd.fuzzysheet fzs -application/vnd.genomatix.tuxedo txd -# application/vnd.geo+json obsoleted by application/geo+json -application/vnd.geocube+xml g3 g³ -application/vnd.geogebra.file ggb -application/vnd.geogebra.tool ggt -application/vnd.geometry-explorer gex gre -application/vnd.geonext gxt -application/vnd.geoplan g2w -application/vnd.geospace g3w -# gbr: application/rpki-ghostbusters -application/vnd.gerber -application/vnd.globalplatform.card-content-mgt -application/vnd.globalplatform.card-content-mgt-response -application/vnd.gmx gmx -application/vnd.google-earth.kml+xml kml -application/vnd.google-earth.kmz kmz -application/vnd.gov.sk.e-form+xml -application/vnd.gov.sk.e-form+zip -application/vnd.gov.sk.xmldatacontainer+xml -application/vnd.grafeq gqf gqs -application/vnd.gridmp -application/vnd.groove-account gac -application/vnd.groove-help ghf -application/vnd.groove-identity-message gim -application/vnd.groove-injector grv -application/vnd.groove-tool-message gtm -application/vnd.groove-tool-template tpl -application/vnd.groove-vcard vcg -application/vnd.hal+json -application/vnd.hal+xml hal -application/vnd.HandHeld-Entertainment+xml zmm -application/vnd.hbci hbci hbc kom upa pkd bpd -application/vnd.hc+json -# rep: application/vnd.businessobjects -application/vnd.hcl-bireports -application/vnd.hdt hdt -application/vnd.heroku+json -application/vnd.hhe.lesson-player les -application/vnd.hp-HPGL hpgl -application/vnd.hp-hpid hpi hpid -application/vnd.hp-hps hps -application/vnd.hp-jlyt jlt -application/vnd.hp-PCL pcl -application/vnd.hp-PCLXL -application/vnd.httphone -application/vnd.hydrostatix.sof-data sfd-hdstx -application/vnd.hyperdrive+json -application/vnd.hzn-3d-crossword x3d -application/vnd.ibm.afplinedata -application/vnd.ibm.electronic-media emm -application/vnd.ibm.MiniPay mpy -application/vnd.ibm.modcap list3820 listafp afp pseg3820 -application/vnd.ibm.rights-management irm -application/vnd.ibm.secure-container sc -application/vnd.iccprofile icc icm -application/vnd.ieee.1905 1905.1 -application/vnd.igloader igl -application/vnd.imagemeter.folder+zip imf -application/vnd.imagemeter.image+zip imi -application/vnd.immervision-ivp ivp -application/vnd.immervision-ivu ivu -application/vnd.ims.imsccv1p1 imscc -application/vnd.ims.imsccv1p2 -application/vnd.ims.imsccv1p3 -application/vnd.ims.lis.v2.result+json -application/vnd.ims.lti.v2.toolconsumerprofile+json -application/vnd.ims.lti.v2.toolproxy.id+json -application/vnd.ims.lti.v2.toolproxy+json -application/vnd.ims.lti.v2.toolsettings+json -application/vnd.ims.lti.v2.toolsettings.simple+json -application/vnd.informedcontrol.rms+xml -# application/vnd.informix-visionary obsoleted by application/vnd.visionary -application/vnd.infotech.project -application/vnd.infotech.project+xml -application/vnd.innopath.wamp.notification -application/vnd.insors.igm igm -application/vnd.intercon.formnet xpw xpx -application/vnd.intergeo i2g -application/vnd.intertrust.digibox -application/vnd.intertrust.nncp -application/vnd.intu.qbo qbo -application/vnd.intu.qfx qfx -application/vnd.iptc.g2.catalogitem+xml -application/vnd.iptc.g2.conceptitem+xml -application/vnd.iptc.g2.knowledgeitem+xml -application/vnd.iptc.g2.newsitem+xml -application/vnd.iptc.g2.newsmessage+xml -application/vnd.iptc.g2.packageitem+xml -application/vnd.iptc.g2.planningitem+xml -application/vnd.ipunplugged.rcprofile rcprofile -application/vnd.irepository.package+xml irp -application/vnd.is-xpr xpr -application/vnd.isac.fcs fcs -application/vnd.jam jam -application/vnd.japannet-directory-service -application/vnd.japannet-jpnstore-wakeup -application/vnd.japannet-payment-wakeup -application/vnd.japannet-registration -application/vnd.japannet-registration-wakeup -application/vnd.japannet-setstore-wakeup -application/vnd.japannet-verification -application/vnd.japannet-verification-wakeup -application/vnd.jcp.javame.midlet-rms rms -application/vnd.jisp jisp -application/vnd.joost.joda-archive joda -application/vnd.jsk.isdn-ngn -application/vnd.kahootz ktz ktr -application/vnd.kde.karbon karbon -application/vnd.kde.kchart chrt -application/vnd.kde.kformula kfo -application/vnd.kde.kivio flw -application/vnd.kde.kontour kon -application/vnd.kde.kpresenter kpr kpt -application/vnd.kde.kspread ksp -application/vnd.kde.kword kwd kwt -application/vnd.kenameaapp htke -application/vnd.kidspiration kia -application/vnd.Kinar kne knp sdf -application/vnd.koan skp skd skm skt -application/vnd.kodak-descriptor sse -application/vnd.las.las+json lasjson -application/vnd.las.las+xml lasxml -application/vnd.liberty-request+xml -application/vnd.llamagraphics.life-balance.desktop lbd -application/vnd.llamagraphics.life-balance.exchange+xml lbe -application/vnd.lotus-1-2-3 123 wk4 wk3 wk1 -application/vnd.lotus-approach apr vew -application/vnd.lotus-freelance prz pre -application/vnd.lotus-notes nsf ntf ndl ns4 ns3 ns2 nsh nsg -application/vnd.lotus-organizer or3 or2 org -application/vnd.lotus-screencam scm -application/vnd.lotus-wordpro lwp sam -application/vnd.macports.portpkg portpkg -application/vnd.mapbox-vector-tile mvt -application/vnd.marlin.drm.actiontoken+xml -application/vnd.marlin.drm.conftoken+xml -application/vnd.marlin.drm.license+xml -application/vnd.marlin.drm.mdcf mdc -application/vnd.mason+json -application/vnd.maxmind.maxmind-db mmdb -application/vnd.mcd mcd -application/vnd.medcalcdata mc1 -application/vnd.mediastation.cdkey cdkey -application/vnd.meridian-slingshot -application/vnd.MFER mwf -application/vnd.mfmp mfm -application/vnd.micro+json -application/vnd.micrografx.flo flo -application/vnd.micrografx.igx igx -application/vnd.microsoft.portable-executable -application/vnd.microsoft.windows.thumbnail-cache -application/vnd.miele+json -application/vnd.mif mif -application/vnd.minisoft-hp3000-save -application/vnd.mitsubishi.misty-guard.trustweb -application/vnd.Mobius.DAF daf -application/vnd.Mobius.DIS dis -application/vnd.Mobius.MBK mbk -application/vnd.Mobius.MQY mqy -application/vnd.Mobius.MSL msl -application/vnd.Mobius.PLC plc -application/vnd.Mobius.TXF txf -application/vnd.mophun.application mpn -application/vnd.mophun.certificate mpc -application/vnd.motorola.flexsuite -application/vnd.motorola.flexsuite.adsi -application/vnd.motorola.flexsuite.fis -application/vnd.motorola.flexsuite.gotap -application/vnd.motorola.flexsuite.kmr -application/vnd.motorola.flexsuite.ttc -application/vnd.motorola.flexsuite.wem -application/vnd.motorola.iprm -application/vnd.mozilla.xul+xml xul -application/vnd.ms-3mfdocument 3mf -application/vnd.ms-artgalry cil -application/vnd.ms-asf asf -application/vnd.ms-cab-compressed cab -application/vnd.ms-excel xls xlm xla xlc xlt xlw -application/vnd.ms-excel.template.macroEnabled.12 xltm -application/vnd.ms-excel.addin.macroEnabled.12 xlam -application/vnd.ms-excel.sheet.binary.macroEnabled.12 xlsb -application/vnd.ms-excel.sheet.macroEnabled.12 xlsm -application/vnd.ms-fontobject eot -application/vnd.ms-htmlhelp chm -application/vnd.ms-ims ims -application/vnd.ms-lrm lrm -application/vnd.ms-office.activeX+xml -application/vnd.ms-officetheme thmx -application/vnd.ms-playready.initiator+xml -application/vnd.ms-powerpoint ppt pps pot -application/vnd.ms-powerpoint.addin.macroEnabled.12 ppam -application/vnd.ms-powerpoint.presentation.macroEnabled.12 pptm -application/vnd.ms-powerpoint.slide.macroEnabled.12 sldm -application/vnd.ms-powerpoint.slideshow.macroEnabled.12 ppsm -application/vnd.ms-powerpoint.template.macroEnabled.12 potm -application/vnd.ms-PrintDeviceCapabilities+xml -application/vnd.ms-PrintSchemaTicket+xml -application/vnd.ms-project mpp mpt -application/vnd.ms-tnef tnef tnf -application/vnd.ms-windows.devicepairing -application/vnd.ms-windows.nwprinting.oob -application/vnd.ms-windows.printerpairing -application/vnd.ms-windows.wsd.oob -application/vnd.ms-wmdrm.lic-chlg-req -application/vnd.ms-wmdrm.lic-resp -application/vnd.ms-wmdrm.meter-chlg-req -application/vnd.ms-wmdrm.meter-resp -application/vnd.ms-word.document.macroEnabled.12 docm -application/vnd.ms-word.template.macroEnabled.12 dotm -application/vnd.ms-works wcm wdb wks wps -application/vnd.ms-wpl wpl -application/vnd.ms-xpsdocument xps -application/vnd.msa-disk-image msa -application/vnd.mseq mseq -application/vnd.msign -application/vnd.multiad.creator crtr -application/vnd.multiad.creator.cif cif -application/vnd.music-niff -application/vnd.musician mus -application/vnd.muvee.style msty -application/vnd.mynfc taglet -application/vnd.ncd.control -application/vnd.ncd.reference -application/vnd.nearst.inv+json -application/vnd.nervana entity request bkm kcm -application/vnd.netfpx -# ntf: application/vnd.lotus-notes -application/vnd.nitf nitf -application/vnd.neurolanguage.nlu nlu -application/vnd.nintendo.nitro.rom nds -application/vnd.nintendo.snes.rom sfc smc -application/vnd.noblenet-directory nnd -application/vnd.noblenet-sealer nns -application/vnd.noblenet-web nnw -application/vnd.nokia.catalogs -application/vnd.nokia.conml+wbxml -application/vnd.nokia.conml+xml -application/vnd.nokia.iptv.config+xml -application/vnd.nokia.iSDS-radio-presets -application/vnd.nokia.landmark+wbxml -application/vnd.nokia.landmark+xml -application/vnd.nokia.landmarkcollection+xml -application/vnd.nokia.n-gage.ac+xml ac -application/vnd.nokia.n-gage.data ngdat -application/vnd.nokia.n-gage.symbian.install n-gage -application/vnd.nokia.ncd -application/vnd.nokia.pcd+wbxml -application/vnd.nokia.pcd+xml -application/vnd.nokia.radio-preset rpst -application/vnd.nokia.radio-presets rpss -application/vnd.novadigm.EDM edm -application/vnd.novadigm.EDX edx -application/vnd.novadigm.EXT ext -application/vnd.ntt-local.content-share -application/vnd.ntt-local.file-transfer -application/vnd.ntt-local.ogw_remote-access -application/vnd.ntt-local.sip-ta_remote -application/vnd.ntt-local.sip-ta_tcp_stream -application/vnd.oasis.opendocument.chart odc -application/vnd.oasis.opendocument.chart-template otc -application/vnd.oasis.opendocument.database odb -application/vnd.oasis.opendocument.formula odf -# otf: font/otf -application/vnd.oasis.opendocument.formula-template -application/vnd.oasis.opendocument.graphics odg -application/vnd.oasis.opendocument.graphics-template otg -application/vnd.oasis.opendocument.image odi -application/vnd.oasis.opendocument.image-template oti -application/vnd.oasis.opendocument.presentation odp -application/vnd.oasis.opendocument.presentation-template otp -application/vnd.oasis.opendocument.spreadsheet ods -application/vnd.oasis.opendocument.spreadsheet-template ots -application/vnd.oasis.opendocument.text odt -application/vnd.oasis.opendocument.text-master odm -application/vnd.oasis.opendocument.text-template ott -application/vnd.oasis.opendocument.text-web oth -application/vnd.obn -application/vnd.ocf+cbor -application/vnd.oftn.l10n+json -application/vnd.oipf.contentaccessdownload+xml -application/vnd.oipf.contentaccessstreaming+xml -application/vnd.oipf.cspg-hexbinary -application/vnd.oipf.dae.svg+xml -application/vnd.oipf.dae.xhtml+xml -application/vnd.oipf.mippvcontrolmessage+xml -application/vnd.oipf.pae.gem -application/vnd.oipf.spdiscovery+xml -application/vnd.oipf.spdlist+xml -application/vnd.oipf.ueprofile+xml -application/vnd.olpc-sugar xo -application/vnd.oma.bcast.associated-procedure-parameter+xml -application/vnd.oma.bcast.drm-trigger+xml -application/vnd.oma.bcast.imd+xml -application/vnd.oma.bcast.ltkm -application/vnd.oma.bcast.notification+xml -application/vnd.oma.bcast.provisioningtrigger -application/vnd.oma.bcast.sgboot -application/vnd.oma.bcast.sgdd+xml -application/vnd.oma.bcast.sgdu -application/vnd.oma.bcast.simple-symbol-container -application/vnd.oma.bcast.smartcard-trigger+xml -application/vnd.oma.bcast.sprov+xml -application/vnd.oma.bcast.stkm -application/vnd.oma.cab-address-book+xml -application/vnd.oma.cab-feature-handler+xml -application/vnd.oma.cab-pcc+xml -application/vnd.oma.cab-subs-invite+xml -application/vnd.oma.cab-user-prefs+xml -application/vnd.oma.dcd -application/vnd.oma.dcdc -application/vnd.oma.dd2+xml dd2 -application/vnd.oma.drm.risd+xml -application/vnd.oma.group-usage-list+xml -application/vnd.oma.lwm2m+json -application/vnd.oma.lwm2m+tlv -application/vnd.oma.pal+xml -application/vnd.oma.poc.detailed-progress-report+xml -application/vnd.oma.poc.final-report+xml -application/vnd.oma.poc.groups+xml -application/vnd.oma.poc.invocation-descriptor+xml -application/vnd.oma.poc.optimized-progress-report+xml -application/vnd.oma.push -application/vnd.oma.scidm.messages+xml -application/vnd.oma.xcap-directory+xml -application/vnd.oma-scws-config -application/vnd.oma-scws-http-request -application/vnd.oma-scws-http-response -application/vnd.omads-email+xml -application/vnd.omads-file+xml -application/vnd.omads-folder+xml -application/vnd.omaloc-supl-init -application/vnd.onepager tam -application/vnd.onepagertamp tamp -application/vnd.onepagertamx tamx -application/vnd.onepagertat tat -application/vnd.onepagertatp tatp -application/vnd.onepagertatx tatx -application/vnd.openblox.game+xml obgx -application/vnd.openblox.game-binary obg -application/vnd.openeye.oeb oeb -application/vnd.openofficeorg.extension oxt -application/vnd.openstreetmap.data+xml osm -application/vnd.openxmlformats-officedocument.custom-properties+xml -application/vnd.openxmlformats-officedocument.customXmlProperties+xml -application/vnd.openxmlformats-officedocument.drawing+xml -application/vnd.openxmlformats-officedocument.drawingml.chart+xml -application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml -application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml -application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml -application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml -application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml -application/vnd.openxmlformats-officedocument.extended-properties+xml -application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml -application/vnd.openxmlformats-officedocument.presentationml.comments+xml -application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml -application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml -application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml -application/vnd.openxmlformats-officedocument.presentationml.presProps+xml -application/vnd.openxmlformats-officedocument.presentationml.presentation pptx -application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml -application/vnd.openxmlformats-officedocument.presentationml.slide sldx -application/vnd.openxmlformats-officedocument.presentationml.slide+xml -application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml -application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml -application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml -application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx -application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml -application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml -application/vnd.openxmlformats-officedocument.presentationml.tags+xml -application/vnd.openxmlformats-officedocument.presentationml.template potx -application/vnd.openxmlformats-officedocument.presentationml.template.main+xml -application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx -application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx -application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml -application/vnd.openxmlformats-officedocument.theme+xml -application/vnd.openxmlformats-officedocument.themeOverride+xml -application/vnd.openxmlformats-officedocument.vmlDrawing -application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.document docx -application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx -application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml -application/vnd.openxmlformats-package.core-properties+xml -application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml -application/vnd.openxmlformats-package.relationships+xml -application/vnd.oracle.resource+json -application/vnd.orange.indata -application/vnd.osa.netdeploy ndc -application/vnd.osgeo.mapguide.package mgp -# jar: application/x-java-archive -application/vnd.osgi.bundle -application/vnd.osgi.dp dp -application/vnd.osgi.subsystem esa -application/vnd.otps.ct-kip+xml -application/vnd.oxli.countgraph oxlicg -application/vnd.pagerduty+json -application/vnd.palm prc pdb pqa oprc -application/vnd.panoply plp -application/vnd.paos+xml -application/vnd.pawaafile paw -application/vnd.pcos -application/vnd.pg.format str -application/vnd.pg.osasli ei6 -application/vnd.piaccess.application-license pil -application/vnd.picsel efif -application/vnd.pmi.widget wg -application/vnd.poc.group-advertisement+xml -application/vnd.pocketlearn plf -application/vnd.powerbuilder6 pbd -application/vnd.powerbuilder6-s -application/vnd.powerbuilder7 -application/vnd.powerbuilder7-s -application/vnd.powerbuilder75 -application/vnd.powerbuilder75-s -application/vnd.preminet preminet -application/vnd.previewsystems.box box vbox -application/vnd.proteus.magazine mgz -application/vnd.publishare-delta-tree qps -# pti: image/prs.pti -application/vnd.pvi.ptid1 ptid -application/vnd.pwg-multiplexed -application/vnd.pwg-xhtml-print+xml -application/vnd.qualcomm.brew-app-res bar -application/vnd.quarantainenet -application/vnd.Quark.QuarkXPress qxd qxt qwd qwt qxl qxb -application/vnd.quobject-quoxdocument quox quiz -application/vnd.radisys.moml+xml -application/vnd.radisys.msml-audit-conf+xml -application/vnd.radisys.msml-audit-conn+xml -application/vnd.radisys.msml-audit-dialog+xml -application/vnd.radisys.msml-audit-stream+xml -application/vnd.radisys.msml-audit+xml -application/vnd.radisys.msml-conf+xml -application/vnd.radisys.msml-dialog-base+xml -application/vnd.radisys.msml-dialog-fax-detect+xml -application/vnd.radisys.msml-dialog-fax-sendrecv+xml -application/vnd.radisys.msml-dialog-group+xml -application/vnd.radisys.msml-dialog-speech+xml -application/vnd.radisys.msml-dialog-transform+xml -application/vnd.radisys.msml-dialog+xml -application/vnd.radisys.msml+xml -application/vnd.rainstor.data tree -application/vnd.rapid -application/vnd.rar rar -application/vnd.realvnc.bed bed -application/vnd.recordare.musicxml mxl -application/vnd.recordare.musicxml+xml -application/vnd.RenLearn.rlprint -application/vnd.rig.cryptonote cryptonote -application/vnd.route66.link66+xml link66 -# gbr: application/rpki-ghostbusters -application/vnd.rs-274x -application/vnd.ruckus.download -application/vnd.s3sms -application/vnd.sailingtracker.track st -application/vnd.sbm.cid -application/vnd.sbm.mid2 -application/vnd.scribus scd sla slaz -application/vnd.sealed.3df s3df -application/vnd.sealed.csf scsf -application/vnd.sealed.doc sdoc sdo s1w -application/vnd.sealed.eml seml sem -application/vnd.sealed.mht smht smh -application/vnd.sealed.net -# spp: application/scvp-vp-response -application/vnd.sealed.ppt sppt s1p -application/vnd.sealed.tiff stif -application/vnd.sealed.xls sxls sxl s1e -# stm: audio/x-stm -application/vnd.sealedmedia.softseal.html stml s1h -application/vnd.sealedmedia.softseal.pdf spdf spd s1a -application/vnd.seemail see -application/vnd.sema sema -application/vnd.semd semd -application/vnd.semf semf -application/vnd.shana.informed.formdata ifm -application/vnd.shana.informed.formtemplate itp -application/vnd.shana.informed.interchange iif -application/vnd.shana.informed.package ipk -application/vnd.SimTech-MindMapper twd twds -application/vnd.siren+json -application/vnd.smaf mmf -application/vnd.smart.notebook notebook -application/vnd.smart.teacher teacher -application/vnd.software602.filler.form+xml fo -application/vnd.software602.filler.form-xml-zip zfo -application/vnd.solent.sdkm+xml sdkm sdkd -application/vnd.spotfire.dxp dxp -application/vnd.spotfire.sfs sfs -application/vnd.sss-cod -application/vnd.sss-dtf -application/vnd.sss-ntf -application/vnd.stepmania.package smzip -application/vnd.stepmania.stepchart sm -application/vnd.street-stream -application/vnd.sun.wadl+xml wadl -application/vnd.sus-calendar sus susp -application/vnd.svd -application/vnd.swiftview-ics -application/vnd.syncml+xml xsm -application/vnd.syncml.dm+wbxml bdm -application/vnd.syncml.dm+xml xdm -application/vnd.syncml.dm.notification -application/vnd.syncml.dmddf+wbxml -application/vnd.syncml.dmddf+xml ddf -application/vnd.syncml.dmtnds+wbxml -application/vnd.syncml.dmtnds+xml -application/vnd.syncml.ds.notification -application/vnd.tableschema+json -application/vnd.tao.intent-module-archive tao -application/vnd.tcpdump.pcap pcap cap dmp -application/vnd.theqvd qvd -application/vnd.tmd.mediaflex.api+xml -application/vnd.tml vfr viaframe -application/vnd.tmobile-livetv tmo -application/vnd.tri.onesource -application/vnd.trid.tpt tpt -application/vnd.triscape.mxs mxs -application/vnd.trueapp tra -application/vnd.truedoc -# cab: application/vnd.ms-cab-compressed -application/vnd.ubisoft.webplayer -application/vnd.ufdl ufdl ufd frm -application/vnd.uiq.theme utz -application/vnd.umajin umj -application/vnd.unity unityweb -application/vnd.uoml+xml uoml uo -application/vnd.uplanet.alert -application/vnd.uplanet.alert-wbxml -application/vnd.uplanet.bearer-choice -application/vnd.uplanet.bearer-choice-wbxml -application/vnd.uplanet.cacheop -application/vnd.uplanet.cacheop-wbxml -application/vnd.uplanet.channel -application/vnd.uplanet.channel-wbxml -application/vnd.uplanet.list -application/vnd.uplanet.list-wbxml -application/vnd.uplanet.listcmd -application/vnd.uplanet.listcmd-wbxml -application/vnd.uplanet.signal -application/vnd.uri-map urim urimap -application/vnd.valve.source.material vmt -application/vnd.vcx vcx -# sxi: application/vnd.sun.xml.impress -application/vnd.vd-study mxi study-inter model-inter -# mcd: application/vnd.mcd -application/vnd.vectorworks vwx -application/vnd.vel+json -application/vnd.verimatrix.vcas -application/vnd.vidsoft.vidconference vsc -application/vnd.visio vsd vst vsw vss -application/vnd.visionary vis -# vsc: application/vnd.vidsoft.vidconference -application/vnd.vividence.scriptfile -application/vnd.vsf vsf -application/vnd.wap.sic sic -application/vnd.wap.slc slc -application/vnd.wap.wbxml wbxml -application/vnd.wap.wmlc wmlc -application/vnd.wap.wmlscriptc wmlsc -application/vnd.webturbo wtb -application/vnd.wfa.p2p p2p -application/vnd.wfa.wsc wsc -application/vnd.windows.devicepairing -application/vnd.wmc wmc -application/vnd.wmf.bootstrap -# nb: application/mathematica for now -application/vnd.wolfram.mathematica -application/vnd.wolfram.mathematica.package m -application/vnd.wolfram.player nbp -application/vnd.wordperfect wpd -application/vnd.wqd wqd -application/vnd.wrq-hp3000-labelled -application/vnd.wt.stf stf -application/vnd.wv.csp+xml -application/vnd.wv.csp+wbxml wv -application/vnd.wv.ssp+xml -application/vnd.xacml+json -application/vnd.xara xar -application/vnd.xfdl xfdl xfd -application/vnd.xfdl.webform -application/vnd.xmi+xml -application/vnd.xmpie.cpkg cpkg -application/vnd.xmpie.dpkg dpkg -# dpkg: application/vnd.xmpie.dpkg -application/vnd.xmpie.plan -application/vnd.xmpie.ppkg ppkg -application/vnd.xmpie.xlim xlim -application/vnd.yamaha.hv-dic hvd -application/vnd.yamaha.hv-script hvs -application/vnd.yamaha.hv-voice hvp -application/vnd.yamaha.openscoreformat osf -application/vnd.yamaha.openscoreformat.osfpvg+xml -application/vnd.yamaha.remote-setup -application/vnd.yamaha.smaf-audio saf -application/vnd.yamaha.smaf-phrase spf -application/vnd.yamaha.through-ngn -application/vnd.yamaha.tunnel-udpencap -application/vnd.yaoweme yme -application/vnd.yellowriver-custom-menu cmp -application/vnd.zul zir zirz -application/vnd.zzazz.deck+xml zaz -application/voicexml+xml vxml -application/vq-rtcp-xr -application/watcherinfo+xml wif -application/whoispp-query -application/whoispp-response -application/widget wgt -application/wita -application/wordperfect5.1 -application/wsdl+xml wsdl -application/wspolicy+xml wspolicy -# yes, this *is* IANA registered despite of x- -application/x-www-form-urlencoded -application/x400-bp -application/xacml+xml -application/xcap-att+xml xav -application/xcap-caps+xml xca -application/xcap-diff+xml xdf -application/xcap-el+xml xel -application/xcap-error+xml xer -application/xcap-ns+xml xns -application/xcon-conference-info-diff+xml -application/xcon-conference-info+xml -application/xenc+xml -application/xhtml+xml xhtml xhtm xht -# xml, xsd, rng: text/xml -application/xml -# mod: audio/x-mod -application/xml-dtd dtd -# ent: text/xml-external-parsed-entity -application/xml-external-parsed-entity -application/xml-patch+xml -application/xmpp+xml -application/xop+xml xop -application/xslt+xml xsl xslt -application/xv+xml mxml xhvml xvml xvm -application/yang yang -application/yang-data+json -application/yang-data+xml -application/yang-patch+json -application/yang-patch+xml -application/yin+xml yin -application/zip zip -application/zlib -audio/1d-interleaved-parityfec -audio/32kadpcm 726 -# 3gp, 3gpp: video/3gpp -audio/3gpp -# 3g2, 3gpp2: video/3gpp2 -audio/3gpp2 -audio/ac3 ac3 -audio/AMR amr -audio/AMR-WB awb -audio/amr-wb+ -audio/aptx -audio/asc acn -# aa3, omg: audio/ATRAC3 -audio/ATRAC-ADVANCED-LOSSLESS aal -# aa3, omg: audio/ATRAC3 -audio/ATRAC-X atx -audio/ATRAC3 at3 aa3 omg -audio/basic au snd -audio/BV16 -audio/BV32 -audio/clearmode -audio/CN -audio/DAT12 -audio/dls dls -audio/dsr-es201108 -audio/dsr-es202050 -audio/dsr-es202211 -audio/dsr-es202212 -audio/DV -audio/DVI4 -audio/eac3 -audio/encaprtp -audio/EVRC evc -# qcp: audio/qcelp -audio/EVRC-QCP -audio/EVRC0 -audio/EVRC1 -audio/EVRCB evb -audio/EVRCB0 -audio/EVRCB1 -audio/EVRCNW enw -audio/EVRCNW0 -audio/EVRCNW1 -audio/EVRCWB evw -audio/EVRCWB0 -audio/EVRCWB1 -audio/EVS -audio/example -audio/fwdred -audio/G711-0 -audio/G719 -audio/G722 -audio/G7221 -audio/G723 -audio/G726-16 -audio/G726-24 -audio/G726-32 -audio/G726-40 -audio/G728 -audio/G729 -audio/G7291 -audio/G729D -audio/G729E -audio/GSM -audio/GSM-EFR -audio/GSM-HR-08 -audio/iLBC lbc -audio/ip-mr_v2.5 -# wav: audio/x-wav -audio/L16 l16 -audio/L20 -audio/L24 -audio/L8 -audio/LPC -audio/MELP -audio/MELP600 -audio/MELP1200 -audio/MELP2400 -audio/mobile-xmf mxmf -# mp4, mpg4: video/mp4, see RFC 4337 -audio/mp4 m4a -audio/MP4A-LATM -audio/MPA -audio/mpa-robust -audio/mpeg mp3 mpga mp1 mp2 -audio/mpeg4-generic -audio/ogg oga ogg opus spx -audio/opus -audio/parityfec -audio/PCMA -audio/PCMA-WB -audio/PCMU -audio/PCMU-WB -audio/prs.sid sid psid -audio/qcelp qcp -audio/raptorfec -audio/RED -audio/rtp-enc-aescm128 -audio/rtp-midi -audio/rtploopback -audio/rtx -audio/SMV smv -# qcp: audio/qcelp, see RFC 3625 -audio/SMV-QCP -audio/SMV0 -# mid: audio/midi -audio/sp-midi -audio/speex -audio/t140c -audio/t38 -audio/telephone-event -audio/tone -audio/UEMCLIP -audio/ulpfec -audio/VDVI -audio/VMR-WB -audio/vnd.3gpp.iufp -audio/vnd.4SB -audio/vnd.audikoz koz -audio/vnd.CELP -audio/vnd.cisco.nse -audio/vnd.cmles.radio-events -audio/vnd.cns.anp1 -audio/vnd.cns.inf1 -audio/vnd.dece.audio uva uvva -audio/vnd.digital-winds eol -audio/vnd.dlna.adts -audio/vnd.dolby.heaac.1 -audio/vnd.dolby.heaac.2 -audio/vnd.dolby.mlp mlp -audio/vnd.dolby.mps -audio/vnd.dolby.pl2 -audio/vnd.dolby.pl2x -audio/vnd.dolby.pl2z -audio/vnd.dolby.pulse.1 -audio/vnd.dra -# wav: audio/x-wav, cpt: application/mac-compactpro -audio/vnd.dts dts -audio/vnd.dts.hd dtshd -# dvb: video/vnd.dvb.file -audio/vnd.dvb.file -audio/vnd.everad.plj plj -# rm: audio/x-pn-realaudio -audio/vnd.hns.audio -audio/vnd.lucent.voice lvp -audio/vnd.ms-playready.media.pya pya -# mxmf: audio/mobile-xmf -audio/vnd.nokia.mobile-xmf -audio/vnd.nortel.vbk vbk -audio/vnd.nuera.ecelp4800 ecelp4800 -audio/vnd.nuera.ecelp7470 ecelp7470 -audio/vnd.nuera.ecelp9600 ecelp9600 -audio/vnd.octel.sbc -# audio/vnd.qcelp deprecated in favour of audio/qcelp -audio/vnd.rhetorex.32kadpcm -audio/vnd.rip rip -audio/vnd.sealedmedia.softseal.mpeg smp3 smp s1m -audio/vnd.vmx.cvsd -audio/vorbis -audio/vorbis-config -font/collection ttc -font/otf otf -font/sfnt -font/ttf ttf -font/woff woff -font/woff2 woff2 -image/bmp bmp dib -image/cgm cgm -image/dicom-rle drle -image/emf emf -image/example -image/fits fits fit fts -image/g3fax -image/gif gif -image/ief ief -image/jls jls -image/jp2 jp2 jpg2 -image/jpeg jpg jpeg jpe jfif -image/jpm jpm jpgm -image/jpx jpx jpf -image/ktx ktx -image/naplps -image/png png -image/prs.btif btif btf -image/prs.pti pti -image/pwg-raster -image/svg+xml svg svgz -image/t38 t38 -image/tiff tiff tif -image/tiff-fx tfx -image/vnd.adobe.photoshop psd -image/vnd.airzip.accelerator.azv azv -image/vnd.cns.inf2 -image/vnd.dece.graphic uvi uvvi uvg uvvg -image/vnd.djvu djvu djv -# sub: text/vnd.dvb.subtitle -image/vnd.dvb.subtitle -image/vnd.dwg dwg -image/vnd.dxf dxf -image/vnd.fastbidsheet fbs -image/vnd.fpx fpx -image/vnd.fst fst -image/vnd.fujixerox.edmics-mmr mmr -image/vnd.fujixerox.edmics-rlc rlc -image/vnd.globalgraphics.pgb pgb -image/vnd.microsoft.icon ico -image/vnd.mix -image/vnd.mozilla.apng apng -image/vnd.ms-modi mdi -image/vnd.net-fpx -image/vnd.radiance hdr rgbe xyze -image/vnd.sealed.png spng spn s1n -image/vnd.sealedmedia.softseal.gif sgif sgi s1g -image/vnd.sealedmedia.softseal.jpg sjpg sjp s1j -image/vnd.svf -image/vnd.tencent.tap tap -image/vnd.valve.source.texture vtf -image/vnd.wap.wbmp wbmp -image/vnd.xiff xif -image/vnd.zbrush.pcx pcx -image/wmf wmf -message/CPIM -message/delivery-status -message/disposition-notification -message/example -message/external-body -message/feedback-report -message/global u8msg -message/global-delivery-status u8dsn -message/global-disposition-notification u8mdn -message/global-headers u8hdr -message/http -# cl: application/simple-filter+xml -message/imdn+xml -# message/news obsoleted by message/rfc822 -message/partial -message/rfc822 eml mail art -message/s-http -message/sip -message/sipfrag -message/tracking-status -message/vnd.si.simp -# wsc: application/vnd.wfa.wsc -message/vnd.wfa.wsc -model/example -model/gltf+json gltf -model/iges igs iges -model/mesh msh mesh silo -model/vnd.collada+xml dae -model/vnd.dwf dwf -# 3dml, 3dm: text/vnd.in3d.3dml -model/vnd.flatland.3dml -model/vnd.gdl gdl gsm win dor lmp rsm msm ism -model/vnd.gs-gdl -model/vnd.gtw gtw -model/vnd.moml+xml moml -model/vnd.mts mts -model/vnd.opengex ogex -model/vnd.parasolid.transmit.binary x_b xmt_bin -model/vnd.parasolid.transmit.text x_t xmt_txt -model/vnd.rosette.annotated-data-model -model/vnd.valve.source.compiled-map bsp -model/vnd.vtu vtu -model/vrml wrl vrml -# x3db: model/x3d+xml -model/x3d+fastinfoset -# x3d: application/vnd.hzn-3d-crossword -model/x3d+xml x3db -model/x3d-vrml x3dv x3dvz -multipart/alternative -multipart/appledouble -multipart/byteranges -multipart/digest -multipart/encrypted -multipart/form-data -multipart/header-set -multipart/mixed -multipart/parallel -multipart/related -multipart/report -multipart/signed -multipart/vnd.bint.med-plus bmed -multipart/voice-message vpm -multipart/x-mixed-replace -text/1d-interleaved-parityfec -text/cache-manifest appcache manifest -text/calendar ics ifb -text/css css -text/csv csv -text/csv-schema csvs -text/directory -text/dns soa zone -text/encaprtp -# text/ecmascript obsoleted by application/ecmascript -text/enriched -text/example -text/fwdred -text/grammar-ref-list -text/html html htm -# text/javascript obsoleted by application/javascript -text/jcr-cnd cnd -text/markdown markdown md -text/mizar miz -text/n3 n3 -text/parameters -text/parityfec -text/plain txt asc text pm el c h cc hh cxx hxx f90 conf log -text/provenance-notation provn -text/prs.fallenstein.rst rst -text/prs.lines.tag tag dsc -text/prs.prop.logic -text/raptorfec -text/RED -text/rfc822-headers -text/richtext rtx -# rtf: application/rtf -text/rtf -text/rtp-enc-aescm128 -text/rtploopback -text/rtx -text/sgml sgml sgm -text/strings -text/t140 -text/tab-separated-values tsv -text/troff t tr roff -text/turtle ttl -text/ulpfec -text/uri-list uris uri -text/vcard vcf vcard -text/vnd.a a -text/vnd.abc abc -text/vnd.ascii-art ascii -# curl: application/vnd.curl -text/vnd.curl -text/vnd.debian.copyright copyright -text/vnd.DMClientScript dms -text/vnd.dvb.subtitle sub -text/vnd.esmertec.theme-descriptor jtd -text/vnd.fly fly -text/vnd.fmi.flexstor flx -text/vnd.graphviz gv dot -text/vnd.in3d.3dml 3dml 3dm -text/vnd.in3d.spot spot spo -text/vnd.IPTC.NewsML -text/vnd.IPTC.NITF -text/vnd.latex-z -text/vnd.motorola.reflex -text/vnd.ms-mediapackage mpf -text/vnd.net2phone.commcenter.command ccc -text/vnd.radisys.msml-basic-layout -text/vnd.si.uricatalogue uric -text/vnd.sun.j2me.app-descriptor jad -text/vnd.trolltech.linguist ts -text/vnd.wap.si si -text/vnd.wap.sl sl -text/vnd.wap.wml wml -text/vnd.wap.wmlscript wmls -text/xml xml xsd rng -text/xml-external-parsed-entity ent -video/1d-interleaved-parityfec -video/3gpp 3gp 3gpp -video/3gpp2 3g2 3gpp2 -video/3gpp-tt -video/BMPEG -video/BT656 -video/CelB -video/DV -video/encaprtp -video/example -video/H261 -video/H263 -video/H263-1998 -video/H263-2000 -video/H264 -video/H264-RCDO -video/H264-SVC -video/H265 -video/iso.segment m4s -video/JPEG -video/jpeg2000 -video/mj2 mj2 mjp2 -video/MP1S -video/MP2P -video/MP2T -video/mp4 mp4 mpg4 m4v -video/MP4V-ES -video/mpeg mpeg mpg mpe m1v m2v -video/mpeg4-generic -video/MPV -video/nv -video/ogg ogv -video/parityfec -video/pointer -video/quicktime mov qt -video/raptorfec -video/raw -video/rtp-enc-aescm128 -video/rtploopback -video/rtx -video/SMPTE292M -video/ulpfec -video/vc1 -video/vnd.CCTV -video/vnd.dece.hd uvh uvvh -video/vnd.dece.mobile uvm uvvm -video/vnd.dece.mp4 uvu uvvu -video/vnd.dece.pd uvp uvvp -video/vnd.dece.sd uvs uvvs -video/vnd.dece.video uvv uvvv -video/vnd.directv.mpeg -video/vnd.directv.mpeg-tts -video/vnd.dlna.mpeg-tts -video/vnd.dvb.file dvb -video/vnd.fvt fvt -# rm: audio/x-pn-realaudio -video/vnd.hns.video -video/vnd.iptvforum.1dparityfec-1010 -video/vnd.iptvforum.1dparityfec-2005 -video/vnd.iptvforum.2dparityfec-1010 -video/vnd.iptvforum.2dparityfec-2005 -video/vnd.iptvforum.ttsavc -video/vnd.iptvforum.ttsmpeg2 -video/vnd.motorola.video -video/vnd.motorola.videop -video/vnd.mpegurl mxu m4u -video/vnd.ms-playready.media.pyv pyv -video/vnd.nokia.interleaved-multimedia nim -video/vnd.nokia.videovoip -# mp4: video/mp4 -video/vnd.objectvideo -video/vnd.radgamettools.bink bik bk2 -video/vnd.radgamettools.smacker smk -video/vnd.sealed.mpeg1 smpg s11 -# smpg: video/vnd.sealed.mpeg1 -video/vnd.sealed.mpeg4 s14 -video/vnd.sealed.swf sswf ssw -video/vnd.sealedmedia.softseal.mov smov smo s1q -# uvu, uvvu: video/vnd.dece.mp4 -video/vnd.uvvu.mp4 -video/vnd.vivo viv -video/VP8 - -# Non-IANA types - -application/mac-compactpro cpt -application/metalink+xml metalink -application/owl+xml owx -application/rss+xml rss -application/vnd.android.package-archive apk -application/vnd.oma.dd+xml dd -application/vnd.oma.drm.content dcf -# odf: application/vnd.oasis.opendocument.formula -application/vnd.oma.drm.dcf o4a o4v -application/vnd.oma.drm.message dm -application/vnd.oma.drm.rights+wbxml drc -application/vnd.oma.drm.rights+xml dr -application/vnd.sun.xml.calc sxc -application/vnd.sun.xml.calc.template stc -application/vnd.sun.xml.draw sxd -application/vnd.sun.xml.draw.template std -application/vnd.sun.xml.impress sxi -application/vnd.sun.xml.impress.template sti -application/vnd.sun.xml.math sxm -application/vnd.sun.xml.writer sxw -application/vnd.sun.xml.writer.global sxg -application/vnd.sun.xml.writer.template stw -application/vnd.symbian.install sis -application/vnd.wap.mms-message mms -application/x-annodex anx -application/x-bcpio bcpio -application/x-bittorrent torrent -application/x-bzip2 bz2 -application/x-cdlink vcd -application/x-chrome-extension crx -application/x-cpio cpio -application/x-csh csh -application/x-director dcr dir dxr -application/x-dvi dvi -application/x-futuresplash spl -application/x-gtar gtar -application/x-hdf hdf -application/x-java-archive jar -application/x-java-jnlp-file jnlp -application/x-java-pack200 pack -application/x-killustrator kil -application/x-latex latex -application/x-netcdf nc cdf -application/x-perl pl -application/x-rpm rpm -application/x-sh sh -application/x-shar shar -application/x-stuffit sit -application/x-sv4cpio sv4cpio -application/x-sv4crc sv4crc -application/x-tar tar -application/x-tcl tcl -application/x-tex tex -application/x-texinfo texinfo texi -application/x-troff-man man 1 2 3 4 5 6 7 8 -application/x-troff-me me -application/x-troff-ms ms -application/x-ustar ustar -application/x-wais-source src -application/x-xpinstall xpi -application/x-xspf+xml xspf -application/x-xz xz -audio/midi mid midi kar -audio/x-aiff aif aiff aifc -audio/x-annodex axa -audio/x-flac flac -audio/x-matroska mka -audio/x-mod mod ult uni m15 mtm 669 med -audio/x-mpegurl m3u -audio/x-ms-wax wax -audio/x-ms-wma wma -audio/x-pn-realaudio ram rm -audio/x-realaudio ra -audio/x-s3m s3m -audio/x-stm stm -audio/x-wav wav -chemical/x-xyz xyz -image/webp webp -image/x-cmu-raster ras -image/x-portable-anymap pnm -image/x-portable-bitmap pbm -image/x-portable-graymap pgm -image/x-portable-pixmap ppm -image/x-rgb rgb -image/x-targa tga -image/x-xbitmap xbm -image/x-xpixmap xpm -image/x-xwindowdump xwd -text/html-sandboxed sandboxed -text/x-pod pod -text/x-setext etx -video/webm webm -video/x-annodex axv -video/x-flv flv -video/x-javafx fxm -video/x-matroska mkv -video/x-matroska-3d mk3d -video/x-ms-asf asx -video/x-ms-wm wm -video/x-ms-wmv wmv -video/x-ms-wmx wmx -video/x-ms-wvx wvx -video/x-msvideo avi -video/x-sgi-movie movie -x-conference/x-cooltalk ice -x-epoc/x-sisx-app sisx diff --git a/cmd/swarm/run_test.go b/cmd/swarm/run_test.go deleted file mode 100644 index b68a1e897..000000000 --- a/cmd/swarm/run_test.go +++ /dev/null @@ -1,502 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "context" - "crypto/ecdsa" - "flag" - "fmt" - "io/ioutil" - "net" - "os" - "path" - "path/filepath" - "runtime" - "sync" - "syscall" - "testing" - "time" - - "github.com/docker/docker/pkg/reexec" - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/internal/cmdtest" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/swarm" - "github.com/ethereum/go-ethereum/swarm/api" - swarmhttp "github.com/ethereum/go-ethereum/swarm/api/http" -) - -var loglevel = flag.Int("loglevel", 3, "verbosity of logs") - -func init() { - // Run the app if we've been exec'd as "swarm-test" in runSwarm. - reexec.Register("swarm-test", func() { - if err := app.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - os.Exit(0) - }) -} - -const clusterSize = 3 - -func serverFunc(api *api.API) swarmhttp.TestServer { - return swarmhttp.NewServer(api, "") -} -func TestMain(m *testing.M) { - // check if we have been reexec'd - if reexec.Init() { - return - } - os.Exit(m.Run()) -} - -func runSwarm(t *testing.T, args ...string) *cmdtest.TestCmd { - tt := cmdtest.NewTestCmd(t, nil) - - found := false - for _, v := range args { - if v == "--bootnodes" { - found = true - break - } - } - - if !found { - args = append([]string{"--bootnodes", ""}, args...) - } - - // Boot "swarm". This actually runs the test binary but the TestMain - // function will prevent any tests from running. - tt.Run("swarm-test", args...) - - return tt -} - -type testCluster struct { - Nodes []*testNode - TmpDir string -} - -// newTestCluster starts a test swarm cluster of the given size. -// -// A temporary directory is created and each node gets a data directory inside -// it. -// -// Each node listens on 127.0.0.1 with random ports for both the HTTP and p2p -// ports (assigned by first listening on 127.0.0.1:0 and then passing the ports -// as flags). -// -// When starting more than one node, they are connected together using the -// admin SetPeer RPC method. - -func newTestCluster(t *testing.T, size int) *testCluster { - cluster := &testCluster{} - defer func() { - if t.Failed() { - cluster.Shutdown() - } - }() - - tmpdir, err := ioutil.TempDir("", "swarm-test") - if err != nil { - t.Fatal(err) - } - cluster.TmpDir = tmpdir - - // start the nodes - cluster.StartNewNodes(t, size) - - if size == 1 { - return cluster - } - - // connect the nodes together - for _, node := range cluster.Nodes { - if err := node.Client.Call(nil, "admin_addPeer", cluster.Nodes[0].Enode); err != nil { - t.Fatal(err) - } - } - - // wait until all nodes have the correct number of peers -outer: - for _, node := range cluster.Nodes { - var peers []*p2p.PeerInfo - for start := time.Now(); time.Since(start) < time.Minute; time.Sleep(50 * time.Millisecond) { - if err := node.Client.Call(&peers, "admin_peers"); err != nil { - t.Fatal(err) - } - if len(peers) == len(cluster.Nodes)-1 { - continue outer - } - } - t.Fatalf("%s only has %d / %d peers", node.Name, len(peers), len(cluster.Nodes)-1) - } - - return cluster -} - -func (c *testCluster) Shutdown() { - c.Stop() - c.Cleanup() -} - -func (c *testCluster) Stop() { - for _, node := range c.Nodes { - node.Shutdown() - } -} - -func (c *testCluster) StartNewNodes(t *testing.T, size int) { - c.Nodes = make([]*testNode, 0, size) - - errors := make(chan error, size) - nodes := make(chan *testNode, size) - for i := 0; i < size; i++ { - go func(nodeIndex int) { - dir := filepath.Join(c.TmpDir, fmt.Sprintf("swarm%02d", nodeIndex)) - if err := os.Mkdir(dir, 0700); err != nil { - errors <- err - return - } - - node := newTestNode(t, dir) - node.Name = fmt.Sprintf("swarm%02d", nodeIndex) - nodes <- node - }(i) - } - - for i := 0; i < size; i++ { - select { - case node := <-nodes: - c.Nodes = append(c.Nodes, node) - case err := <-errors: - t.Error(err) - } - } - - if t.Failed() { - c.Shutdown() - t.FailNow() - } -} - -func (c *testCluster) StartExistingNodes(t *testing.T, size int, bzzaccount string) { - c.Nodes = make([]*testNode, 0, size) - for i := 0; i < size; i++ { - dir := filepath.Join(c.TmpDir, fmt.Sprintf("swarm%02d", i)) - node := existingTestNode(t, dir, bzzaccount) - node.Name = fmt.Sprintf("swarm%02d", i) - - c.Nodes = append(c.Nodes, node) - } -} - -func (c *testCluster) Cleanup() { - os.RemoveAll(c.TmpDir) -} - -type testNode struct { - Name string - Addr string - URL string - Enode string - Dir string - IpcPath string - PrivateKey *ecdsa.PrivateKey - Client *rpc.Client - Cmd *cmdtest.TestCmd -} - -const testPassphrase = "swarm-test-passphrase" - -func getTestAccount(t *testing.T, dir string) (conf *node.Config, account accounts.Account) { - // create key - conf = &node.Config{ - DataDir: dir, - IPCPath: "bzzd.ipc", - NoUSB: true, - } - n, err := node.New(conf) - if err != nil { - t.Fatal(err) - } - account, err = n.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore).NewAccount(testPassphrase) - if err != nil { - t.Fatal(err) - } - - // use a unique IPCPath when running tests on Windows - if runtime.GOOS == "windows" { - conf.IPCPath = fmt.Sprintf("bzzd-%s.ipc", account.Address.String()) - } - - return conf, account -} - -func existingTestNode(t *testing.T, dir string, bzzaccount string) *testNode { - conf, _ := getTestAccount(t, dir) - node := &testNode{Dir: dir} - - // use a unique IPCPath when running tests on Windows - if runtime.GOOS == "windows" { - conf.IPCPath = fmt.Sprintf("bzzd-%s.ipc", bzzaccount) - } - - // assign ports - ports, err := getAvailableTCPPorts(2) - if err != nil { - t.Fatal(err) - } - p2pPort := ports[0] - httpPort := ports[1] - - // start the node - node.Cmd = runSwarm(t, - "--bootnodes", "", - "--port", p2pPort, - "--nat", "extip:127.0.0.1", - "--datadir", dir, - "--ipcpath", conf.IPCPath, - "--ens-api", "", - "--bzzaccount", bzzaccount, - "--bzznetworkid", "321", - "--bzzport", httpPort, - "--verbosity", fmt.Sprint(*loglevel), - ) - node.Cmd.InputLine(testPassphrase) - defer func() { - if t.Failed() { - node.Shutdown() - } - }() - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // ensure that all ports have active listeners - // so that the next node will not get the same - // when calling getAvailableTCPPorts - err = waitTCPPorts(ctx, ports...) - if err != nil { - t.Fatal(err) - } - - // wait for the node to start - for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { - node.Client, err = rpc.Dial(conf.IPCEndpoint()) - if err == nil { - break - } - } - if node.Client == nil { - t.Fatal(err) - } - - // load info - var info swarm.Info - if err := node.Client.Call(&info, "bzz_info"); err != nil { - t.Fatal(err) - } - node.Addr = net.JoinHostPort("127.0.0.1", info.Port) - node.URL = "http://" + node.Addr - - var nodeInfo p2p.NodeInfo - if err := node.Client.Call(&nodeInfo, "admin_nodeInfo"); err != nil { - t.Fatal(err) - } - node.Enode = nodeInfo.Enode - node.IpcPath = conf.IPCPath - return node -} - -func newTestNode(t *testing.T, dir string) *testNode { - - conf, account := getTestAccount(t, dir) - ks := keystore.NewKeyStore(path.Join(dir, "keystore"), 1<<18, 1) - - pk := decryptStoreAccount(ks, account.Address.Hex(), []string{testPassphrase}) - - node := &testNode{Dir: dir, PrivateKey: pk} - - // assign ports - ports, err := getAvailableTCPPorts(2) - if err != nil { - t.Fatal(err) - } - p2pPort := ports[0] - httpPort := ports[1] - - // start the node - node.Cmd = runSwarm(t, - "--bootnodes", "", - "--port", p2pPort, - "--nat", "extip:127.0.0.1", - "--datadir", dir, - "--ipcpath", conf.IPCPath, - "--ens-api", "", - "--bzzaccount", account.Address.String(), - "--bzznetworkid", "321", - "--bzzport", httpPort, - "--verbosity", fmt.Sprint(*loglevel), - ) - node.Cmd.InputLine(testPassphrase) - defer func() { - if t.Failed() { - node.Shutdown() - } - }() - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // ensure that all ports have active listeners - // so that the next node will not get the same - // when calling getAvailableTCPPorts - err = waitTCPPorts(ctx, ports...) - if err != nil { - t.Fatal(err) - } - - // wait for the node to start - for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { - node.Client, err = rpc.Dial(conf.IPCEndpoint()) - if err == nil { - break - } - } - if node.Client == nil { - t.Fatal(err) - } - - // load info - var info swarm.Info - if err := node.Client.Call(&info, "bzz_info"); err != nil { - t.Fatal(err) - } - node.Addr = net.JoinHostPort("127.0.0.1", info.Port) - node.URL = "http://" + node.Addr - - var nodeInfo p2p.NodeInfo - if err := node.Client.Call(&nodeInfo, "admin_nodeInfo"); err != nil { - t.Fatal(err) - } - node.Enode = nodeInfo.Enode - node.IpcPath = conf.IPCPath - return node -} - -func (n *testNode) Shutdown() { - if n.Cmd != nil { - n.Cmd.Kill() - } -} - -// getAvailableTCPPorts returns a set of ports that -// nothing is listening on at the time. -// -// Function assignTCPPort cannot be called in sequence -// and guardantee that the same port will be returned in -// different calls as the listener is closed within the function, -// not after all listeners are started and selected unique -// available ports. -func getAvailableTCPPorts(count int) (ports []string, err error) { - for i := 0; i < count; i++ { - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return nil, err - } - // defer close in the loop to be sure the same port will not - // be selected in the next iteration - defer l.Close() - - _, port, err := net.SplitHostPort(l.Addr().String()) - if err != nil { - return nil, err - } - ports = append(ports, port) - } - return ports, nil -} - -// waitTCPPorts blocks until tcp connections can be -// established on all provided ports. It runs all -// ports dialers in parallel, and returns the first -// encountered error. -// See waitTCPPort also. -func waitTCPPorts(ctx context.Context, ports ...string) error { - var err error - // mu locks err variable that is assigned in - // other goroutines - var mu sync.Mutex - - // cancel is canceling all goroutines - // when the firs error is returned - // to prevent unnecessary waiting - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - var wg sync.WaitGroup - for _, port := range ports { - wg.Add(1) - go func(port string) { - defer wg.Done() - - e := waitTCPPort(ctx, port) - - mu.Lock() - defer mu.Unlock() - if e != nil && err == nil { - err = e - cancel() - } - }(port) - } - wg.Wait() - - return err -} - -// waitTCPPort blocks until tcp connection can be established -// ona provided port. It has a 3 minute timeout as maximum, -// to prevent long waiting, but it can be shortened with -// a provided context instance. Dialer has a 10 second timeout -// in every iteration, and connection refused error will be -// retried in 100 milliseconds periods. -func waitTCPPort(ctx context.Context, port string) error { - ctx, cancel := context.WithTimeout(ctx, 3*time.Minute) - defer cancel() - - for { - c, err := (&net.Dialer{Timeout: 10 * time.Second}).DialContext(ctx, "tcp", "127.0.0.1:"+port) - if err != nil { - if operr, ok := err.(*net.OpError); ok { - if syserr, ok := operr.Err.(*os.SyscallError); ok && syserr.Err == syscall.ECONNREFUSED { - time.Sleep(100 * time.Millisecond) - continue - } - } - return err - } - return c.Close() - } -} diff --git a/cmd/swarm/swarm-smoke/feed_upload_and_sync.go b/cmd/swarm/swarm-smoke/feed_upload_and_sync.go deleted file mode 100644 index b5ffc43d2..000000000 --- a/cmd/swarm/swarm-smoke/feed_upload_and_sync.go +++ /dev/null @@ -1,291 +0,0 @@ -package main - -import ( - "bytes" - "crypto/md5" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "strings" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/swarm/storage/feed" - "github.com/ethereum/go-ethereum/swarm/testutil" - "github.com/pborman/uuid" - cli "gopkg.in/urfave/cli.v1" -) - -const ( - feedRandomDataLength = 8 -) - -func feedUploadAndSyncCmd(ctx *cli.Context) error { - errc := make(chan error) - - go func() { - errc <- feedUploadAndSync(ctx) - }() - - select { - case err := <-errc: - if err != nil { - metrics.GetOrRegisterCounter(fmt.Sprintf("%s.fail", commandName), nil).Inc(1) - } - return err - case <-time.After(time.Duration(timeout) * time.Second): - metrics.GetOrRegisterCounter(fmt.Sprintf("%s.timeout", commandName), nil).Inc(1) - - return fmt.Errorf("timeout after %v sec", timeout) - } -} - -func feedUploadAndSync(c *cli.Context) error { - log.Info("generating and uploading feeds to " + httpEndpoint(hosts[0]) + " and syncing") - - // create a random private key to sign updates with and derive the address - pkFile, err := ioutil.TempFile("", "swarm-feed-smoke-test") - if err != nil { - return err - } - defer pkFile.Close() - defer os.Remove(pkFile.Name()) - - privkeyHex := "0000000000000000000000000000000000000000000000000000000000001976" - privKey, err := crypto.HexToECDSA(privkeyHex) - if err != nil { - return err - } - user := crypto.PubkeyToAddress(privKey.PublicKey) - userHex := hexutil.Encode(user.Bytes()) - - // save the private key to a file - _, err = io.WriteString(pkFile, privkeyHex) - if err != nil { - return err - } - - // keep hex strings for topic and subtopic - var topicHex string - var subTopicHex string - - // and create combination hex topics for bzz-feed retrieval - // xor'ed with topic (zero-value topic if no topic) - var subTopicOnlyHex string - var mergedSubTopicHex string - - // generate random topic and subtopic and put a hex on them - topicBytes, err := generateRandomData(feed.TopicLength) - topicHex = hexutil.Encode(topicBytes) - subTopicBytes, err := generateRandomData(8) - subTopicHex = hexutil.Encode(subTopicBytes) - if err != nil { - return err - } - mergedSubTopic, err := feed.NewTopic(subTopicHex, topicBytes) - if err != nil { - return err - } - mergedSubTopicHex = hexutil.Encode(mergedSubTopic[:]) - subTopicOnlyBytes, err := feed.NewTopic(subTopicHex, nil) - if err != nil { - return err - } - subTopicOnlyHex = hexutil.Encode(subTopicOnlyBytes[:]) - - // create feed manifest, topic only - var out bytes.Buffer - cmd := exec.Command("swarm", "--bzzapi", httpEndpoint(hosts[0]), "feed", "create", "--topic", topicHex, "--user", userHex) - cmd.Stdout = &out - log.Debug("create feed manifest topic cmd", "cmd", cmd) - err = cmd.Run() - if err != nil { - return err - } - manifestWithTopic := strings.TrimRight(out.String(), string([]byte{0x0a})) - if len(manifestWithTopic) != 64 { - return fmt.Errorf("unknown feed create manifest hash format (topic): (%d) %s", len(out.String()), manifestWithTopic) - } - log.Debug("create topic feed", "manifest", manifestWithTopic) - out.Reset() - - // create feed manifest, subtopic only - cmd = exec.Command("swarm", "--bzzapi", httpEndpoint(hosts[0]), "feed", "create", "--name", subTopicHex, "--user", userHex) - cmd.Stdout = &out - log.Debug("create feed manifest subtopic cmd", "cmd", cmd) - err = cmd.Run() - if err != nil { - return err - } - manifestWithSubTopic := strings.TrimRight(out.String(), string([]byte{0x0a})) - if len(manifestWithSubTopic) != 64 { - return fmt.Errorf("unknown feed create manifest hash format (subtopic): (%d) %s", len(out.String()), manifestWithSubTopic) - } - log.Debug("create subtopic feed", "manifest", manifestWithTopic) - out.Reset() - - // create feed manifest, merged topic - cmd = exec.Command("swarm", "--bzzapi", httpEndpoint(hosts[0]), "feed", "create", "--topic", topicHex, "--name", subTopicHex, "--user", userHex) - cmd.Stdout = &out - log.Debug("create feed manifest mergetopic cmd", "cmd", cmd) - err = cmd.Run() - if err != nil { - log.Error(err.Error()) - return err - } - manifestWithMergedTopic := strings.TrimRight(out.String(), string([]byte{0x0a})) - if len(manifestWithMergedTopic) != 64 { - return fmt.Errorf("unknown feed create manifest hash format (mergedtopic): (%d) %s", len(out.String()), manifestWithMergedTopic) - } - log.Debug("create mergedtopic feed", "manifest", manifestWithMergedTopic) - out.Reset() - - // create test data - data, err := generateRandomData(feedRandomDataLength) - if err != nil { - return err - } - h := md5.New() - h.Write(data) - dataHash := h.Sum(nil) - dataHex := hexutil.Encode(data) - - // update with topic - cmd = exec.Command("swarm", "--bzzaccount", pkFile.Name(), "--bzzapi", httpEndpoint(hosts[0]), "feed", "update", "--topic", topicHex, dataHex) - cmd.Stdout = &out - log.Debug("update feed manifest topic cmd", "cmd", cmd) - err = cmd.Run() - if err != nil { - return err - } - log.Debug("feed update topic", "out", out) - out.Reset() - - // update with subtopic - cmd = exec.Command("swarm", "--bzzaccount", pkFile.Name(), "--bzzapi", httpEndpoint(hosts[0]), "feed", "update", "--name", subTopicHex, dataHex) - cmd.Stdout = &out - log.Debug("update feed manifest subtopic cmd", "cmd", cmd) - err = cmd.Run() - if err != nil { - return err - } - log.Debug("feed update subtopic", "out", out) - out.Reset() - - // update with merged topic - cmd = exec.Command("swarm", "--bzzaccount", pkFile.Name(), "--bzzapi", httpEndpoint(hosts[0]), "feed", "update", "--topic", topicHex, "--name", subTopicHex, dataHex) - cmd.Stdout = &out - log.Debug("update feed manifest merged topic cmd", "cmd", cmd) - err = cmd.Run() - if err != nil { - return err - } - log.Debug("feed update mergedtopic", "out", out) - out.Reset() - - time.Sleep(3 * time.Second) - - // retrieve the data - wg := sync.WaitGroup{} - for _, host := range hosts { - // raw retrieve, topic only - for _, hex := range []string{topicHex, subTopicOnlyHex, mergedSubTopicHex} { - wg.Add(1) - ruid := uuid.New()[:8] - go func(hex string, endpoint string, ruid string) { - for { - err := fetchFeed(hex, userHex, httpEndpoint(host), dataHash, ruid) - if err != nil { - continue - } - - wg.Done() - return - } - }(hex, httpEndpoint(host), ruid) - } - } - wg.Wait() - log.Info("all endpoints synced random data successfully") - - // upload test file - log.Info("feed uploading to "+httpEndpoint(hosts[0])+" and syncing", "seed", seed) - - randomBytes := testutil.RandomBytes(seed, filesize*1000) - - hash, err := upload(randomBytes, httpEndpoint(hosts[0])) - if err != nil { - return err - } - hashBytes, err := hexutil.Decode("0x" + hash) - if err != nil { - return err - } - multihashHex := hexutil.Encode(hashBytes) - fileHash := h.Sum(nil) - - log.Info("uploaded successfully", "hash", hash, "digest", fmt.Sprintf("%x", fileHash)) - - // update file with topic - cmd = exec.Command("swarm", "--bzzaccount", pkFile.Name(), "--bzzapi", httpEndpoint(hosts[0]), "feed", "update", "--topic", topicHex, multihashHex) - cmd.Stdout = &out - err = cmd.Run() - if err != nil { - return err - } - log.Debug("feed update topic", "out", out) - out.Reset() - - // update file with subtopic - cmd = exec.Command("swarm", "--bzzaccount", pkFile.Name(), "--bzzapi", httpEndpoint(hosts[0]), "feed", "update", "--name", subTopicHex, multihashHex) - cmd.Stdout = &out - err = cmd.Run() - if err != nil { - return err - } - log.Debug("feed update subtopic", "out", out) - out.Reset() - - // update file with merged topic - cmd = exec.Command("swarm", "--bzzaccount", pkFile.Name(), "--bzzapi", httpEndpoint(hosts[0]), "feed", "update", "--topic", topicHex, "--name", subTopicHex, multihashHex) - cmd.Stdout = &out - err = cmd.Run() - if err != nil { - return err - } - log.Debug("feed update mergedtopic", "out", out) - out.Reset() - - time.Sleep(3 * time.Second) - - for _, host := range hosts { - - // manifest retrieve, topic only - for _, url := range []string{manifestWithTopic, manifestWithSubTopic, manifestWithMergedTopic} { - wg.Add(1) - ruid := uuid.New()[:8] - go func(url string, endpoint string, ruid string) { - for { - err := fetch(url, endpoint, fileHash, ruid) - if err != nil { - continue - } - - wg.Done() - return - } - }(url, httpEndpoint(host), ruid) - } - - } - wg.Wait() - log.Info("all endpoints synced random file successfully") - - return nil -} diff --git a/cmd/swarm/swarm-smoke/main.go b/cmd/swarm/swarm-smoke/main.go deleted file mode 100644 index 03e2cc2c4..000000000 --- a/cmd/swarm/swarm-smoke/main.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "fmt" - "os" - "sort" - - "github.com/ethereum/go-ethereum/cmd/utils" - gethmetrics "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/metrics/influxdb" - swarmmetrics "github.com/ethereum/go-ethereum/swarm/metrics" - "github.com/ethereum/go-ethereum/swarm/tracing" - - "github.com/ethereum/go-ethereum/log" - - cli "gopkg.in/urfave/cli.v1" -) - -var ( - gitCommit string // Git SHA1 commit hash of the release (set via linker flags) -) - -var ( - allhosts string - hosts []string - filesize int - syncDelay bool - inputSeed int - httpPort int - wsPort int - verbosity int - timeout int - single bool - onlyUpload bool -) - -func main() { - - app := cli.NewApp() - app.Name = "smoke-test" - app.Usage = "" - - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "hosts", - Value: "", - Usage: "comma-separated list of swarm hosts", - Destination: &allhosts, - }, - cli.IntFlag{ - Name: "http-port", - Value: 80, - Usage: "http port", - Destination: &httpPort, - }, - cli.IntFlag{ - Name: "ws-port", - Value: 8546, - Usage: "ws port", - Destination: &wsPort, - }, - cli.IntFlag{ - Name: "seed", - Value: 0, - Usage: "input seed in case we need deterministic upload", - Destination: &inputSeed, - }, - cli.IntFlag{ - Name: "filesize", - Value: 1024, - Usage: "file size for generated random file in KB", - Destination: &filesize, - }, - cli.BoolFlag{ - Name: "sync-delay", - Usage: "wait for content to be synced", - Destination: &syncDelay, - }, - cli.IntFlag{ - Name: "verbosity", - Value: 1, - Usage: "verbosity", - Destination: &verbosity, - }, - cli.IntFlag{ - Name: "timeout", - Value: 180, - Usage: "timeout in seconds after which kill the process", - Destination: &timeout, - }, - cli.BoolFlag{ - Name: "single", - Usage: "whether to fetch content from a single node or from all nodes", - Destination: &single, - }, - cli.BoolFlag{ - Name: "only-upload", - Usage: "whether to only upload content to a single node without fetching", - Destination: &onlyUpload, - }, - } - - app.Flags = append(app.Flags, []cli.Flag{ - utils.MetricsEnabledFlag, - swarmmetrics.MetricsInfluxDBEndpointFlag, - swarmmetrics.MetricsInfluxDBDatabaseFlag, - swarmmetrics.MetricsInfluxDBUsernameFlag, - swarmmetrics.MetricsInfluxDBPasswordFlag, - swarmmetrics.MetricsInfluxDBTagsFlag, - }...) - - app.Flags = append(app.Flags, tracing.Flags...) - - app.Commands = []cli.Command{ - { - Name: "upload_and_sync", - Aliases: []string{"c"}, - Usage: "upload and sync", - Action: wrapCliCommand("upload-and-sync", uploadAndSyncCmd), - }, - { - Name: "feed_sync", - Aliases: []string{"f"}, - Usage: "feed update generate, upload and sync", - Action: wrapCliCommand("feed-and-sync", feedUploadAndSyncCmd), - }, - { - Name: "upload_speed", - Aliases: []string{"u"}, - Usage: "measure upload speed", - Action: wrapCliCommand("upload-speed", uploadSpeedCmd), - }, - { - Name: "sliding_window", - Aliases: []string{"s"}, - Usage: "measure network aggregate capacity", - Action: wrapCliCommand("sliding-window", slidingWindowCmd), - }, - } - - sort.Sort(cli.FlagsByName(app.Flags)) - sort.Sort(cli.CommandsByName(app.Commands)) - - app.Before = func(ctx *cli.Context) error { - tracing.Setup(ctx) - return nil - } - - app.After = func(ctx *cli.Context) error { - return emitMetrics(ctx) - } - - err := app.Run(os.Args) - if err != nil { - log.Error(err.Error()) - - os.Exit(1) - } -} - -func emitMetrics(ctx *cli.Context) error { - if gethmetrics.Enabled { - var ( - endpoint = ctx.GlobalString(swarmmetrics.MetricsInfluxDBEndpointFlag.Name) - database = ctx.GlobalString(swarmmetrics.MetricsInfluxDBDatabaseFlag.Name) - username = ctx.GlobalString(swarmmetrics.MetricsInfluxDBUsernameFlag.Name) - password = ctx.GlobalString(swarmmetrics.MetricsInfluxDBPasswordFlag.Name) - tags = ctx.GlobalString(swarmmetrics.MetricsInfluxDBTagsFlag.Name) - ) - - tagsMap := utils.SplitTagsFlag(tags) - tagsMap["version"] = gitCommit - tagsMap["filesize"] = fmt.Sprintf("%v", filesize) - - return influxdb.InfluxDBWithTagsOnce(gethmetrics.DefaultRegistry, endpoint, database, username, password, "swarm-smoke.", tagsMap) - } - - return nil -} diff --git a/cmd/swarm/swarm-smoke/sliding_window.go b/cmd/swarm/swarm-smoke/sliding_window.go deleted file mode 100644 index 6ca3d3947..000000000 --- a/cmd/swarm/swarm-smoke/sliding_window.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "bytes" - "fmt" - "math/rand" - "time" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/swarm/testutil" - "github.com/pborman/uuid" - - cli "gopkg.in/urfave/cli.v1" -) - -type uploadResult struct { - hash string - digest []byte -} - -func slidingWindowCmd(ctx *cli.Context) error { - errc := make(chan error) - - go func() { - errc <- slidingWindow(ctx) - }() - - err := <-errc - if err != nil { - metrics.GetOrRegisterCounter(fmt.Sprintf("%s.fail", commandName), nil).Inc(1) - } - return err -} - -func slidingWindow(ctx *cli.Context) error { - var hashes []uploadResult //swarm hashes of the uploads - nodes := len(hosts) - log.Info("sliding window test started", "nodes", nodes, "filesize(kb)", filesize, "timeout", timeout) - uploadedBytes := 0 - networkDepth := 0 - errored := false - -outer: - for { - seed = int(time.Now().UTC().UnixNano()) - log.Info("uploading to "+httpEndpoint(hosts[0])+" and syncing", "seed", seed) - - t1 := time.Now() - - randomBytes := testutil.RandomBytes(seed, filesize*1000) - - hash, err := upload(randomBytes, httpEndpoint(hosts[0])) - if err != nil { - log.Error(err.Error()) - return err - } - - metrics.GetOrRegisterResettingTimer("sliding-window.upload-time", nil).UpdateSince(t1) - metrics.GetOrRegisterGauge("sliding-window.upload-depth", nil).Update(int64(len(hashes))) - - fhash, err := digest(bytes.NewReader(randomBytes)) - if err != nil { - log.Error(err.Error()) - return err - } - - log.Info("uploaded successfully", "hash", hash, "digest", fmt.Sprintf("%x", fhash), "wait for sync", syncDelay) - hashes = append(hashes, uploadResult{hash: hash, digest: fhash}) - - if syncDelay { - waitToSync() - } - - uploadedBytes += filesize * 1000 - q := make(chan struct{}, 1) - d := make(chan struct{}) - defer close(q) - defer close(d) - for i, v := range hashes { - timeoutC := time.After(time.Duration(timeout) * time.Second) - errored = false - - task: - for { - select { - case q <- struct{}{}: - go func() { - var start time.Time - done := false - for !done { - log.Info("trying to retrieve hash", "hash", v.hash) - idx := 1 + rand.Intn(len(hosts)-1) - ruid := uuid.New()[:8] - start = time.Now() - // fetch hangs when swarm dies out, so we have to jump through a bit more hoops to actually - // catch the timeout, but also allow this retry logic - err := fetch(v.hash, httpEndpoint(hosts[idx]), v.digest, ruid) - if err != nil { - log.Error("error fetching hash", "err", err) - continue - } - done = true - } - metrics.GetOrRegisterResettingTimer("sliding-window.single.fetch-time", nil).UpdateSince(start) - d <- struct{}{} - }() - case <-d: - <-q - break task - case <-timeoutC: - errored = true - log.Error("error retrieving hash. timeout", "hash idx", i) - metrics.GetOrRegisterCounter("sliding-window.single.error", nil).Inc(1) - break outer - default: - } - } - - networkDepth = i - metrics.GetOrRegisterGauge("sliding-window.network-depth", nil).Update(int64(networkDepth)) - log.Info("sliding window test successfully fetched file", "currentDepth", networkDepth) - // this test might take a long time to finish - but we'd like to see metrics while they accumulate and not just when - // the test finishes. therefore emit the metrics on each iteration - emitMetrics(ctx) - } - } - - log.Info("sliding window test finished", "errored?", errored, "networkDepth", networkDepth, "networkDepth(kb)", networkDepth*filesize) - log.Info("stats", "uploadedFiles", len(hashes), "uploadedKb", uploadedBytes/1000, "filesizeKb", filesize) - - return nil -} diff --git a/cmd/swarm/swarm-smoke/upload_and_sync.go b/cmd/swarm/swarm-smoke/upload_and_sync.go deleted file mode 100644 index 7338e3473..000000000 --- a/cmd/swarm/swarm-smoke/upload_and_sync.go +++ /dev/null @@ -1,376 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "bytes" - "context" - "encoding/hex" - "fmt" - "io/ioutil" - "math/rand" - "os" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/swarm/chunk" - "github.com/ethereum/go-ethereum/swarm/storage" - "github.com/ethereum/go-ethereum/swarm/testutil" - - cli "gopkg.in/urfave/cli.v1" -) - -func uploadAndSyncCmd(ctx *cli.Context) error { - // use input seed if it has been set - if inputSeed != 0 { - seed = inputSeed - } - - randomBytes := testutil.RandomBytes(seed, filesize*1000) - - errc := make(chan error) - - go func() { - errc <- uploadAndSync(ctx, randomBytes) - }() - - var err error - select { - case err = <-errc: - if err != nil { - metrics.GetOrRegisterCounter(fmt.Sprintf("%s.fail", commandName), nil).Inc(1) - } - case <-time.After(time.Duration(timeout) * time.Second): - metrics.GetOrRegisterCounter(fmt.Sprintf("%s.timeout", commandName), nil).Inc(1) - - err = fmt.Errorf("timeout after %v sec", timeout) - } - - // trigger debug functionality on randomBytes - e := trackChunks(randomBytes[:], true) - if e != nil { - log.Error(e.Error()) - } - - return err -} - -func trackChunks(testData []byte, submitMetrics bool) error { - addrs, err := getAllRefs(testData) - if err != nil { - return err - } - - for i, ref := range addrs { - log.Debug(fmt.Sprintf("ref %d", i), "ref", ref) - } - - var globalYes, globalNo int - var globalMu sync.Mutex - var hasErr bool - - var wg sync.WaitGroup - wg.Add(len(hosts)) - - var mu sync.Mutex // mutex protecting the allHostsChunks and bzzAddrs maps - allHostChunks := map[string]string{} // host->bitvector of presence for chunks - bzzAddrs := map[string]string{} // host->bzzAddr - - for _, host := range hosts { - host := host - go func() { - defer wg.Done() - httpHost := fmt.Sprintf("ws://%s:%d", host, 8546) - - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - rpcClient, err := rpc.DialContext(ctx, httpHost) - if rpcClient != nil { - defer rpcClient.Close() - } - if err != nil { - log.Error("error dialing host", "err", err, "host", httpHost) - hasErr = true - return - } - - hostChunks, err := getChunksBitVectorFromHost(rpcClient, addrs) - if err != nil { - log.Error("error getting chunks bit vector from host", "err", err, "host", httpHost) - hasErr = true - return - } - - bzzAddr, err := getBzzAddrFromHost(rpcClient) - if err != nil { - log.Error("error getting bzz addrs from host", "err", err, "host", httpHost) - hasErr = true - return - } - - mu.Lock() - allHostChunks[host] = hostChunks - bzzAddrs[host] = bzzAddr - mu.Unlock() - - yes, no := 0, 0 - for _, val := range hostChunks { - if val == '1' { - yes++ - } else { - no++ - } - } - - if no == 0 { - log.Info("host reported to have all chunks", "host", host) - } - - log.Debug("chunks", "chunks", hostChunks, "yes", yes, "no", no, "host", host) - - if submitMetrics { - globalMu.Lock() - globalYes += yes - globalNo += no - globalMu.Unlock() - } - }() - } - - wg.Wait() - - checkChunksVsMostProxHosts(addrs, allHostChunks, bzzAddrs) - - if !hasErr && submitMetrics { - // remove the chunks stored on the uploader node - globalYes -= len(addrs) - - metrics.GetOrRegisterCounter("deployment.chunks.yes", nil).Inc(int64(globalYes)) - metrics.GetOrRegisterCounter("deployment.chunks.no", nil).Inc(int64(globalNo)) - metrics.GetOrRegisterCounter("deployment.chunks.refs", nil).Inc(int64(len(addrs))) - } - - return nil -} - -// getChunksBitVectorFromHost returns a bit vector of presence for a given slice of chunks from a given host -func getChunksBitVectorFromHost(client *rpc.Client, addrs []storage.Address) (string, error) { - var hostChunks string - - err := client.Call(&hostChunks, "bzz_has", addrs) - if err != nil { - return "", err - } - - return hostChunks, nil -} - -// getBzzAddrFromHost returns the bzzAddr for a given host -func getBzzAddrFromHost(client *rpc.Client) (string, error) { - var hive string - - err := client.Call(&hive, "bzz_hive") - if err != nil { - return "", err - } - - // we make an ugly assumption about the output format of the hive.String() method - // ideally we should replace this with an API call that returns the bzz addr for a given host, - // but this also works for now (provided we don't change the hive.String() method, which we haven't in some time - ss := strings.Split(strings.Split(hive, "\n")[3], " ") - return ss[len(ss)-1], nil -} - -// checkChunksVsMostProxHosts is checking: -// 1. whether a chunk has been found at less than 2 hosts. Considering our NN size, this should not happen. -// 2. if a chunk is not found at its closest node. This should also not happen. -// Together with the --only-upload flag, we could run this smoke test and make sure that our syncing -// functionality is correct (without even trying to retrieve the content). -// -// addrs - a slice with all uploaded chunk refs -// allHostChunks - host->bit vector, showing what chunks are present on what hosts -// bzzAddrs - host->bzz address, used when determining the most proximate host for a given chunk -func checkChunksVsMostProxHosts(addrs []storage.Address, allHostChunks map[string]string, bzzAddrs map[string]string) { - for k, v := range bzzAddrs { - log.Trace("bzzAddr", "bzz", v, "host", k) - } - - for i := range addrs { - var foundAt int - maxProx := -1 - var maxProxHost string - for host := range allHostChunks { - if allHostChunks[host][i] == '1' { - foundAt++ - } - - ba, err := hex.DecodeString(bzzAddrs[host]) - if err != nil { - panic(err) - } - - // calculate the host closest to any chunk - prox := chunk.Proximity(addrs[i], ba) - if prox > maxProx { - maxProx = prox - maxProxHost = host - } - } - - if allHostChunks[maxProxHost][i] == '0' { - log.Error("chunk not found at max prox host", "ref", addrs[i], "host", maxProxHost, "bzzAddr", bzzAddrs[maxProxHost]) - } else { - log.Trace("chunk present at max prox host", "ref", addrs[i], "host", maxProxHost, "bzzAddr", bzzAddrs[maxProxHost]) - } - - // if chunk found at less than 2 hosts - if foundAt < 2 { - log.Error("chunk found at less than two hosts", "foundAt", foundAt, "ref", addrs[i]) - } - } -} - -func getAllRefs(testData []byte) (storage.AddressCollection, error) { - datadir, err := ioutil.TempDir("", "chunk-debug") - if err != nil { - return nil, fmt.Errorf("unable to create temp dir: %v", err) - } - defer os.RemoveAll(datadir) - fileStore, err := storage.NewLocalFileStore(datadir, make([]byte, 32), chunk.NewTags()) - if err != nil { - return nil, err - } - - reader := bytes.NewReader(testData) - return fileStore.GetAllReferences(context.Background(), reader, false) -} - -func uploadAndSync(c *cli.Context, randomBytes []byte) error { - log.Info("uploading to "+httpEndpoint(hosts[0])+" and syncing", "seed", seed) - - t1 := time.Now() - hash, err := upload(randomBytes, httpEndpoint(hosts[0])) - if err != nil { - log.Error(err.Error()) - return err - } - t2 := time.Since(t1) - metrics.GetOrRegisterResettingTimer("upload-and-sync.upload-time", nil).Update(t2) - - fhash, err := digest(bytes.NewReader(randomBytes)) - if err != nil { - log.Error(err.Error()) - return err - } - - log.Info("uploaded successfully", "hash", hash, "took", t2, "digest", fmt.Sprintf("%x", fhash)) - - // wait to sync and log chunks before fetch attempt, only if syncDelay is set to true - if syncDelay { - waitToSync() - - log.Debug("chunks before fetch attempt", "hash", hash) - - err = trackChunks(randomBytes, false) - if err != nil { - log.Error(err.Error()) - } - } - - if onlyUpload { - log.Debug("only-upload is true, stoppping test", "hash", hash) - return nil - } - - randIndex := 1 + rand.Intn(len(hosts)-1) - - for { - start := time.Now() - err := fetch(hash, httpEndpoint(hosts[randIndex]), fhash, "") - if err != nil { - time.Sleep(2 * time.Second) - continue - } - ended := time.Since(start) - - metrics.GetOrRegisterResettingTimer("upload-and-sync.single.fetch-time", nil).Update(ended) - log.Info("fetch successful", "took", ended, "endpoint", httpEndpoint(hosts[randIndex])) - break - } - - return nil -} - -func isSyncing(wsHost string) (bool, error) { - rpcClient, err := rpc.Dial(wsHost) - if rpcClient != nil { - defer rpcClient.Close() - } - - if err != nil { - log.Error("error dialing host", "err", err) - return false, err - } - - var isSyncing bool - err = rpcClient.Call(&isSyncing, "bzz_isSyncing") - if err != nil { - log.Error("error calling host for isSyncing", "err", err) - return false, err - } - - log.Debug("isSyncing result", "host", wsHost, "isSyncing", isSyncing) - - return isSyncing, nil -} - -func waitToSync() { - t1 := time.Now() - - ns := uint64(1) - - for ns > 0 { - time.Sleep(3 * time.Second) - - notSynced := uint64(0) - var wg sync.WaitGroup - wg.Add(len(hosts)) - for i := 0; i < len(hosts); i++ { - i := i - go func(idx int) { - stillSyncing, err := isSyncing(wsEndpoint(hosts[idx])) - - if stillSyncing || err != nil { - atomic.AddUint64(¬Synced, 1) - } - wg.Done() - }(i) - } - wg.Wait() - - ns = atomic.LoadUint64(¬Synced) - } - - t2 := time.Since(t1) - metrics.GetOrRegisterResettingTimer("upload-and-sync.single.wait-for-sync.deployment", nil).Update(t2) -} diff --git a/cmd/swarm/swarm-smoke/upload_speed.go b/cmd/swarm/swarm-smoke/upload_speed.go deleted file mode 100644 index 047ea0092..000000000 --- a/cmd/swarm/swarm-smoke/upload_speed.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "bytes" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/swarm/testutil" - - cli "gopkg.in/urfave/cli.v1" -) - -func uploadSpeedCmd(ctx *cli.Context) error { - log.Info("uploading to "+hosts[0], "seed", seed) - randomBytes := testutil.RandomBytes(seed, filesize*1000) - - errc := make(chan error) - - go func() { - errc <- uploadSpeed(ctx, randomBytes) - }() - - select { - case err := <-errc: - if err != nil { - metrics.GetOrRegisterCounter(fmt.Sprintf("%s.fail", commandName), nil).Inc(1) - } - return err - case <-time.After(time.Duration(timeout) * time.Second): - metrics.GetOrRegisterCounter(fmt.Sprintf("%s.timeout", commandName), nil).Inc(1) - - // trigger debug functionality on randomBytes - - return fmt.Errorf("timeout after %v sec", timeout) - } -} - -func uploadSpeed(c *cli.Context, data []byte) error { - t1 := time.Now() - hash, err := upload(data, hosts[0]) - if err != nil { - log.Error(err.Error()) - return err - } - metrics.GetOrRegisterCounter("upload-speed.upload-time", nil).Inc(int64(time.Since(t1))) - - fhash, err := digest(bytes.NewReader(data)) - if err != nil { - log.Error(err.Error()) - return err - } - - log.Info("uploaded successfully", "hash", hash, "digest", fmt.Sprintf("%x", fhash)) - return nil -} diff --git a/cmd/swarm/swarm-smoke/util.go b/cmd/swarm/swarm-smoke/util.go deleted file mode 100644 index b95f993e8..000000000 --- a/cmd/swarm/swarm-smoke/util.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "bytes" - "context" - "crypto/md5" - crand "crypto/rand" - "errors" - "fmt" - "io" - "io/ioutil" - "math/rand" - "net/http" - "net/http/httptrace" - "os" - "strings" - "time" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/swarm/api" - "github.com/ethereum/go-ethereum/swarm/api/client" - "github.com/ethereum/go-ethereum/swarm/spancontext" - opentracing "github.com/opentracing/opentracing-go" - cli "gopkg.in/urfave/cli.v1" -) - -var ( - commandName = "" - seed = int(time.Now().UTC().UnixNano()) -) - -func init() { - rand.Seed(int64(seed)) -} - -func httpEndpoint(host string) string { - return fmt.Sprintf("http://%s:%d", host, httpPort) -} - -func wsEndpoint(host string) string { - return fmt.Sprintf("ws://%s:%d", host, wsPort) -} - -func wrapCliCommand(name string, command func(*cli.Context) error) func(*cli.Context) error { - return func(ctx *cli.Context) error { - log.PrintOrigins(true) - log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(verbosity), log.StreamHandler(os.Stdout, log.TerminalFormat(false)))) - - commandName = name - - hosts = strings.Split(allhosts, ",") - - defer func(now time.Time) { - totalTime := time.Since(now) - log.Info("total time", "time", totalTime, "kb", filesize) - metrics.GetOrRegisterResettingTimer(name+".total-time", nil).Update(totalTime) - }(time.Now()) - - log.Info("smoke test starting", "task", name, "timeout", timeout) - metrics.GetOrRegisterCounter(name, nil).Inc(1) - - return command(ctx) - } -} - -func fetchFeed(topic string, user string, endpoint string, original []byte, ruid string) error { - ctx, sp := spancontext.StartSpan(context.Background(), "feed-and-sync.fetch") - defer sp.Finish() - - log.Trace("sleeping", "ruid", ruid) - time.Sleep(3 * time.Second) - - log.Trace("http get request (feed)", "ruid", ruid, "api", endpoint, "topic", topic, "user", user) - - var tn time.Time - reqUri := endpoint + "/bzz-feed:/?topic=" + topic + "&user=" + user - req, _ := http.NewRequest("GET", reqUri, nil) - - opentracing.GlobalTracer().Inject( - sp.Context(), - opentracing.HTTPHeaders, - opentracing.HTTPHeadersCarrier(req.Header)) - - trace := client.GetClientTrace("feed-and-sync - http get", "feed-and-sync", ruid, &tn) - - req = req.WithContext(httptrace.WithClientTrace(ctx, trace)) - transport := http.DefaultTransport - - //transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - tn = time.Now() - res, err := transport.RoundTrip(req) - if err != nil { - log.Error(err.Error(), "ruid", ruid) - return err - } - - log.Trace("http get response (feed)", "ruid", ruid, "api", endpoint, "topic", topic, "user", user, "code", res.StatusCode, "len", res.ContentLength) - - if res.StatusCode != 200 { - return fmt.Errorf("expected status code %d, got %v (ruid %v)", 200, res.StatusCode, ruid) - } - - defer res.Body.Close() - - rdigest, err := digest(res.Body) - if err != nil { - log.Warn(err.Error(), "ruid", ruid) - return err - } - - if !bytes.Equal(rdigest, original) { - err := fmt.Errorf("downloaded imported file md5=%x is not the same as the generated one=%x", rdigest, original) - log.Warn(err.Error(), "ruid", ruid) - return err - } - - log.Trace("downloaded file matches random file", "ruid", ruid, "len", res.ContentLength) - - return nil -} - -// fetch is getting the requested `hash` from the `endpoint` and compares it with the `original` file -func fetch(hash string, endpoint string, original []byte, ruid string) error { - ctx, sp := spancontext.StartSpan(context.Background(), "upload-and-sync.fetch") - defer sp.Finish() - - log.Info("http get request", "ruid", ruid, "endpoint", endpoint, "hash", hash) - - var tn time.Time - reqUri := endpoint + "/bzz:/" + hash + "/" - req, _ := http.NewRequest("GET", reqUri, nil) - - opentracing.GlobalTracer().Inject( - sp.Context(), - opentracing.HTTPHeaders, - opentracing.HTTPHeadersCarrier(req.Header)) - - trace := client.GetClientTrace(commandName+" - http get", commandName, ruid, &tn) - - req = req.WithContext(httptrace.WithClientTrace(ctx, trace)) - transport := http.DefaultTransport - - //transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - tn = time.Now() - res, err := transport.RoundTrip(req) - if err != nil { - log.Error(err.Error(), "ruid", ruid) - return err - } - log.Info("http get response", "ruid", ruid, "endpoint", endpoint, "hash", hash, "code", res.StatusCode, "len", res.ContentLength) - - if res.StatusCode != 200 { - err := fmt.Errorf("expected status code %d, got %v", 200, res.StatusCode) - log.Warn(err.Error(), "ruid", ruid) - return err - } - - defer res.Body.Close() - - rdigest, err := digest(res.Body) - if err != nil { - log.Warn(err.Error(), "ruid", ruid) - return err - } - - if !bytes.Equal(rdigest, original) { - err := fmt.Errorf("downloaded imported file md5=%x is not the same as the generated one=%x", rdigest, original) - log.Warn(err.Error(), "ruid", ruid) - return err - } - - log.Trace("downloaded file matches random file", "ruid", ruid, "len", res.ContentLength) - - return nil -} - -// upload an arbitrary byte as a plaintext file to `endpoint` using the api client -func upload(data []byte, endpoint string) (string, error) { - swarm := client.NewClient(endpoint) - f := &client.File{ - ReadCloser: ioutil.NopCloser(bytes.NewReader(data)), - ManifestEntry: api.ManifestEntry{ - ContentType: "text/plain", - Mode: 0660, - Size: int64(len(data)), - }, - } - - // upload data to bzz:// and retrieve the content-addressed manifest hash, hex-encoded. - return swarm.Upload(f, "", false) -} - -func digest(r io.Reader) ([]byte, error) { - h := md5.New() - _, err := io.Copy(h, r) - if err != nil { - return nil, err - } - return h.Sum(nil), nil -} - -// generates random data in heap buffer -func generateRandomData(datasize int) ([]byte, error) { - b := make([]byte, datasize) - c, err := crand.Read(b) - if err != nil { - return nil, err - } else if c != datasize { - return nil, errors.New("short read") - } - return b, nil -} diff --git a/cmd/swarm/swarm-snapshot/create.go b/cmd/swarm/swarm-snapshot/create.go deleted file mode 100644 index 434561a49..000000000 --- a/cmd/swarm/swarm-snapshot/create.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "os" - "path" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p/simulations" - "github.com/ethereum/go-ethereum/p2p/simulations/adapters" - "github.com/ethereum/go-ethereum/swarm/network" - "github.com/ethereum/go-ethereum/swarm/network/simulation" - cli "gopkg.in/urfave/cli.v1" -) - -// create is used as the entry function for "create" app command. -func create(ctx *cli.Context) error { - log.PrintOrigins(true) - log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(ctx.Int("verbosity")), log.StreamHandler(os.Stdout, log.TerminalFormat(true)))) - - if len(ctx.Args()) < 1 { - return errors.New("argument should be the filename to verify or write-to") - } - filename, err := touchPath(ctx.Args()[0]) - if err != nil { - return err - } - return createSnapshot(filename, ctx.Int("nodes"), strings.Split(ctx.String("services"), ",")) -} - -// createSnapshot creates a new snapshot on filesystem with provided filename, -// number of nodes and service names. -func createSnapshot(filename string, nodes int, services []string) (err error) { - log.Debug("create snapshot", "filename", filename, "nodes", nodes, "services", services) - - sim := simulation.New(map[string]simulation.ServiceFunc{ - "bzz": func(ctx *adapters.ServiceContext, bucket *sync.Map) (node.Service, func(), error) { - addr := network.NewAddr(ctx.Config.Node()) - kad := network.NewKademlia(addr.Over(), network.NewKadParams()) - hp := network.NewHiveParams() - hp.KeepAliveInterval = time.Duration(200) * time.Millisecond - hp.Discovery = true // discovery must be enabled when creating a snapshot - - // store the kademlia in the bucket, needed later in the WaitTillHealthy function - bucket.Store(simulation.BucketKeyKademlia, kad) - - config := &network.BzzConfig{ - OverlayAddr: addr.Over(), - UnderlayAddr: addr.Under(), - HiveParams: hp, - } - return network.NewBzz(config, kad, nil, nil, nil), nil, nil - }, - }) - defer sim.Close() - - ids, err := sim.AddNodes(nodes) - if err != nil { - return fmt.Errorf("add nodes: %v", err) - } - - err = sim.Net.ConnectNodesRing(ids) - if err != nil { - return fmt.Errorf("connect nodes: %v", err) - } - - ctx, cancelSimRun := context.WithTimeout(context.Background(), 3*time.Minute) - defer cancelSimRun() - if _, err := sim.WaitTillHealthy(ctx); err != nil { - return fmt.Errorf("wait for healthy kademlia: %v", err) - } - - var snap *simulations.Snapshot - if len(services) > 0 { - // If service names are provided, include them in the snapshot. - // But, check if "bzz" service is not among them to remove it - // form the snapshot as it exists on snapshot creation. - var removeServices []string - var wantBzz bool - for _, s := range services { - if s == "bzz" { - wantBzz = true - break - } - } - if !wantBzz { - removeServices = []string{"bzz"} - } - snap, err = sim.Net.SnapshotWithServices(services, removeServices) - } else { - snap, err = sim.Net.Snapshot() - } - if err != nil { - return fmt.Errorf("create snapshot: %v", err) - } - jsonsnapshot, err := json.Marshal(snap) - if err != nil { - return fmt.Errorf("json encode snapshot: %v", err) - } - return ioutil.WriteFile(filename, jsonsnapshot, 0666) -} - -// touchPath creates an empty file and all subdirectories -// that are missing. -func touchPath(filename string) (string, error) { - if path.IsAbs(filename) { - if _, err := os.Stat(filename); err == nil { - // path exists, overwrite - return filename, nil - } - } - - d, f := path.Split(filename) - dir, err := filepath.Abs(filepath.Dir(os.Args[0])) - if err != nil { - return "", err - } - - _, err = os.Stat(path.Join(dir, filename)) - if err == nil { - // path exists, overwrite - return filename, nil - } - - dirPath := path.Join(dir, d) - filePath := path.Join(dirPath, f) - if d != "" { - err = os.MkdirAll(dirPath, os.ModeDir) - if err != nil { - return "", err - } - } - - return filePath, nil -} diff --git a/cmd/swarm/swarm-snapshot/create_test.go b/cmd/swarm/swarm-snapshot/create_test.go deleted file mode 100644 index 4cd78f35a..000000000 --- a/cmd/swarm/swarm-snapshot/create_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "sort" - "strconv" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/p2p/simulations" -) - -// TestSnapshotCreate is a high level e2e test that tests for snapshot generation. -// It runs a few "create" commands with different flag values and loads generated -// snapshot files to validate their content. -func TestSnapshotCreate(t *testing.T) { - t.Skip("test is flaky. disabling until underlying problem is addressed") - - for _, v := range []struct { - name string - nodes int - services string - }{ - { - name: "defaults", - }, - { - name: "more nodes", - nodes: defaultNodes + 4, - }, - { - name: "services", - services: "stream,pss,zorglub", - }, - { - name: "services with bzz", - services: "bzz,pss", - }, - } { - t.Run(v.name, func(t *testing.T) { - t.Parallel() - - file, err := ioutil.TempFile("", "swarm-snapshot") - if err != nil { - t.Fatal(err) - } - defer os.Remove(file.Name()) - - if err = file.Close(); err != nil { - t.Error(err) - } - - args := []string{"create"} - if v.nodes > 0 { - args = append(args, "--nodes", strconv.Itoa(v.nodes)) - } - if v.services != "" { - args = append(args, "--services", v.services) - } - testCmd := runSnapshot(t, append(args, file.Name())...) - - testCmd.WaitExit() - if code := testCmd.ExitStatus(); code != 0 { - t.Fatalf("command exit code %v, expected 0", code) - } - - f, err := os.Open(file.Name()) - if err != nil { - t.Fatal(err) - } - defer func() { - err := f.Close() - if err != nil { - t.Error("closing snapshot file", "err", err) - } - }() - - b, err := ioutil.ReadAll(f) - if err != nil { - t.Fatal(err) - } - var snap simulations.Snapshot - err = json.Unmarshal(b, &snap) - if err != nil { - t.Fatal(err) - } - - wantNodes := v.nodes - if wantNodes == 0 { - wantNodes = defaultNodes - } - gotNodes := len(snap.Nodes) - if gotNodes != wantNodes { - t.Errorf("got %v nodes, want %v", gotNodes, wantNodes) - } - - if len(snap.Conns) == 0 { - t.Error("no connections in a snapshot") - } - - var wantServices []string - if v.services != "" { - wantServices = strings.Split(v.services, ",") - } else { - wantServices = []string{"bzz"} - } - // sort service names so they can be comparable - // as strings to every node sorted services - sort.Strings(wantServices) - - for i, n := range snap.Nodes { - gotServices := n.Node.Config.Services - sort.Strings(gotServices) - if fmt.Sprint(gotServices) != fmt.Sprint(wantServices) { - t.Errorf("got services %v for node %v, want %v", gotServices, i, wantServices) - } - } - - }) - } -} diff --git a/cmd/swarm/swarm-snapshot/main.go b/cmd/swarm/swarm-snapshot/main.go deleted file mode 100644 index 4a1e8b4f5..000000000 --- a/cmd/swarm/swarm-snapshot/main.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "os" - - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/log" - cli "gopkg.in/urfave/cli.v1" -) - -var gitCommit string // Git SHA1 commit hash of the release (set via linker flags) -var gitDate string - -// default value for "create" command --nodes flag -const defaultNodes = 8 - -func main() { - err := newApp().Run(os.Args) - if err != nil { - log.Error(err.Error()) - os.Exit(1) - } -} - -// newApp construct a new instance of Swarm Snapshot Utility. -// Method Run is called on it in the main function and in tests. -func newApp() (app *cli.App) { - app = utils.NewApp(gitCommit, gitDate, "Swarm Snapshot Utility") - - app.Name = "swarm-snapshot" - app.Usage = "" - - // app flags (for all commands) - app.Flags = []cli.Flag{ - cli.IntFlag{ - Name: "verbosity", - Value: 1, - Usage: "verbosity level", - }, - } - - app.Commands = []cli.Command{ - { - Name: "create", - Aliases: []string{"c"}, - Usage: "create a swarm snapshot", - Action: create, - // Flags only for "create" command. - // Allow app flags to be specified after the - // command argument. - Flags: append(app.Flags, - cli.IntFlag{ - Name: "nodes", - Value: defaultNodes, - Usage: "number of nodes", - }, - cli.StringFlag{ - Name: "services", - Value: "bzz", - Usage: "comma separated list of services to boot the nodes with", - }, - ), - }, - } - - return app -} diff --git a/cmd/swarm/swarm-snapshot/run_test.go b/cmd/swarm/swarm-snapshot/run_test.go deleted file mode 100644 index d9a041597..000000000 --- a/cmd/swarm/swarm-snapshot/run_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "fmt" - "os" - "testing" - - "github.com/docker/docker/pkg/reexec" - "github.com/ethereum/go-ethereum/internal/cmdtest" -) - -func init() { - reexec.Register("swarm-snapshot", func() { - if err := newApp().Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - os.Exit(0) - }) -} - -func runSnapshot(t *testing.T, args ...string) *cmdtest.TestCmd { - tt := cmdtest.NewTestCmd(t, nil) - tt.Run("swarm-snapshot", args...) - return tt -} - -func TestMain(m *testing.M) { - if reexec.Init() { - return - } - os.Exit(m.Run()) -} diff --git a/cmd/swarm/testdata/datastore_fixture.go b/cmd/swarm/testdata/datastore_fixture.go deleted file mode 100644 index 6a147a6a4..000000000 --- a/cmd/swarm/testdata/datastore_fixture.go +++ /dev/null @@ -1,1390 +0,0 @@ -package testdata - -const DATADIR_MIGRATION_FIXTURE = `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` diff --git a/cmd/swarm/upload.go b/cmd/swarm/upload.go deleted file mode 100644 index ab0790835..000000000 --- a/cmd/swarm/upload.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -// Command bzzup uploads files to the swarm HTTP API. -package main - -import ( - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "os/user" - "path" - "path/filepath" - "strconv" - "strings" - - "github.com/ethereum/go-ethereum/log" - swarm "github.com/ethereum/go-ethereum/swarm/api/client" - - "github.com/ethereum/go-ethereum/cmd/utils" - "gopkg.in/urfave/cli.v1" -) - -var upCommand = cli.Command{ - Action: upload, - CustomHelpTemplate: helpTemplate, - Name: "up", - Usage: "uploads a file or directory to swarm using the HTTP API", - ArgsUsage: "<file>", - Flags: []cli.Flag{SwarmEncryptedFlag}, - Description: "uploads a file or directory to swarm using the HTTP API and prints the root hash", -} - -func upload(ctx *cli.Context) { - args := ctx.Args() - var ( - bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") - recursive = ctx.GlobalBool(SwarmRecursiveFlag.Name) - wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name) - defaultPath = ctx.GlobalString(SwarmUploadDefaultPath.Name) - fromStdin = ctx.GlobalBool(SwarmUpFromStdinFlag.Name) - mimeType = ctx.GlobalString(SwarmUploadMimeType.Name) - client = swarm.NewClient(bzzapi) - toEncrypt = ctx.Bool(SwarmEncryptedFlag.Name) - autoDefaultPath = false - file string - ) - if autoDefaultPathString := os.Getenv(SwarmAutoDefaultPath); autoDefaultPathString != "" { - b, err := strconv.ParseBool(autoDefaultPathString) - if err != nil { - utils.Fatalf("invalid environment variable %s: %v", SwarmAutoDefaultPath, err) - } - autoDefaultPath = b - } - if len(args) != 1 { - if fromStdin { - tmp, err := ioutil.TempFile("", "swarm-stdin") - if err != nil { - utils.Fatalf("error create tempfile: %s", err) - } - defer os.Remove(tmp.Name()) - n, err := io.Copy(tmp, os.Stdin) - if err != nil { - utils.Fatalf("error copying stdin to tempfile: %s", err) - } else if n == 0 { - utils.Fatalf("error reading from stdin: zero length") - } - file = tmp.Name() - } else { - utils.Fatalf("Need filename as the first and only argument") - } - } else { - file = expandPath(args[0]) - } - - if !wantManifest { - f, err := swarm.Open(file) - if err != nil { - utils.Fatalf("Error opening file: %s", err) - } - defer f.Close() - hash, err := client.UploadRaw(f, f.Size, toEncrypt) - if err != nil { - utils.Fatalf("Upload failed: %s", err) - } - fmt.Println(hash) - return - } - - stat, err := os.Stat(file) - if err != nil { - utils.Fatalf("Error opening file: %s", err) - } - - // define a function which either uploads a directory or single file - // based on the type of the file being uploaded - var doUpload func() (hash string, err error) - if stat.IsDir() { - doUpload = func() (string, error) { - if !recursive { - return "", errors.New("Argument is a directory and recursive upload is disabled") - } - if autoDefaultPath && defaultPath == "" { - defaultEntryCandidate := path.Join(file, "index.html") - log.Debug("trying to find default path", "path", defaultEntryCandidate) - defaultEntryStat, err := os.Stat(defaultEntryCandidate) - if err == nil && !defaultEntryStat.IsDir() { - log.Debug("setting auto detected default path", "path", defaultEntryCandidate) - defaultPath = defaultEntryCandidate - } - } - if defaultPath != "" { - // construct absolute default path - absDefaultPath, _ := filepath.Abs(defaultPath) - absFile, _ := filepath.Abs(file) - // make sure absolute directory ends with only one "/" - // to trim it from absolute default path and get relative default path - absFile = strings.TrimRight(absFile, "/") + "/" - if absDefaultPath != "" && absFile != "" && strings.HasPrefix(absDefaultPath, absFile) { - defaultPath = strings.TrimPrefix(absDefaultPath, absFile) - } - } - return client.UploadDirectory(file, defaultPath, "", toEncrypt) - } - } else { - doUpload = func() (string, error) { - f, err := swarm.Open(file) - if err != nil { - return "", fmt.Errorf("error opening file: %s", err) - } - defer f.Close() - if mimeType != "" { - f.ContentType = mimeType - } - return client.Upload(f, "", toEncrypt) - } - } - hash, err := doUpload() - if err != nil { - utils.Fatalf("Upload failed: %s", err) - } - fmt.Println(hash) -} - -// Expands a file path -// 1. replace tilde with users home dir -// 2. expands embedded environment variables -// 3. cleans the path, e.g. /a/b/../c -> /a/c -// Note, it has limitations, e.g. ~someuser/tmp will not be expanded -func expandPath(p string) string { - if i := strings.Index(p, ":"); i > 0 { - return p - } - if i := strings.Index(p, "@"); i > 0 { - return p - } - if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") { - if home := homeDir(); home != "" { - p = home + p[1:] - } - } - return path.Clean(os.ExpandEnv(p)) -} - -func homeDir() string { - if home := os.Getenv("HOME"); home != "" { - return home - } - if usr, err := user.Current(); err == nil { - return usr.HomeDir - } - return "" -} diff --git a/cmd/swarm/upload_test.go b/cmd/swarm/upload_test.go deleted file mode 100644 index 356424c66..000000000 --- a/cmd/swarm/upload_test.go +++ /dev/null @@ -1,359 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "path" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - "github.com/ethereum/go-ethereum/log" - swarmapi "github.com/ethereum/go-ethereum/swarm/api/client" - "github.com/ethereum/go-ethereum/swarm/testutil" - "github.com/mattn/go-colorable" -) - -func init() { - log.PrintOrigins(true) - log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true)))) -} - -func TestSwarmUp(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip() - } - - cluster := newTestCluster(t, clusterSize) - defer cluster.Shutdown() - - cases := []struct { - name string - f func(t *testing.T, cluster *testCluster) - }{ - {"NoEncryption", testNoEncryption}, - {"Encrypted", testEncrypted}, - {"RecursiveNoEncryption", testRecursiveNoEncryption}, - {"RecursiveEncrypted", testRecursiveEncrypted}, - {"DefaultPathAll", testDefaultPathAll}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - tc.f(t, cluster) - }) - } -} - -// testNoEncryption tests that running 'swarm up' makes the resulting file -// available from all nodes via the HTTP API -func testNoEncryption(t *testing.T, cluster *testCluster) { - testDefault(t, cluster, false) -} - -// testEncrypted tests that running 'swarm up --encrypted' makes the resulting file -// available from all nodes via the HTTP API -func testEncrypted(t *testing.T, cluster *testCluster) { - testDefault(t, cluster, true) -} - -func testRecursiveNoEncryption(t *testing.T, cluster *testCluster) { - testRecursive(t, cluster, false) -} - -func testRecursiveEncrypted(t *testing.T, cluster *testCluster) { - testRecursive(t, cluster, true) -} - -func testDefault(t *testing.T, cluster *testCluster, toEncrypt bool) { - tmpFileName := testutil.TempFileWithContent(t, data) - defer os.Remove(tmpFileName) - - // write data to file - hashRegexp := `[a-f\d]{64}` - flags := []string{ - "--bzzapi", cluster.Nodes[0].URL, - "up", - tmpFileName} - if toEncrypt { - hashRegexp = `[a-f\d]{128}` - flags = []string{ - "--bzzapi", cluster.Nodes[0].URL, - "up", - "--encrypt", - tmpFileName} - } - // upload the file with 'swarm up' and expect a hash - log.Info(fmt.Sprintf("uploading file with 'swarm up'")) - up := runSwarm(t, flags...) - _, matches := up.ExpectRegexp(hashRegexp) - up.ExpectExit() - hash := matches[0] - log.Info("file uploaded", "hash", hash) - - // get the file from the HTTP API of each node - for _, node := range cluster.Nodes { - log.Info("getting file from node", "node", node.Name) - - res, err := http.Get(node.URL + "/bzz:/" + hash) - if err != nil { - t.Fatal(err) - } - defer res.Body.Close() - - reply, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 200 { - t.Fatalf("expected HTTP status 200, got %s", res.Status) - } - if string(reply) != data { - t.Fatalf("expected HTTP body %q, got %q", data, reply) - } - log.Debug("verifying uploaded file using `swarm down`") - //try to get the content with `swarm down` - tmpDownload, err := ioutil.TempDir("", "swarm-test") - tmpDownload = path.Join(tmpDownload, "tmpfile.tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDownload) - - bzzLocator := "bzz:/" + hash - flags = []string{ - "--bzzapi", cluster.Nodes[0].URL, - "down", - bzzLocator, - tmpDownload, - } - - down := runSwarm(t, flags...) - down.ExpectExit() - - fi, err := os.Stat(tmpDownload) - if err != nil { - t.Fatalf("could not stat path: %v", err) - } - - switch mode := fi.Mode(); { - case mode.IsRegular(): - downloadedBytes, err := ioutil.ReadFile(tmpDownload) - if err != nil { - t.Fatalf("had an error reading the downloaded file: %v", err) - } - if !bytes.Equal(downloadedBytes, bytes.NewBufferString(data).Bytes()) { - t.Fatalf("retrieved data and posted data not equal!") - } - - default: - t.Fatalf("expected to download regular file, got %s", fi.Mode()) - } - } - - timeout := time.Duration(2 * time.Second) - httpClient := http.Client{ - Timeout: timeout, - } - - // try to squeeze a timeout by getting an non-existent hash from each node - for _, node := range cluster.Nodes { - _, err := httpClient.Get(node.URL + "/bzz:/1023e8bae0f70be7d7b5f74343088ba408a218254391490c85ae16278e230340") - // we're speeding up the timeout here since netstore has a 60 seconds timeout on a request - if err != nil && !strings.Contains(err.Error(), "Client.Timeout exceeded while awaiting headers") { - t.Fatal(err) - } - // this is disabled since it takes 60s due to netstore timeout - // if res.StatusCode != 404 { - // t.Fatalf("expected HTTP status 404, got %s", res.Status) - // } - } -} - -func testRecursive(t *testing.T, cluster *testCluster, toEncrypt bool) { - tmpUploadDir, err := ioutil.TempDir("", "swarm-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpUploadDir) - // create tmp files - for _, path := range []string{"tmp1", "tmp2"} { - if err := ioutil.WriteFile(filepath.Join(tmpUploadDir, path), bytes.NewBufferString(data).Bytes(), 0644); err != nil { - t.Fatal(err) - } - } - - hashRegexp := `[a-f\d]{64}` - flags := []string{ - "--bzzapi", cluster.Nodes[0].URL, - "--recursive", - "up", - tmpUploadDir} - if toEncrypt { - hashRegexp = `[a-f\d]{128}` - flags = []string{ - "--bzzapi", cluster.Nodes[0].URL, - "--recursive", - "up", - "--encrypt", - tmpUploadDir} - } - // upload the file with 'swarm up' and expect a hash - log.Info(fmt.Sprintf("uploading file with 'swarm up'")) - up := runSwarm(t, flags...) - _, matches := up.ExpectRegexp(hashRegexp) - up.ExpectExit() - hash := matches[0] - log.Info("dir uploaded", "hash", hash) - - // get the file from the HTTP API of each node - for _, node := range cluster.Nodes { - log.Info("getting file from node", "node", node.Name) - //try to get the content with `swarm down` - tmpDownload, err := ioutil.TempDir("", "swarm-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDownload) - bzzLocator := "bzz:/" + hash - flagss := []string{ - "--bzzapi", cluster.Nodes[0].URL, - "down", - "--recursive", - bzzLocator, - tmpDownload, - } - - fmt.Println("downloading from swarm with recursive") - down := runSwarm(t, flagss...) - down.ExpectExit() - - files, err := ioutil.ReadDir(tmpDownload) - for _, v := range files { - fi, err := os.Stat(path.Join(tmpDownload, v.Name())) - if err != nil { - t.Fatalf("got an error: %v", err) - } - - switch mode := fi.Mode(); { - case mode.IsRegular(): - if file, err := swarmapi.Open(path.Join(tmpDownload, v.Name())); err != nil { - t.Fatalf("encountered an error opening the file returned from the CLI: %v", err) - } else { - ff := make([]byte, len(data)) - io.ReadFull(file, ff) - buf := bytes.NewBufferString(data) - - if !bytes.Equal(ff, buf.Bytes()) { - t.Fatalf("retrieved data and posted data not equal!") - } - } - default: - t.Fatalf("this shouldnt happen") - } - } - if err != nil { - t.Fatalf("could not list files at: %v", files) - } - } -} - -// testDefaultPathAll tests swarm recursive upload with relative and absolute -// default paths and with encryption. -func testDefaultPathAll(t *testing.T, cluster *testCluster) { - testDefaultPath(t, cluster, false, false) - testDefaultPath(t, cluster, false, true) - testDefaultPath(t, cluster, true, false) - testDefaultPath(t, cluster, true, true) -} - -func testDefaultPath(t *testing.T, cluster *testCluster, toEncrypt bool, absDefaultPath bool) { - tmp, err := ioutil.TempDir("", "swarm-defaultpath-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmp) - - err = ioutil.WriteFile(filepath.Join(tmp, "index.html"), []byte("<h1>Test</h1>"), 0666) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(tmp, "robots.txt"), []byte("Disallow: /"), 0666) - if err != nil { - t.Fatal(err) - } - - defaultPath := "index.html" - if absDefaultPath { - defaultPath = filepath.Join(tmp, defaultPath) - } - - args := []string{ - "--bzzapi", - cluster.Nodes[0].URL, - "--recursive", - "--defaultpath", - defaultPath, - "up", - tmp, - } - if toEncrypt { - args = append(args, "--encrypt") - } - - up := runSwarm(t, args...) - hashRegexp := `[a-f\d]{64,128}` - _, matches := up.ExpectRegexp(hashRegexp) - up.ExpectExit() - hash := matches[0] - - client := swarmapi.NewClient(cluster.Nodes[0].URL) - - m, isEncrypted, err := client.DownloadManifest(hash) - if err != nil { - t.Fatal(err) - } - - if toEncrypt != isEncrypted { - t.Error("downloaded manifest is not encrypted") - } - - var found bool - var entriesCount int - for _, e := range m.Entries { - entriesCount++ - if e.Path == "" { - found = true - } - } - - if !found { - t.Error("manifest default entry was not found") - } - - if entriesCount != 3 { - t.Errorf("manifest contains %v entries, expected %v", entriesCount, 3) - } -} |