diff --git a/Gopkg.lock b/Gopkg.lock index 11fdcff51..d5b9e6596 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -323,6 +323,12 @@ ] revision = "380174f817a09abe5982a82f94ad50938a8df65d" +[[projects]] + branch = "master" + name = "github.com/t3rm1n4l/go-mega" + packages = ["."] + revision = "4e68b16e97ffc3b77abacbf727817a4d48fb0b66" + [[projects]] branch = "master" name = "github.com/xanzy/ssh-agent" @@ -475,6 +481,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "50d82f1173802259032be4dddb962f1b7ed8eebdbc24c73febcde47d8deecb30" + inputs-digest = "09939c0d5f32998497c8304c84dd5c397c88816d235441d87f5306ae5db43b8a" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 6d280c0b2..e5e2d63a7 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -148,3 +148,7 @@ [[constraint]] branch = "master" name = "github.com/sevlyar/go-daemon" + +[[constraint]] + branch = "master" + name = "github.com/t3rm1n4l/go-mega" diff --git a/vendor/github.com/t3rm1n4l/go-mega/.travis.yml b/vendor/github.com/t3rm1n4l/go-mega/.travis.yml new file mode 100644 index 000000000..96561bbd5 --- /dev/null +++ b/vendor/github.com/t3rm1n4l/go-mega/.travis.yml @@ -0,0 +1,29 @@ +language: go +sudo: false +osx_image: xcode7.3 +os: +- linux +go: +- 1.6.4 +- 1.7.6 +- 1.8.7 +- 1.9.5 +- "1.10.1" +- tip +install: +- make build_dep +script: +- make check +- make test +matrix: + allow_failures: + - go: tip + include: + - os: osx + go: "1.10.1" +env: + global: + - secure: RzsF80V1i69FVJwKSF8WrFzk5bRUKtPxRkhjiLOO0b1usFg0EIY6XFp3s/VTR6oT91LRXml3Bp7wHHrkPvGnHyUyuxj6loj3gIrsX8cZHUtjyQX/Szfi9MOJpbdJvfCcHByEh9YGldAz//9zvEo5oGuI29Luur3cv+BJNJElmHg= + - secure: Eu3kWJbxpKyioitPQo75gI3gL/HKEHVMdp6YLxxcmlrbG2xyXdlFhTB2YkkmnC8jNvf7XJWdtYnhlWM9MrNY1fUiRyGSAmpSlzzCa9XQ9lCv0hUH57+D3PAcH6gdgKn6q1iOk26CxOCKAHVaj5xdDMIyCc4mD+sLyTDQhBIHABc= +notifications: + email: false diff --git a/vendor/github.com/t3rm1n4l/go-mega/Makefile b/vendor/github.com/t3rm1n4l/go-mega/Makefile new file mode 100644 index 000000000..7ce140041 --- /dev/null +++ b/vendor/github.com/t3rm1n4l/go-mega/Makefile @@ -0,0 +1,18 @@ +build: + go build + +test: + go test -cpu 4 -v -race + +# Get the build dependencies +build_dep: + go get -u github.com/kisielk/errcheck + go get -u golang.org/x/tools/cmd/goimports + go get -u github.com/golang/lint/golint + +# Do source code quality checks +check: + go vet + errcheck + goimports -d . | grep . ; test $$? -eq 1 + -#golint diff --git a/vendor/github.com/t3rm1n4l/go-mega/README.md b/vendor/github.com/t3rm1n4l/go-mega/README.md new file mode 100644 index 000000000..f5755f1a4 --- /dev/null +++ b/vendor/github.com/t3rm1n4l/go-mega/README.md @@ -0,0 +1,65 @@ +go-mega +======= + +A client library in go for mega.co.nz storage service. + +An implementation of command-line utility can be found at [https://github.com/t3rm1n4l/megacmd](https://github.com/t3rm1n4l/megacmd) + +[![Build Status](https://secure.travis-ci.org/t3rm1n4l/go-mega.png?branch=master)](http://travis-ci.org/t3rm1n4l/go-mega) + +### What can i do with this library? +This is an API client library for MEGA storage service. Currently, the library supports the basic APIs and operations as follows: + - User login + - Fetch filesystem tree + - Upload file + - Download file + - Create directory + - Move file or directory + - Rename file or directory + - Delete file or directory + - Parallel split download and upload + - Filesystem events auto sync + - Unit tests + +### API methods + +Please find full doc at [http://godoc.org/github.com/t3rm1n4l/go-mega](http://godoc.org/github.com/t3rm1n4l/go-mega) + +### Testing + + export MEGA_USER= + export MEGA_PASSWD= + $ make test + go test -v + === RUN TestLogin + --- PASS: TestLogin (1.90 seconds) + === RUN TestGetUser + --- PASS: TestGetUser (1.65 seconds) + === RUN TestUploadDownload + --- PASS: TestUploadDownload (12.28 seconds) + === RUN TestMove + --- PASS: TestMove (9.31 seconds) + === RUN TestRename + --- PASS: TestRename (9.16 seconds) + === RUN TestDelete + --- PASS: TestDelete (3.87 seconds) + === RUN TestCreateDir + --- PASS: TestCreateDir (2.34 seconds) + === RUN TestConfig + --- PASS: TestConfig (0.01 seconds) + === RUN TestPathLookup + --- PASS: TestPathLookup (8.54 seconds) + === RUN TestEventNotify + --- PASS: TestEventNotify (19.65 seconds) + PASS + ok github.com/t3rm1n4l/go-mega68.745s + +### TODO + - Implement APIs for public download url generation + - Implement download from public url + - Add shared user content management APIs + - Add contact list management APIs + +### License + +MIT diff --git a/vendor/github.com/t3rm1n4l/go-mega/errors.go b/vendor/github.com/t3rm1n4l/go-mega/errors.go new file mode 100644 index 000000000..343ea7c07 --- /dev/null +++ b/vendor/github.com/t3rm1n4l/go-mega/errors.go @@ -0,0 +1,85 @@ +package mega + +import ( + "errors" + "fmt" +) + +var ( + // General errors + EINTERNAL = errors.New("Internal error occured") + EARGS = errors.New("Invalid arguments") + EAGAIN = errors.New("Try again") + ERATELIMIT = errors.New("Rate limit reached") + EBADRESP = errors.New("Bad response from server") + + // Upload errors + EFAILED = errors.New("The upload failed. Please restart it from scratch") + ETOOMANY = errors.New("Too many concurrent IP addresses are accessing this upload target URL") + ERANGE = errors.New("The upload file packet is out of range or not starting and ending on a chunk boundary") + EEXPIRED = errors.New("The upload target URL you are trying to access has expired. Please request a fresh one") + + // Filesystem/Account errors + ENOENT = errors.New("Object (typically, node or user) not found") + ECIRCULAR = errors.New("Circular linkage attempted") + EACCESS = errors.New("Access violation") + EEXIST = errors.New("Trying to create an object that already exists") + EINCOMPLETE = errors.New("Trying to access an incomplete resource") + EKEY = errors.New("A decryption operation failed") + ESID = errors.New("Invalid or expired user session, please relogin") + EBLOCKED = errors.New("User blocked") + EOVERQUOTA = errors.New("Request over quota") + ETEMPUNAVAIL = errors.New("Resource temporarily not available, please try again later") + EMACMISMATCH = errors.New("MAC verification failed") + EBADATTR = errors.New("Bad node attribute") + + // Config errors + EWORKER_LIMIT_EXCEEDED = errors.New("Maximum worker limit exceeded") +) + +type ErrorMsg int + +func parseError(errno ErrorMsg) error { + switch { + case errno == 0: + return nil + case errno == -1: + return EINTERNAL + case errno == -2: + return EARGS + case errno == -3: + return EAGAIN + case errno == -4: + return ERATELIMIT + case errno == -5: + return EFAILED + case errno == -6: + return ETOOMANY + case errno == -7: + return ERANGE + case errno == -8: + return EEXPIRED + case errno == -9: + return ENOENT + case errno == -10: + return ECIRCULAR + case errno == -11: + return EACCESS + case errno == -12: + return EEXIST + case errno == -13: + return EINCOMPLETE + case errno == -14: + return EKEY + case errno == -15: + return ESID + case errno == -16: + return EBLOCKED + case errno == -17: + return EOVERQUOTA + case errno == -18: + return ETEMPUNAVAIL + } + + return fmt.Errorf("Unknown mega error %d", errno) +} diff --git a/vendor/github.com/t3rm1n4l/go-mega/mega.go b/vendor/github.com/t3rm1n4l/go-mega/mega.go new file mode 100644 index 000000000..6d22eeff4 --- /dev/null +++ b/vendor/github.com/t3rm1n4l/go-mega/mega.go @@ -0,0 +1,1724 @@ +package mega + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "math/big" + mrand "math/rand" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +// Default settings +const ( + API_URL = "https://g.api.mega.co.nz" + BASE_DOWNLOAD_URL = "https://mega.co.nz" + RETRIES = 10 + DOWNLOAD_WORKERS = 3 + MAX_DOWNLOAD_WORKERS = 30 + UPLOAD_WORKERS = 1 + MAX_UPLOAD_WORKERS = 30 + TIMEOUT = time.Second * 10 + minSleepTime = 10 * time.Millisecond // for retries + maxSleepTime = 5 * time.Second // for retries +) + +type config struct { + baseurl string + retries int + dl_workers int + ul_workers int + timeout time.Duration +} + +func newConfig() config { + return config{ + baseurl: API_URL, + retries: RETRIES, + dl_workers: DOWNLOAD_WORKERS, + ul_workers: UPLOAD_WORKERS, + timeout: TIMEOUT, + } +} + +// Set mega service base url +func (c *config) SetAPIUrl(u string) { + if strings.HasSuffix(u, "/") { + u = strings.TrimRight(u, "/") + } + c.baseurl = u +} + +// Set number of retries for api calls +func (c *config) SetRetries(r int) { + c.retries = r +} + +// Set concurrent download workers +func (c *config) SetDownloadWorkers(w int) error { + if w <= MAX_DOWNLOAD_WORKERS { + c.dl_workers = w + return nil + } + + return EWORKER_LIMIT_EXCEEDED +} + +// Set connection timeout +func (c *config) SetTimeOut(t time.Duration) { + c.timeout = t +} + +// Set concurrent upload workers +func (c *config) SetUploadWorkers(w int) error { + if w <= MAX_UPLOAD_WORKERS { + c.ul_workers = w + return nil + } + + return EWORKER_LIMIT_EXCEEDED +} + +type Mega struct { + config + // Sequence number + sn int64 + // Server state sn + ssn string + // Session ID + sid []byte + // Master key + k []byte + // User handle + uh []byte + // Filesystem object + FS *MegaFS + // HTTP Client + client *http.Client + // Loggers + logf func(format string, v ...interface{}) + debugf func(format string, v ...interface{}) + // serialize the API requests + apiMu sync.Mutex +} + +// Filesystem node types +const ( + FILE = 0 + FOLDER = 1 + ROOT = 2 + INBOX = 3 + TRASH = 4 +) + +// Filesystem node +type Node struct { + fs *MegaFS + name string + hash string + parent *Node + children []*Node + ntype int + size int64 + ts time.Time + meta NodeMeta +} + +func (n *Node) removeChild(c *Node) bool { + index := -1 + for i, v := range n.children { + if v.hash == c.hash { + index = i + break + } + } + + if index >= 0 { + n.children[index] = n.children[len(n.children)-1] + n.children = n.children[:len(n.children)-1] + return true + } + + return false +} + +func (n *Node) addChild(c *Node) { + if n != nil { + n.children = append(n.children, c) + } +} + +func (n *Node) getChildren() []*Node { + return n.children +} + +func (n *Node) GetType() int { + n.fs.mutex.Lock() + defer n.fs.mutex.Unlock() + return n.ntype +} + +func (n *Node) GetSize() int64 { + n.fs.mutex.Lock() + defer n.fs.mutex.Unlock() + return n.size +} + +func (n *Node) GetTimeStamp() time.Time { + n.fs.mutex.Lock() + defer n.fs.mutex.Unlock() + return n.ts +} + +func (n *Node) GetName() string { + n.fs.mutex.Lock() + defer n.fs.mutex.Unlock() + return n.name +} + +func (n *Node) GetHash() string { + n.fs.mutex.Lock() + defer n.fs.mutex.Unlock() + return n.hash +} + +type NodeMeta struct { + key []byte + compkey []byte + iv []byte + mac []byte +} + +// Mega filesystem object +type MegaFS struct { + root *Node + trash *Node + inbox *Node + sroots []*Node + lookup map[string]*Node + skmap map[string]string + mutex sync.Mutex +} + +// Get filesystem root node +func (fs *MegaFS) GetRoot() *Node { + fs.mutex.Lock() + defer fs.mutex.Unlock() + return fs.root +} + +// Get filesystem trash node +func (fs *MegaFS) GetTrash() *Node { + fs.mutex.Lock() + defer fs.mutex.Unlock() + return fs.trash +} + +// Get inbox node +func (fs *MegaFS) GetInbox() *Node { + fs.mutex.Lock() + defer fs.mutex.Unlock() + return fs.inbox +} + +// Get a node pointer from its hash +func (fs *MegaFS) HashLookup(h string) *Node { + fs.mutex.Lock() + defer fs.mutex.Unlock() + + return fs.hashLookup(h) +} + +func (fs *MegaFS) hashLookup(h string) *Node { + if node, ok := fs.lookup[h]; ok { + return node + } + + return nil +} + +// Get the list of child nodes for a given node +func (fs *MegaFS) GetChildren(n *Node) ([]*Node, error) { + fs.mutex.Lock() + defer fs.mutex.Unlock() + + var empty []*Node + + if n == nil { + return empty, EARGS + } + + node := fs.hashLookup(n.hash) + if node == nil { + return empty, ENOENT + } + + return node.getChildren(), nil +} + +// Retreive all the nodes in the given node tree path by name +// This method returns array of nodes upto the matched subpath +// (in same order as input names array) even if the target node is not located. +func (fs *MegaFS) PathLookup(root *Node, ns []string) ([]*Node, error) { + fs.mutex.Lock() + defer fs.mutex.Unlock() + + if root == nil { + return nil, EARGS + } + + var err error + var found bool = true + + nodepath := []*Node{} + + children := root.children + for _, name := range ns { + found = false + for _, n := range children { + if n.name == name { + nodepath = append(nodepath, n) + children = n.children + found = true + break + } + } + + if found == false { + break + } + } + + if found == false { + err = ENOENT + } + + return nodepath, err +} + +// Get top level directory nodes shared by other users +func (fs *MegaFS) GetSharedRoots() []*Node { + fs.mutex.Lock() + defer fs.mutex.Unlock() + return fs.sroots +} + +func newMegaFS() *MegaFS { + fs := &MegaFS{ + lookup: make(map[string]*Node), + skmap: make(map[string]string), + } + return fs +} + +func New() *Mega { + max := big.NewInt(0x100000000) + bigx, _ := rand.Int(rand.Reader, max) + cfg := newConfig() + mgfs := newMegaFS() + m := &Mega{ + config: cfg, + sn: bigx.Int64(), + FS: mgfs, + client: newHttpClient(cfg.timeout), + } + m.SetLogger(log.Printf) + m.SetDebugger(nil) + return m +} + +// SetClient sets the HTTP client in use +func (m *Mega) SetClient(client *http.Client) *Mega { + m.client = client + return m +} + +// discardLogf discards the log messages +func discardLogf(format string, v ...interface{}) { +} + +// SetLogger sets the logger for important messages. By default this +// is log.Printf. Use nil to discard the messages. +func (m *Mega) SetLogger(logf func(format string, v ...interface{})) *Mega { + if logf == nil { + logf = discardLogf + } + m.logf = logf + return m +} + +// SetDebugger sets the logger for debug messages. By default these +// messages are not output. +func (m *Mega) SetDebugger(debugf func(format string, v ...interface{})) *Mega { + if debugf == nil { + debugf = discardLogf + } + m.debugf = debugf + return m +} + +// backOffSleep sleeps for the time pointed to then adjusts it by +// doubling it up to a maximum of maxSleepTime. +// +// This produces a truncated exponential backoff sleep +func backOffSleep(pt *time.Duration) { + time.Sleep(*pt) + *pt *= 2 + if *pt > maxSleepTime { + *pt = maxSleepTime + } +} + +// API request method +func (m *Mega) api_request(r []byte) (buf []byte, err error) { + var resp *http.Response + // serialize the API requests + m.apiMu.Lock() + defer func() { + m.sn++ + m.apiMu.Unlock() + }() + + url := fmt.Sprintf("%s/cs?id=%d", m.baseurl, m.sn) + + if m.sid != nil { + url = fmt.Sprintf("%s&sid=%s", url, string(m.sid)) + } + + sleepTime := minSleepTime // inital backoff time + for i := 0; i < m.retries+1; i++ { + if i != 0 { + m.debugf("Retry API request %d/%d: %v", i, m.retries, err) + backOffSleep(&sleepTime) + } + resp, err = m.client.Post(url, "application/json", bytes.NewBuffer(r)) + if err != nil { + continue + } + if resp.StatusCode != 200 { + // err must be not-nil on a continue + err = errors.New("Http Status: " + resp.Status) + _ = resp.Body.Close() + continue + } + buf, err = ioutil.ReadAll(resp.Body) + if err != nil { + _ = resp.Body.Close() + continue + } + err = resp.Body.Close() + if err != nil { + continue + } + + // at this point the body is read and closed + + if bytes.HasPrefix(buf, []byte("[")) == false && bytes.HasPrefix(buf, []byte("-")) == false { + return nil, EBADRESP + } + + if len(buf) < 6 { + var emsg [1]ErrorMsg + err = json.Unmarshal(buf, &emsg) + if err != nil { + err = json.Unmarshal(buf, &emsg[0]) + } + if err != nil { + return buf, EBADRESP + } + err = parseError(emsg[0]) + if err == EAGAIN { + continue + } + return buf, err + } + + if err == nil { + return buf, nil + } + } + + return nil, err +} + +// Authenticate and start a session +func (m *Mega) Login(email string, passwd string) error { + var msg [1]LoginMsg + var res [1]LoginResp + var err error + var result []byte + + passkey := password_key(passwd) + uhandle := stringhash(email, passkey) + m.uh = make([]byte, len(uhandle)) + copy(m.uh, uhandle) + + msg[0].Cmd = "us" + msg[0].User = email + msg[0].Handle = string(uhandle) + + req, _ := json.Marshal(msg) + result, err = m.api_request(req) + + if err != nil { + return err + } + + err = json.Unmarshal(result, &res) + if err != nil { + return err + } + + m.k = base64urldecode([]byte(res[0].Key)) + cipher, err := aes.NewCipher(passkey) + cipher.Decrypt(m.k, m.k) + m.sid, err = decryptSessionId([]byte(res[0].Privk), []byte(res[0].Csid), m.k) + if err != nil { + return err + } + + err = m.getFileSystem() + + return err +} + +// Get user information +func (m *Mega) GetUser() (UserResp, error) { + var msg [1]UserMsg + var res [1]UserResp + + msg[0].Cmd = "ug" + + req, _ := json.Marshal(msg) + result, err := m.api_request(req) + + if err != nil { + return res[0], err + } + + err = json.Unmarshal(result, &res) + return res[0], err +} + +// Get quota information +func (m *Mega) GetQuota() (QuotaResp, error) { + var msg [1]QuotaMsg + var res [1]QuotaResp + + msg[0].Cmd = "uq" + msg[0].Xfer = 1 + msg[0].Strg = 1 + + req, _ := json.Marshal(msg) + result, err := m.api_request(req) + if err != nil { + return res[0], err + } + + err = json.Unmarshal(result, &res) + return res[0], err +} + +// Add a node into filesystem +func (m *Mega) addFSNode(itm FSNode) (*Node, error) { + var compkey, key []uint32 + var attr FileAttr + var node, parent *Node + var err error + + master_aes, _ := aes.NewCipher(m.k) + + switch { + case itm.T == FOLDER || itm.T == FILE: + args := strings.Split(itm.Key, ":") + + switch { + // File or folder owned by current user + case args[0] == itm.User: + buf := base64urldecode([]byte(args[1])) + err = blockDecrypt(master_aes, buf, buf) + if err != nil { + return nil, err + } + compkey = bytes_to_a32(buf) + // Shared folder + case itm.SUser != "" && itm.SKey != "": + sk := base64urldecode([]byte(itm.SKey)) + err = blockDecrypt(master_aes, sk, sk) + if err != nil { + return nil, err + } + sk_aes, _ := aes.NewCipher(sk) + + m.FS.skmap[itm.Hash] = itm.SKey + buf := base64urldecode([]byte(args[1])) + err = blockDecrypt(sk_aes, buf, buf) + if err != nil { + return nil, err + } + compkey = bytes_to_a32(buf) + // Shared file + default: + k := m.FS.skmap[args[0]] + b := base64urldecode([]byte(k)) + err = blockDecrypt(master_aes, b, b) + if err != nil { + return nil, err + } + block, _ := aes.NewCipher(b) + buf := base64urldecode([]byte(args[1])) + err = blockDecrypt(block, buf, buf) + if err != nil { + return nil, err + } + compkey = bytes_to_a32(buf) + } + + switch { + case itm.T == FILE: + key = []uint32{compkey[0] ^ compkey[4], compkey[1] ^ compkey[5], compkey[2] ^ compkey[6], compkey[3] ^ compkey[7]} + default: + key = compkey + } + + attr, err = decryptAttr(a32_to_bytes(key), []byte(itm.Attr)) + // FIXME: + if err != nil { + attr.Name = "BAD ATTRIBUTE" + } + } + + n, ok := m.FS.lookup[itm.Hash] + switch { + case ok: + node = n + default: + node = &Node{ + fs: m.FS, + ntype: itm.T, + size: itm.Sz, + ts: time.Unix(itm.Ts, 0), + } + + m.FS.lookup[itm.Hash] = node + } + + n, ok = m.FS.lookup[itm.Parent] + switch { + case ok: + parent = n + parent.removeChild(node) + parent.addChild(node) + default: + parent = nil + if itm.Parent != "" { + parent = &Node{ + fs: m.FS, + children: []*Node{node}, + ntype: FOLDER, + } + m.FS.lookup[itm.Parent] = parent + } + } + + switch { + case itm.T == FILE: + var meta NodeMeta + meta.key = a32_to_bytes(key) + meta.iv = a32_to_bytes([]uint32{compkey[4], compkey[5], 0, 0}) + meta.mac = a32_to_bytes([]uint32{compkey[6], compkey[7]}) + meta.compkey = a32_to_bytes(compkey) + node.meta = meta + case itm.T == FOLDER: + var meta NodeMeta + meta.key = a32_to_bytes(key) + meta.compkey = a32_to_bytes(compkey) + node.meta = meta + case itm.T == ROOT: + attr.Name = "Cloud Drive" + m.FS.root = node + case itm.T == INBOX: + attr.Name = "InBox" + m.FS.inbox = node + case itm.T == TRASH: + attr.Name = "Trash" + m.FS.trash = node + } + + // Shared directories + if itm.SUser != "" && itm.SKey != "" { + m.FS.sroots = append(m.FS.sroots, node) + } + + node.name = attr.Name + node.hash = itm.Hash + node.parent = parent + node.ntype = itm.T + + return node, nil +} + +// Get all nodes from filesystem +func (m *Mega) getFileSystem() error { + m.FS.mutex.Lock() + defer m.FS.mutex.Unlock() + + var msg [1]FilesMsg + var res [1]FilesResp + + msg[0].Cmd = "f" + msg[0].C = 1 + + req, _ := json.Marshal(msg) + result, err := m.api_request(req) + + if err != nil { + return err + } + + err = json.Unmarshal(result, &res) + if err != nil { + return err + } + + for _, sk := range res[0].Ok { + m.FS.skmap[sk.Hash] = sk.Key + } + + for _, itm := range res[0].F { + _, err = m.addFSNode(itm) + if err != nil { + return err + } + } + + m.ssn = res[0].Sn + + go m.pollEvents() + + return nil +} + +// Download contains the internal state of a download +type Download struct { + m *Mega + src *Node + resourceUrl string + aes_block cipher.Block + iv []byte + mac_enc cipher.BlockMode + mutex sync.Mutex // to protect the following + chunks []chunkSize + chunk_macs [][]byte +} + +// an all nil IV for mac calculations +var zero_iv = make([]byte, 16) + +// Create a new Download from the src Node +// +// Call Chunks to find out how many chunks there are, then for id = +// 0..chunks-1 call DownloadChunk. Finally call Finish() to receive +// the error status. +func (m *Mega) NewDownload(src *Node) (*Download, error) { + if src == nil { + return nil, EARGS + } + + var msg [1]DownloadMsg + var res [1]DownloadResp + + m.FS.mutex.Lock() + msg[0].Cmd = "g" + msg[0].G = 1 + msg[0].N = src.hash + key := src.meta.key + m.FS.mutex.Unlock() + + request, err := json.Marshal(msg) + if err != nil { + return nil, err + } + result, err := m.api_request(request) + if err != nil { + return nil, err + } + + err = json.Unmarshal(result, &res) + if err != nil { + return nil, err + } + + _, err = decryptAttr(key, []byte(res[0].Attr)) + if err != nil { + return nil, err + } + + chunks := getChunkSizes(int64(res[0].Size)) + + aes_block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + mac_enc := cipher.NewCBCEncrypter(aes_block, zero_iv) + t := bytes_to_a32(src.meta.iv) + iv := a32_to_bytes([]uint32{t[0], t[1], t[0], t[1]}) + + d := &Download{ + m: m, + src: src, + resourceUrl: res[0].G, + aes_block: aes_block, + iv: iv, + mac_enc: mac_enc, + chunks: chunks, + chunk_macs: make([][]byte, len(chunks)), + } + return d, nil +} + +// Chunks returns The number of chunks in the download. +func (d *Download) Chunks() int { + return len(d.chunks) +} + +// ChunkLocation returns the position in the file and the size of the chunk +func (d *Download) ChunkLocation(id int) (position int64, size int, err error) { + if id < 0 || id >= len(d.chunks) { + return 0, 0, EARGS + } + d.mutex.Lock() + defer d.mutex.Unlock() + return d.chunks[id].position, d.chunks[id].size, nil +} + +// DownloadChunk gets a chunk with the given number and update the +// mac, returning the position in the file of the chunk +func (d *Download) DownloadChunk(id int) (chunk []byte, err error) { + if id < 0 || id >= len(d.chunks) { + return nil, EARGS + } + + chk_start, chk_size, err := d.ChunkLocation(id) + if err != nil { + return nil, err + } + + var resp *http.Response + chunk_url := fmt.Sprintf("%s/%d-%d", d.resourceUrl, chk_start, chk_start+int64(chk_size)-1) + sleepTime := minSleepTime // inital backoff time + for retry := 0; retry < d.m.retries+1; retry++ { + resp, err = d.m.client.Get(chunk_url) + if err == nil { + if resp.StatusCode == 200 { + break + } + err = errors.New("Http Status: " + resp.Status) + _ = resp.Body.Close() + } + d.m.debugf("%s: Retry download chunk %d/%d: %v", d.src.name, retry, d.m.retries, err) + backOffSleep(&sleepTime) + } + if err != nil { + return nil, err + } + if resp == nil { + return nil, errors.New("retries exceeded") + } + + chunk, err = ioutil.ReadAll(resp.Body) + if err != nil { + _ = resp.Body.Close() + return nil, err + } + + err = resp.Body.Close() + if err != nil { + return nil, err + } + + // body is read and closed here + + if len(chunk) != chk_size { + return nil, errors.New("wrong size for downloaded chunk") + } + + // Decrypt the block + ctr_iv := bytes_to_a32(d.src.meta.iv) + ctr_iv[2] = uint32(uint64(chk_start) / 0x1000000000) + ctr_iv[3] = uint32(chk_start / 0x10) + ctr_aes := cipher.NewCTR(d.aes_block, a32_to_bytes(ctr_iv)) + ctr_aes.XORKeyStream(chunk, chunk) + + // Update the chunk_macs + enc := cipher.NewCBCEncrypter(d.aes_block, d.iv) + i := 0 + block := make([]byte, 16) + paddedChunk := paddnull(chunk, 16) + for i = 0; i < len(paddedChunk); i += 16 { + enc.CryptBlocks(block, paddedChunk[i:i+16]) + } + + d.mutex.Lock() + if len(d.chunk_macs) > 0 { + d.chunk_macs[id] = make([]byte, 16) + copy(d.chunk_macs[id], block) + } + d.mutex.Unlock() + + return chunk, nil +} + +// Finish checks the accumulated MAC for each block. +// +// If all the chunks weren't downloaded then it will just return nil +func (d *Download) Finish() (err error) { + // Can't check a 0 sized file + if len(d.chunk_macs) == 0 { + return nil + } + mac_data := make([]byte, 16) + for _, v := range d.chunk_macs { + // If a chunk_macs hasn't been set then the whole file + // wasn't downloaded and we can't check it + if v == nil { + return nil + } + d.mac_enc.CryptBlocks(mac_data, v) + } + + tmac := bytes_to_a32(mac_data) + if bytes.Equal(a32_to_bytes([]uint32{tmac[0] ^ tmac[1], tmac[2] ^ tmac[3]}), d.src.meta.mac) == false { + return EMACMISMATCH + } + + return nil +} + +// Download file from filesystem reporting progress if not nil +func (m *Mega) DownloadFile(src *Node, dstpath string, progress *chan int) error { + defer func() { + if progress != nil { + close(*progress) + } + }() + + d, err := m.NewDownload(src) + if err != nil { + return err + } + + _, err = os.Stat(dstpath) + if os.IsExist(err) { + err = os.Remove(dstpath) + if err != nil { + return err + } + } + + outfile, err := os.OpenFile(dstpath, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return err + } + + workch := make(chan int) + errch := make(chan error, m.dl_workers) + wg := sync.WaitGroup{} + + // Fire chunk download workers + for w := 0; w < m.dl_workers; w++ { + wg.Add(1) + + go func() { + defer wg.Done() + + // Wait for work blocked on channel + for id := range workch { + chunk, err := d.DownloadChunk(id) + if err != nil { + errch <- err + return + } + + chk_start, _, err := d.ChunkLocation(id) + if err != nil { + errch <- err + return + } + + _, err = outfile.WriteAt(chunk, chk_start) + if err != nil { + errch <- err + return + } + + if progress != nil { + *progress <- len(chunk) + } + } + }() + } + + // Place chunk download jobs to chan + err = nil + for id := 0; id < d.Chunks() && err == nil; { + select { + case workch <- id: + id++ + case err = <-errch: + } + } + close(workch) + + wg.Wait() + + closeErr := outfile.Close() + if err != nil { + _ = os.Remove(dstpath) + return err + } + if closeErr != nil { + return closeErr + } + + return d.Finish() +} + +// Upload contains the internal state of a upload +type Upload struct { + m *Mega + parenthash string + name string + uploadUrl string + aes_block cipher.Block + iv []byte + kiv []byte + mac_enc cipher.BlockMode + kbytes []byte + ukey []uint32 + mutex sync.Mutex // to protect the following + chunks []chunkSize + chunk_macs [][]byte + completion_handle []byte +} + +// Create a new Upload of name into parent of fileSize +// +// Call Chunks to find out how many chunks there are, then for id = +// 0..chunks-1 Call ChunkLocation then UploadChunk. Finally call +// Finish() to receive the error status and the *Node. +func (m *Mega) NewUpload(parent *Node, name string, fileSize int64) (*Upload, error) { + if parent == nil { + return nil, EARGS + } + + var msg [1]UploadMsg + var res [1]UploadResp + parenthash := parent.GetHash() + + msg[0].Cmd = "u" + msg[0].S = fileSize + + request, err := json.Marshal(msg) + if err != nil { + return nil, err + } + result, err := m.api_request(request) + if err != nil { + return nil, err + } + + err = json.Unmarshal(result, &res) + if err != nil { + return nil, err + } + + uploadUrl := res[0].P + ukey := []uint32{0, 0, 0, 0, 0, 0} + for i, _ := range ukey { + ukey[i] = uint32(mrand.Int31()) + + } + + kbytes := a32_to_bytes(ukey[:4]) + kiv := a32_to_bytes([]uint32{ukey[4], ukey[5], 0, 0}) + aes_block, _ := aes.NewCipher(kbytes) + + mac_enc := cipher.NewCBCEncrypter(aes_block, zero_iv) + iv := a32_to_bytes([]uint32{ukey[4], ukey[5], ukey[4], ukey[5]}) + + chunks := getChunkSizes(fileSize) + + // File size is zero + // Do one empty request to get the completion handle + if len(chunks) == 0 { + chunks = append(chunks, chunkSize{position: 0, size: 0}) + } + + u := &Upload{ + m: m, + parenthash: parenthash, + name: name, + uploadUrl: uploadUrl, + aes_block: aes_block, + iv: iv, + kiv: kiv, + mac_enc: mac_enc, + kbytes: kbytes, + ukey: ukey, + chunks: chunks, + chunk_macs: make([][]byte, len(chunks)), + completion_handle: []byte{}, + } + return u, nil +} + +// Chunks returns The number of chunks in the upload. +func (u *Upload) Chunks() int { + return len(u.chunks) +} + +// ChunkLocation returns the position in the file and the size of the chunk +func (u *Upload) ChunkLocation(id int) (position int64, size int, err error) { + if id < 0 || id >= len(u.chunks) { + return 0, 0, EARGS + } + return u.chunks[id].position, u.chunks[id].size, nil +} + +// UploadChunk uploads the chunk of id +func (u *Upload) UploadChunk(id int, chunk []byte) (err error) { + chk_start, chk_size, err := u.ChunkLocation(id) + if err != nil { + return err + } + if len(chunk) != chk_size { + return errors.New("upload chunk is wrong size") + } + ctr_iv := bytes_to_a32(u.kiv) + ctr_iv[2] = uint32(uint64(chk_start) / 0x1000000000) + ctr_iv[3] = uint32(chk_start / 0x10) + ctr_aes := cipher.NewCTR(u.aes_block, a32_to_bytes(ctr_iv)) + + enc := cipher.NewCBCEncrypter(u.aes_block, u.iv) + + i := 0 + block := make([]byte, 16) + paddedchunk := paddnull(chunk, 16) + for i = 0; i < len(paddedchunk); i += 16 { + copy(block[0:16], paddedchunk[i:i+16]) + enc.CryptBlocks(block, block) + } + + var rsp *http.Response + var req *http.Request + ctr_aes.XORKeyStream(chunk, chunk) + chk_url := fmt.Sprintf("%s/%d", u.uploadUrl, chk_start) + + chunk_resp := []byte{} + sleepTime := minSleepTime // inital backoff time + for retry := 0; retry < u.m.retries+1; retry++ { + reader := bytes.NewBuffer(chunk) + req, err = http.NewRequest("POST", chk_url, reader) + if err != nil { + return err + } + rsp, err = u.m.client.Do(req) + if err == nil { + if rsp.StatusCode == 200 { + break + } + err = errors.New("Http Status: " + rsp.Status) + _ = rsp.Body.Close() + } + u.m.debugf("%s: Retry upload chunk %d/%d: %v", u.name, retry, u.m.retries, err) + backOffSleep(&sleepTime) + } + if err != nil { + return err + } + if rsp == nil { + return errors.New("retries exceeded") + } + + chunk_resp, err = ioutil.ReadAll(rsp.Body) + if err != nil { + _ = rsp.Body.Close() + return err + } + + err = rsp.Body.Close() + if err != nil { + return err + } + + if bytes.Equal(chunk_resp, nil) == false { + u.mutex.Lock() + u.completion_handle = chunk_resp + u.mutex.Unlock() + } + + // Update chunk MACs on success only + u.mutex.Lock() + if len(u.chunk_macs) > 0 { + u.chunk_macs[id] = make([]byte, 16) + copy(u.chunk_macs[id], block) + } + u.mutex.Unlock() + + return nil +} + +// Finish completes the upload and returns the created node +func (u *Upload) Finish() (node *Node, err error) { + mac_data := make([]byte, 16) + for _, v := range u.chunk_macs { + u.mac_enc.CryptBlocks(mac_data, v) + } + + t := bytes_to_a32(mac_data) + meta_mac := []uint32{t[0] ^ t[1], t[2] ^ t[3]} + + attr := FileAttr{u.name} + + attr_data, err := encryptAttr(u.kbytes, attr) + if err != nil { + return nil, err + } + + key := []uint32{u.ukey[0] ^ u.ukey[4], u.ukey[1] ^ u.ukey[5], + u.ukey[2] ^ meta_mac[0], u.ukey[3] ^ meta_mac[1], + u.ukey[4], u.ukey[5], meta_mac[0], meta_mac[1]} + + buf := a32_to_bytes(key) + master_aes, err := aes.NewCipher(u.m.k) + if err != nil { + return nil, err + } + enc := cipher.NewCBCEncrypter(master_aes, zero_iv) + enc.CryptBlocks(buf[:16], buf[:16]) + enc = cipher.NewCBCEncrypter(master_aes, zero_iv) + enc.CryptBlocks(buf[16:], buf[16:]) + + var cmsg [1]UploadCompleteMsg + var cres [1]UploadCompleteResp + + cmsg[0].Cmd = "p" + cmsg[0].T = u.parenthash + cmsg[0].N[0].H = string(u.completion_handle) + cmsg[0].N[0].T = FILE + cmsg[0].N[0].A = string(attr_data) + cmsg[0].N[0].K = string(base64urlencode(buf)) + + request, err := json.Marshal(cmsg) + if err != nil { + return nil, err + } + result, err := u.m.api_request(request) + if err != nil { + return nil, err + } + + err = json.Unmarshal(result, &cres) + if err != nil { + return nil, err + } + + u.m.FS.mutex.Lock() + defer u.m.FS.mutex.Unlock() + return u.m.addFSNode(cres[0].F[0]) +} + +// Upload a file to the filesystem +func (m *Mega) UploadFile(srcpath string, parent *Node, name string, progress *chan int) (*Node, error) { + defer func() { + if progress != nil { + close(*progress) + } + }() + + var infile *os.File + var fileSize int64 + + info, err := os.Stat(srcpath) + if err == nil { + fileSize = info.Size() + } + + infile, err = os.OpenFile(srcpath, os.O_RDONLY, 0666) + if err != nil { + return nil, err + } + + if name == "" { + name = filepath.Base(srcpath) + } + + u, err := m.NewUpload(parent, name, fileSize) + if err != nil { + return nil, err + } + + workch := make(chan int) + errch := make(chan error, m.ul_workers) + wg := sync.WaitGroup{} + + // Fire chunk upload workers + for w := 0; w < m.ul_workers; w++ { + wg.Add(1) + + go func() { + defer wg.Done() + + for id := range workch { + chk_start, chk_size, err := u.ChunkLocation(id) + if err != nil { + errch <- err + return + } + chunk := make([]byte, chk_size) + n, err := infile.ReadAt(chunk, chk_start) + if err != nil && err != io.EOF { + errch <- err + return + } + if n != len(chunk) { + errch <- errors.New("chunk too short") + return + } + + err = u.UploadChunk(id, chunk) + if err != nil { + errch <- err + return + } + + if progress != nil { + *progress <- chk_size + } + } + }() + } + + // Place chunk download jobs to chan + err = nil + for id := 0; id < u.Chunks() && err == nil; { + select { + case workch <- id: + id++ + case err = <-errch: + } + } + + close(workch) + + wg.Wait() + + if err != nil { + return nil, err + } + + return u.Finish() +} + +// Move a file from one location to another +func (m *Mega) Move(src *Node, parent *Node) error { + m.FS.mutex.Lock() + defer m.FS.mutex.Unlock() + + if src == nil || parent == nil { + return EARGS + } + var msg [1]MoveFileMsg + var err error + + msg[0].Cmd = "m" + msg[0].N = src.hash + msg[0].T = parent.hash + msg[0].I, err = randString(10) + if err != nil { + return err + } + + request, _ := json.Marshal(msg) + _, err = m.api_request(request) + + if err != nil { + return err + } + + if src.parent != nil { + src.parent.removeChild(src) + } + + parent.addChild(src) + src.parent = parent + + return nil +} + +// Rename a file or folder +func (m *Mega) Rename(src *Node, name string) error { + m.FS.mutex.Lock() + defer m.FS.mutex.Unlock() + + if src == nil { + return EARGS + } + var msg [1]FileAttrMsg + + master_aes, _ := aes.NewCipher(m.k) + attr := FileAttr{name} + attr_data, _ := encryptAttr(src.meta.key, attr) + key := make([]byte, len(src.meta.compkey)) + err := blockEncrypt(master_aes, key, src.meta.compkey) + if err != nil { + return err + } + + msg[0].Cmd = "a" + msg[0].Attr = string(attr_data) + msg[0].Key = string(base64urlencode(key)) + msg[0].N = src.hash + msg[0].I, err = randString(10) + if err != nil { + return err + } + + req, _ := json.Marshal(msg) + _, err = m.api_request(req) + + src.name = name + + return err +} + +// Create a directory in the filesystem +func (m *Mega) CreateDir(name string, parent *Node) (*Node, error) { + m.FS.mutex.Lock() + defer m.FS.mutex.Unlock() + + if parent == nil { + return nil, EARGS + } + var msg [1]UploadCompleteMsg + var res [1]UploadCompleteResp + + compkey := []uint32{0, 0, 0, 0, 0, 0} + for i, _ := range compkey { + compkey[i] = uint32(mrand.Int31()) + } + + master_aes, _ := aes.NewCipher(m.k) + attr := FileAttr{name} + ukey := a32_to_bytes(compkey[:4]) + attr_data, _ := encryptAttr(ukey, attr) + key := make([]byte, len(ukey)) + err := blockEncrypt(master_aes, key, ukey) + if err != nil { + return nil, err + } + + msg[0].Cmd = "p" + msg[0].T = parent.hash + msg[0].N[0].H = "xxxxxxxx" + msg[0].N[0].T = FOLDER + msg[0].N[0].A = string(attr_data) + msg[0].N[0].K = string(base64urlencode(key)) + msg[0].I, err = randString(10) + if err != nil { + return nil, err + } + + req, _ := json.Marshal(msg) + result, err := m.api_request(req) + if err != nil { + return nil, err + } + + err = json.Unmarshal(result, &res) + if err != nil { + return nil, err + } + node, err := m.addFSNode(res[0].F[0]) + + return node, err +} + +// Delete a file or directory from filesystem +func (m *Mega) Delete(node *Node, destroy bool) error { + if node == nil { + return EARGS + } + if destroy == false { + return m.Move(node, m.FS.trash) + } + + m.FS.mutex.Lock() + defer m.FS.mutex.Unlock() + + var msg [1]FileDeleteMsg + var err error + msg[0].Cmd = "d" + msg[0].N = node.hash + msg[0].I, err = randString(10) + if err != nil { + return err + } + + req, _ := json.Marshal(msg) + _, err = m.api_request(req) + + parent := m.FS.lookup[node.hash] + parent.removeChild(node) + delete(m.FS.lookup, node.hash) + + return err +} + +// process an add node event +func (m *Mega) processAddNode(evRaw []byte) error { + m.FS.mutex.Lock() + defer m.FS.mutex.Unlock() + + var ev FSEvent + err := json.Unmarshal(evRaw, &ev) + if err != nil { + return err + } + + for _, itm := range ev.T.Files { + _, err = m.addFSNode(itm) + if err != nil { + return err + } + } + return nil +} + +// process an update node event +func (m *Mega) processUpdateNode(evRaw []byte) error { + m.FS.mutex.Lock() + defer m.FS.mutex.Unlock() + + var ev FSEvent + err := json.Unmarshal(evRaw, &ev) + if err != nil { + return err + } + + node := m.FS.hashLookup(ev.N) + attr, err := decryptAttr(node.meta.key, []byte(ev.Attr)) + if err == nil { + node.name = attr.Name + } else { + node.name = "BAD ATTRIBUTE" + } + + node.ts = time.Unix(ev.Ts, 0) + return nil +} + +// process a delete node event +func (m *Mega) processDeleteNode(evRaw []byte) error { + m.FS.mutex.Lock() + defer m.FS.mutex.Unlock() + + var ev FSEvent + err := json.Unmarshal(evRaw, &ev) + if err != nil { + return err + } + + node := m.FS.hashLookup(ev.N) + if node != nil && node.parent != nil { + node.parent.removeChild(node) + delete(m.FS.lookup, node.hash) + } + return nil +} + +// Listen for server event notifications and play actions +func (m *Mega) pollEvents() { + var err error + var resp *http.Response + sleepTime := minSleepTime // inital backoff time + for { + if err != nil { + m.debugf("pollEvents: error from server", err) + backOffSleep(&sleepTime) + } else { + // reset sleep time to minimum on success + sleepTime = minSleepTime + } + + url := fmt.Sprintf("%s/sc?sn=%s&sid=%s", m.baseurl, m.ssn, string(m.sid)) + resp, err = m.client.Post(url, "application/xml", nil) + if err != nil { + m.logf("pollEvents: Error fetching status: %s", err) + continue + } + + if resp.StatusCode != 200 { + m.logf("pollEvents: Error from server: %s", resp.Status) + _ = resp.Body.Close() + continue + } + + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + m.logf("pollEvents: Error reading body: %v", err) + _ = resp.Body.Close() + continue + } + err = resp.Body.Close() + if err != nil { + m.logf("pollEvents: Error closing body: %v", err) + continue + } + + // body is read and closed here + + // First attempt to parse an array + var events Events + err = json.Unmarshal(buf, &events) + if err != nil { + // Try parsing as a lone error message + var emsg ErrorMsg + err = json.Unmarshal(buf, &emsg) + if err != nil { + m.logf("pollEvents: Bad response received from server: %s", buf) + } else { + err = parseError(emsg) + if err == EAGAIN { + } else if err != nil { + m.logf("pollEvents: Error received from server: %v", err) + } + } + continue + } + + // if wait URL is set, then fetch it and continue - we + // don't expect anything else if we have a wait URL. + if events.W != "" { + if len(events.E) > 0 { + m.logf("pollEvents: Unexpected event with w set: %s", buf) + } + resp, err = m.client.Get(events.W) + if err == nil { + _ = resp.Body.Close() + } + continue + } + m.ssn = events.Sn + + // For each event in the array, parse it + for _, evRaw := range events.E { + // First attempt to unmarshal as an error message + var emsg ErrorMsg + err = json.Unmarshal(evRaw, &emsg) + if err == nil { + m.logf("pollEvents: Error message received %s", evRaw) + err = parseError(emsg) + if err != nil { + m.logf("pollEvents: Event from server was error: %v", err) + } + continue + } + + // Now unmarshal as a generic event + var gev GenericEvent + err = json.Unmarshal(evRaw, &gev) + if err != nil { + m.logf("pollEvents: Couldn't parse event from server: %v: %s", err, evRaw) + continue + } + m.debugf("pollEvents: Parsing event %q: %s", gev.Cmd, evRaw) + + // Work out what to do with the event + var process func([]byte) error + switch gev.Cmd { + case "t": // node addition + process = m.processAddNode + case "u": // node update + process = m.processUpdateNode + case "d": // node deletion + process = m.processDeleteNode + case "s", "s2": // share addition/update/revocation + case "c": // contact addition/update + case "k": // crypto key request + case "fa": // file attribute update + case "ua": // user attribute update + case "psts": // account updated + case "ipc": // incoming pending contact request (to us) + case "opc": // outgoing pending contact request (from us) + case "upci": // incoming pending contact request update (accept/deny/ignore) + case "upco": // outgoing pending contact request update (from them, accept/deny/ignore) + case "ph": // public links handles + case "se": // set email + case "mcc": // chat creation / peer's invitation / peer's removal + case "mcna": // granted / revoked access to a node + case "uac": // user access control + default: + m.debugf("pollEvents: Unknown message %q received: %s", gev.Cmd, evRaw) + } + + // process the event if we can + if process != nil { + err := process(evRaw) + if err != nil { + m.logf("pollEvents: Error processing event %q '%s': %v", gev.Cmd, evRaw, err) + } + } + } + } +} + +func (m *Mega) getLink(n *Node) (string, error) { + var msg [1]GetLinkMsg + var res [1]string + + msg[0].Cmd = "l" + msg[0].N = n.GetHash() + + req, _ := json.Marshal(msg) + result, err := m.api_request(req) + + if err != nil { + return "", err + } + err = json.Unmarshal(result, &res) + if err != nil { + return "", err + } + return res[0], nil +} + +// Exports public link for node, with or without decryption key included +func (m *Mega) Link(n *Node, includeKey bool) (string, error) { + id, err := m.getLink(n) + if err != nil { + return "", err + } + if includeKey { + m.FS.mutex.Lock() + key := string(base64urlencode(n.meta.compkey)) + m.FS.mutex.Unlock() + return fmt.Sprintf("%v/#!%v!%v", BASE_DOWNLOAD_URL, id, key), nil + } else { + return fmt.Sprintf("%v/#!%v", BASE_DOWNLOAD_URL, id), nil + } +} diff --git a/vendor/github.com/t3rm1n4l/go-mega/mega_test.go b/vendor/github.com/t3rm1n4l/go-mega/mega_test.go new file mode 100644 index 000000000..919e2ae33 --- /dev/null +++ b/vendor/github.com/t3rm1n4l/go-mega/mega_test.go @@ -0,0 +1,351 @@ +package mega + +import ( + "crypto/md5" + "crypto/rand" + "fmt" + "io/ioutil" + "os" + "path" + "testing" + "time" +) + +var USER string = os.Getenv("MEGA_USER") +var PASSWORD string = os.Getenv("MEGA_PASSWD") + +// retry runs fn until it succeeds, using what to log and retrying on +// EAGAIN. It uses exponential backoff +func retry(t *testing.T, what string, fn func() error) { + const maxTries = 10 + var err error + sleep := 100 * time.Millisecond + for i := 1; i <= maxTries; i++ { + err = fn() + if err == nil { + return + } + if err != EAGAIN { + break + } + t.Logf("%s failed %d/%d - retrying after %v sleep", what, i, maxTries, sleep) + time.Sleep(sleep) + sleep *= 2 + } + t.Fatalf("%s failed: %v", what, err) +} + +func initSession(t *testing.T) *Mega { + m := New() + // m.SetDebugger(log.Printf) + retry(t, "Login", func() error { + return m.Login(USER, PASSWORD) + }) + return m +} + +// createFile creates a temporary file of a given size along with its MD5SUM +func createFile(t *testing.T, size int64) (string, string) { + b := make([]byte, size) + _, err := rand.Read(b) + if err != nil { + t.Fatalf("Error reading rand: %v", err) + } + file, err := ioutil.TempFile("/tmp/", "gomega-") + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + _, err = file.Write(b) + if err != nil { + t.Fatalf("Error writing temp file: %v", err) + } + h := md5.New() + _, err = h.Write(b) + if err != nil { + t.Fatalf("Error on Write while writing temp file: %v", err) + } + return file.Name(), fmt.Sprintf("%x", h.Sum(nil)) +} + +// uploadFile uploads a temporary file of a given size returning the +// node, name and its MD5SUM +func uploadFile(t *testing.T, session *Mega, size int64, parent *Node) (node *Node, name string, md5sum string) { + name, md5sum = createFile(t, size) + defer func() { + _ = os.Remove(name) + }() + var err error + retry(t, fmt.Sprintf("Upload %q", name), func() error { + node, err = session.UploadFile(name, parent, "", nil) + return err + }) + if node == nil { + t.Fatalf("Failed to obtain node after upload for %q", name) + } + return node, name, md5sum +} + +// createDir creates a directory under parent +func createDir(t *testing.T, session *Mega, name string, parent *Node) (node *Node) { + var err error + retry(t, fmt.Sprintf("Create directory %q", name), func() error { + node, err = session.CreateDir(name, parent) + return err + }) + return node +} + +func fileMD5(t *testing.T, name string) string { + file, err := os.Open(name) + if err != nil { + t.Fatalf("Failed to open %q: %v", name, err) + } + b, err := ioutil.ReadAll(file) + if err != nil { + t.Fatalf("Failed to read all %q: %v", name, err) + } + h := md5.New() + _, err = h.Write(b) + if err != nil { + t.Fatalf("Error on hash in fileMD5: %v", err) + } + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func TestLogin(t *testing.T) { + m := New() + retry(t, "Login", func() error { + return m.Login(USER, PASSWORD) + }) +} + +func TestGetUser(t *testing.T) { + session := initSession(t) + _, err := session.GetUser() + if err != nil { + t.Fatal("GetUser failed", err) + } +} + +func TestUploadDownload(t *testing.T) { + session := initSession(t) + node, name, h1 := uploadFile(t, session, 314573, session.FS.root) + + session.FS.mutex.Lock() + phash := session.FS.root.hash + n := session.FS.lookup[node.hash] + if n.parent.hash != phash { + t.Error("Parent of uploaded file mismatch") + } + session.FS.mutex.Unlock() + + err := session.DownloadFile(node, name, nil) + if err != nil { + t.Fatal("Download failed", err) + } + + h2 := fileMD5(t, name) + err = os.Remove(name) + if err != nil { + t.Error("Failed to remove file", err) + } + + if h1 != h2 { + t.Error("MD5 mismatch for downloaded file") + } +} + +func TestMove(t *testing.T) { + session := initSession(t) + node, _, _ := uploadFile(t, session, 31, session.FS.root) + + hash := node.hash + phash := session.FS.trash.hash + err := session.Move(node, session.FS.trash) + if err != nil { + t.Fatal("Move failed", err) + } + + session.FS.mutex.Lock() + n := session.FS.lookup[hash] + if n.parent.hash != phash { + t.Error("Move happened to wrong parent", phash, n.parent.hash) + } + session.FS.mutex.Unlock() +} + +func TestRename(t *testing.T) { + session := initSession(t) + node, _, _ := uploadFile(t, session, 31, session.FS.root) + + err := session.Rename(node, "newname.txt") + if err != nil { + t.Fatal("Rename failed", err) + } + + session.FS.mutex.Lock() + newname := session.FS.lookup[node.hash].name + if newname != "newname.txt" { + t.Error("Renamed to wrong name", newname) + } + session.FS.mutex.Unlock() +} + +func TestDelete(t *testing.T) { + session := initSession(t) + node, _, _ := uploadFile(t, session, 31, session.FS.root) + + retry(t, "Soft delete", func() error { + return session.Delete(node, false) + }) + + session.FS.mutex.Lock() + node = session.FS.lookup[node.hash] + if node.parent != session.FS.trash { + t.Error("Expects file to be moved to trash") + } + session.FS.mutex.Unlock() + + retry(t, "Hard delete", func() error { + return session.Delete(node, true) + }) + + time.Sleep(1 * time.Second) // wait for the event + + session.FS.mutex.Lock() + if _, ok := session.FS.lookup[node.hash]; ok { + t.Error("Expects file to be dissapeared") + } + session.FS.mutex.Unlock() +} + +func TestCreateDir(t *testing.T) { + session := initSession(t) + node := createDir(t, session, "testdir1", session.FS.root) + node2 := createDir(t, session, "testdir2", node) + + session.FS.mutex.Lock() + nnode2 := session.FS.lookup[node2.hash] + if nnode2.parent.hash != node.hash { + t.Error("Wrong directory parent") + } + session.FS.mutex.Unlock() +} + +func TestConfig(t *testing.T) { + m := New() + m.SetAPIUrl("http://invalid.domain") + err := m.Login(USER, PASSWORD) + if err == nil { + t.Error("API Url: Expected failure") + } + + err = m.SetDownloadWorkers(100) + if err != EWORKER_LIMIT_EXCEEDED { + t.Error("Download: Expected EWORKER_LIMIT_EXCEEDED error") + } + + err = m.SetUploadWorkers(100) + if err != EWORKER_LIMIT_EXCEEDED { + t.Error("Upload: Expected EWORKER_LIMIT_EXCEEDED error") + } + + // TODO: Add timeout test cases + +} + +func TestPathLookup(t *testing.T) { + session := initSession(t) + + rs, err := randString(5) + if err != nil { + t.Fatalf("failed to make random string: %v", err) + } + node1 := createDir(t, session, "dir-1-"+rs, session.FS.root) + node21 := createDir(t, session, "dir-2-1-"+rs, node1) + node22 := createDir(t, session, "dir-2-2-"+rs, node1) + node31 := createDir(t, session, "dir-3-1-"+rs, node21) + node32 := createDir(t, session, "dir-3-2-"+rs, node22) + _ = node32 + + _, name1, _ := uploadFile(t, session, 31, node31) + _, _, _ = uploadFile(t, session, 31, node31) + _, name3, _ := uploadFile(t, session, 31, node22) + + testpaths := [][]string{ + {"dir-1-" + rs, "dir-2-2-" + rs, path.Base(name3)}, + {"dir-1-" + rs, "dir-2-1-" + rs, "dir-3-1-" + rs}, + {"dir-1-" + rs, "dir-2-1-" + rs, "dir-3-1-" + rs, path.Base(name1)}, + {"dir-1-" + rs, "dir-2-1-" + rs, "none"}, + } + + results := []error{nil, nil, nil, ENOENT} + + for i, tst := range testpaths { + ns, e := session.FS.PathLookup(session.FS.root, tst) + switch { + case e != results[i]: + t.Errorf("Test %d failed: wrong result", i) + default: + if results[i] == nil && len(tst) != len(ns) { + t.Errorf("Test %d failed: result array len (%d) mismatch", i, len(ns)) + + } + + arr := []string{} + for n := range ns { + if tst[n] != ns[n].name { + t.Errorf("Test %d failed: result node mismatches (%v) and (%v)", i, tst, arr) + break + } + arr = append(arr, tst[n]) + } + } + } +} + +func TestEventNotify(t *testing.T) { + session1 := initSession(t) + session2 := initSession(t) + + node, _, _ := uploadFile(t, session1, 31, session1.FS.root) + + for i := 0; i < 60; i++ { + time.Sleep(time.Second * 1) + node = session2.FS.HashLookup(node.GetHash()) + if node != nil { + break + } + } + + if node == nil { + t.Fatal("Expects file to found in second client's FS") + } + + retry(t, "Delete", func() error { + return session2.Delete(node, true) + }) + + time.Sleep(time.Second * 5) + node = session1.FS.HashLookup(node.hash) + if node != nil { + t.Fatal("Expects file to not-found in first client's FS") + } +} + +func TestExportLink(t *testing.T) { + session := initSession(t) + node, _, _ := uploadFile(t, session, 31, session.FS.root) + + // Don't include decryption key + retry(t, "Failed to export link (key not included)", func() error { + _, err := session.Link(node, false) + return err + }) + + // Do include decryption key + retry(t, "Failed to export link (key included)", func() error { + _, err := session.Link(node, true) + return err + }) +} diff --git a/vendor/github.com/t3rm1n4l/go-mega/messages.go b/vendor/github.com/t3rm1n4l/go-mega/messages.go new file mode 100644 index 000000000..da3639f15 --- /dev/null +++ b/vendor/github.com/t3rm1n4l/go-mega/messages.go @@ -0,0 +1,200 @@ +package mega + +import "encoding/json" + +type LoginMsg struct { + Cmd string `json:"a"` + User string `json:"user"` + Handle string `json:"uh"` +} + +type LoginResp struct { + Csid string `json:"csid"` + Privk string `json:"privk"` + Key string `json:"k"` +} + +type UserMsg struct { + Cmd string `json:"a"` +} + +type UserResp struct { + U string `json:"u"` + S int `json:"s"` + Email string `json:"email"` + Name string `json:"name"` + Key string `json:"k"` + C int `json:"c"` + Pubk string `json:"pubk"` + Privk string `json:"privk"` + Terms string `json:"terms"` + TS string `json:"ts"` +} + +type QuotaMsg struct { + // Action, should be "uq" for quota request + Cmd string `json:"a"` + // xfer should be 1 + Xfer int `json:"xfer"` + // Without strg=1 only reports total capacity for account + Strg int `json:"strg,omitempty"` +} + +type QuotaResp struct { + // Mstrg is total capacity in bytes + Mstrg uint64 `json:"mstrg"` + // Cstrg is used capacity in bytes + Cstrg uint64 `json:"cstrg"` + // Per folder usage in bytes? + Cstrgn map[string][]int64 `json:"cstrgn"` +} + +type FilesMsg struct { + Cmd string `json:"a"` + C int `json:"c"` +} + +type FSNode struct { + Hash string `json:"h"` + Parent string `json:"p"` + User string `json:"u"` + T int `json:"t"` + Attr string `json:"a"` + Key string `json:"k"` + Ts int64 `json:"ts"` + SUser string `json:"su"` + SKey string `json:"sk"` + Sz int64 `json:"s"` +} + +type FilesResp struct { + F []FSNode `json:"f"` + + Ok []struct { + Hash string `json:"h"` + Key string `json:"k"` + } `json:"ok"` + + S []struct { + Hash string `json:"h"` + User string `json:"u"` + } `json:"s"` + User []struct { + User string `json:"u"` + C int `json:"c"` + Email string `json:"m"` + } `json:"u"` + Sn string `json:"sn"` +} + +type FileAttr struct { + Name string `json:"n"` +} + +type GetLinkMsg struct { + Cmd string `json:"a"` + N string `json:"n"` +} + +type DownloadMsg struct { + Cmd string `json:"a"` + G int `json:"g"` + P string `json:"p,omitempty"` + N string `json:"n,omitempty"` +} + +type DownloadResp struct { + G string `json:"g"` + Size uint64 `json:"s"` + Attr string `json:"at"` + Err uint32 `json:"e"` +} + +type UploadMsg struct { + Cmd string `json:"a"` + S int64 `json:"s"` +} + +type UploadResp struct { + P string `json:"p"` +} + +type UploadCompleteMsg struct { + Cmd string `json:"a"` + T string `json:"t"` + N [1]struct { + H string `json:"h"` + T int `json:"t"` + A string `json:"a"` + K string `json:"k"` + } `json:"n"` + I string `json:"i,omitempty"` +} + +type UploadCompleteResp struct { + F []FSNode `json:"f"` +} + +type FileInfoMsg struct { + Cmd string `json:"a"` + F int `json:"f"` + P string `json:"p"` +} + +type MoveFileMsg struct { + Cmd string `json:"a"` + N string `json:"n"` + T string `json:"t"` + I string `json:"i"` +} + +type FileAttrMsg struct { + Cmd string `json:"a"` + Attr string `json:"attr"` + Key string `json:"key"` + N string `json:"n"` + I string `json:"i"` +} + +type FileDeleteMsg struct { + Cmd string `json:"a"` + N string `json:"n"` + I string `json:"i"` +} + +// GenericEvent is a generic event for parsing the Cmd type before +// decoding more specifically +type GenericEvent struct { + Cmd string `json:"a"` +} + +// FSEvent - event for various file system events +// +// Delete (a=d) +// Update attr (a=u) +// New nodes (a=t) +type FSEvent struct { + Cmd string `json:"a"` + + T struct { + Files []FSNode `json:"f"` + } `json:"t"` + Owner string `json:"ou"` + + N string `json:"n"` + User string `json:"u"` + Attr string `json:"at"` + Key string `json:"k"` + Ts int64 `json:"ts"` + I string `json:"i"` +} + +// Events is received from a poll of the server to read the events +// +// Each event can be an error message or a different field so we delay +// decoding +type Events struct { + W string `json:"w"` + Sn string `json:"sn"` + E []json.RawMessage `json:"a"` +} diff --git a/vendor/github.com/t3rm1n4l/go-mega/utils.go b/vendor/github.com/t3rm1n4l/go-mega/utils.go new file mode 100644 index 000000000..810acd52e --- /dev/null +++ b/vendor/github.com/t3rm1n4l/go-mega/utils.go @@ -0,0 +1,329 @@ +package mega + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "encoding/json" + "errors" + "math/big" + "net" + "net/http" + "strings" + "time" +) + +func newHttpClient(timeout time.Duration) *http.Client { + // TODO: Need to test this out + // Doesn't seem to work as expected + c := &http.Client{ + Transport: &http.Transport{ + Dial: func(netw, addr string) (net.Conn, error) { + c, err := net.DialTimeout(netw, addr, timeout) + if err != nil { + return nil, err + } + return c, nil + }, + Proxy: http.ProxyFromEnvironment, + }, + } + return c +} + +// bytes_to_a32 converts the byte slice b to uint32 slice considering +// the bytes to be in big endian order. +func bytes_to_a32(b []byte) []uint32 { + length := len(b) + 3 + a := make([]uint32, length/4) + buf := bytes.NewBuffer(b) + for i, _ := range a { + _ = binary.Read(buf, binary.BigEndian, &a[i]) + } + + return a +} + +// a32_to_bytes converts the uint32 slice a to byte slice where each +// uint32 is decoded in big endian order. +func a32_to_bytes(a []uint32) []byte { + buf := new(bytes.Buffer) + buf.Grow(len(a) * 4) // To prevent reallocations in Write + for _, v := range a { + _ = binary.Write(buf, binary.BigEndian, v) + } + + return buf.Bytes() +} + +// base64urlencode encodes byte slice b using base64 url encoding. +// It removes `=` padding when necessary +func base64urlencode(b []byte) []byte { + enc := base64.URLEncoding + encSize := enc.EncodedLen(len(b)) + buf := make([]byte, encSize) + enc.Encode(buf, b) + + paddSize := 3 - len(b)%3 + if paddSize < 3 { + encSize -= paddSize + buf = buf[:encSize] + } + + return buf +} + +// base64urldecode decodes the byte slice b using base64 url decoding. +// It adds required '=' padding before decoding. +func base64urldecode(b []byte) []byte { + enc := base64.URLEncoding + padSize := 4 - len(b)%4 + + switch padSize { + case 1: + b = append(b, '=') + case 2: + b = append(b, '=', '=') + } + + decSize := enc.DecodedLen(len(b)) + buf := make([]byte, decSize) + n, _ := enc.Decode(buf, b) + return buf[:n] +} + +// base64_to_a32 converts base64 encoded byte slice b to uint32 slice. +func base64_to_a32(b []byte) []uint32 { + return bytes_to_a32(base64urldecode(b)) +} + +// a32_to_base64 converts uint32 slice to base64 encoded byte slice. +func a32_to_base64(a []uint32) []byte { + return base64urlencode(a32_to_bytes(a)) +} + +// paddnull pads byte slice b such that the size of resulting byte +// slice is a multiple of q. +func paddnull(b []byte, q int) []byte { + if rem := len(b) % q; rem != 0 { + l := q - rem + + for i := 0; i < l; i++ { + b = append(b, 0) + } + } + + return b +} + +// password_key calculates password hash from the user password. +func password_key(p string) []byte { + a := bytes_to_a32(paddnull([]byte(p), 4)) + + pkey := a32_to_bytes([]uint32{0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56}) + + n := (len(a) + 3) / 4 + + ciphers := make([]cipher.Block, n) + + for j := 0; j < len(a); j += 4 { + key := []uint32{0, 0, 0, 0} + for k := 0; k < 4; k++ { + if j+k < len(a) { + key[k] = a[k+j] + } + } + ciphers[j/4], _ = aes.NewCipher(a32_to_bytes(key)) // Uses AES in ECB mode + } + + for i := 65536; i > 0; i-- { + for j := 0; j < n; j++ { + ciphers[j].Encrypt(pkey, pkey) + } + } + + return pkey +} + +// stringhash computes generic string hash. Uses k as the key for AES +// cipher. +func stringhash(s string, k []byte) []byte { + a := bytes_to_a32(paddnull([]byte(s), 4)) + h := []uint32{0, 0, 0, 0} + for i, v := range a { + h[i&3] ^= v + } + + hb := a32_to_bytes(h) + cipher, _ := aes.NewCipher(k) + for i := 16384; i > 0; i-- { + cipher.Encrypt(hb, hb) + } + ha := bytes_to_a32(paddnull(hb, 4)) + + return a32_to_base64([]uint32{ha[0], ha[2]}) +} + +// getMPI returns the length encoded Int and the next slice. +func getMPI(b []byte) (*big.Int, []byte) { + p := new(big.Int) + plen := (uint64(b[0])*256 + uint64(b[1]) + 7) >> 3 + p.SetBytes(b[2 : plen+2]) + b = b[plen+2:] + return p, b +} + +// getRSAKey decodes the RSA Key from the byte slice b. +func getRSAKey(b []byte) (*big.Int, *big.Int, *big.Int) { + p, b := getMPI(b) + q, b := getMPI(b) + d, _ := getMPI(b) + + return p, q, d +} + +// decryptRSA decrypts message m using RSA private key (p,q,d) +func decryptRSA(m, p, q, d *big.Int) []byte { + n := new(big.Int) + r := new(big.Int) + n.Mul(p, q) + r.Exp(m, d, n) + + return r.Bytes() +} + +// blockDecrypt decrypts using the block cipher blk in ECB mode. +func blockDecrypt(blk cipher.Block, dst, src []byte) error { + + if len(src) > len(dst) || len(src)%blk.BlockSize() != 0 { + return errors.New("Block decryption failed") + } + + l := len(src) - blk.BlockSize() + + for i := 0; i <= l; i += blk.BlockSize() { + blk.Decrypt(dst[i:], src[i:]) + } + + return nil +} + +// blockEncrypt encrypts using the block cipher blk in ECB mode. +func blockEncrypt(blk cipher.Block, dst, src []byte) error { + + if len(src) > len(dst) || len(src)%blk.BlockSize() != 0 { + return errors.New("Block encryption failed") + } + + l := len(src) - blk.BlockSize() + + for i := 0; i <= l; i += blk.BlockSize() { + blk.Encrypt(dst[i:], src[i:]) + } + + return nil +} + +// decryptSeessionId decrypts the session id using the given private +// key. +func decryptSessionId(privk []byte, csid []byte, mk []byte) ([]byte, error) { + + block, _ := aes.NewCipher(mk) + pk := base64urldecode(privk) + err := blockDecrypt(block, pk, pk) + if err != nil { + return nil, err + } + + c := base64urldecode(csid) + + m, _ := getMPI(c) + + p, q, d := getRSAKey(pk) + r := decryptRSA(m, p, q, d) + + return base64urlencode(r[:43]), nil + +} + +// chunkSize describes a size and position of chunk +type chunkSize struct { + position int64 + size int +} + +func getChunkSizes(size int64) (chunks []chunkSize) { + p := int64(0) + for i := 1; size > 0; i++ { + var chunk int + if i <= 8 { + chunk = i * 131072 + } else { + chunk = 1048576 + } + if size < int64(chunk) { + chunk = int(size) + } + chunks = append(chunks, chunkSize{position: p, size: chunk}) + p += int64(chunk) + size -= int64(chunk) + } + return chunks +} + +func decryptAttr(key []byte, data []byte) (attr FileAttr, err error) { + err = EBADATTR + block, err := aes.NewCipher(key) + if err != nil { + return attr, err + } + iv := a32_to_bytes([]uint32{0, 0, 0, 0}) + mode := cipher.NewCBCDecrypter(block, iv) + buf := make([]byte, len(data)) + mode.CryptBlocks(buf, base64urldecode([]byte(data))) + + if string(buf[:4]) == "MEGA" { + str := strings.TrimRight(string(buf[4:]), "\x00") + err = json.Unmarshal([]byte(str), &attr) + } + return attr, err +} + +func encryptAttr(key []byte, attr FileAttr) (b []byte, err error) { + err = EBADATTR + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + data, err := json.Marshal(attr) + if err != nil { + return nil, err + } + attrib := []byte("MEGA") + attrib = append(attrib, data...) + attrib = paddnull(attrib, 16) + + iv := a32_to_bytes([]uint32{0, 0, 0, 0}) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(attrib, attrib) + + b = base64urlencode(attrib) + return b, nil +} + +func randString(l int) (string, error) { + encoding := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789AB" + b := make([]byte, l) + _, err := rand.Read(b) + if err != nil { + return "", err + } + enc := base64.NewEncoding(encoding) + d := make([]byte, enc.EncodedLen(len(b))) + enc.Encode(d, b) + d = d[:l] + return string(d), nil +} diff --git a/vendor/github.com/t3rm1n4l/go-mega/utils_test.go b/vendor/github.com/t3rm1n4l/go-mega/utils_test.go new file mode 100644 index 000000000..533037649 --- /dev/null +++ b/vendor/github.com/t3rm1n4l/go-mega/utils_test.go @@ -0,0 +1,105 @@ +package mega + +import ( + "reflect" + "testing" +) + +func TestGetChunkSizes(t *testing.T) { + const k = 1024 + for _, test := range []struct { + size int64 + want []chunkSize + }{ + { + size: 0, + want: []chunkSize(nil), + }, + { + size: 1, + want: []chunkSize{ + {0, 1}, + }, + }, + { + size: 128*k - 1, + want: []chunkSize{ + {0, 128*k - 1}, + }, + }, + { + size: 128 * k, + want: []chunkSize{ + {0, 128 * k}, + }, + }, + { + size: 128*k + 1, + want: []chunkSize{ + {0, 128 * k}, + {128 * k, 1}, + }, + }, + { + size: 384*k - 1, + want: []chunkSize{ + {0, 128 * k}, + {128 * k, 256*k - 1}, + }, + }, + { + size: 384 * k, + want: []chunkSize{ + {0, 128 * k}, + {128 * k, 256 * k}, + }, + }, + { + size: 384*k + 1, + want: []chunkSize{ + {0, 128 * k}, + {128 * k, 256 * k}, + {384 * k, 1}, + }, + }, + { + size: 5 * k * k, + want: []chunkSize{ + {0, 128 * k}, + {128 * k, 256 * k}, + {384 * k, 384 * k}, + {768 * k, 512 * k}, + {1280 * k, 640 * k}, + {1920 * k, 768 * k}, + {2688 * k, 896 * k}, + {3584 * k, 1024 * k}, + {4608 * k, 512 * k}, + }, + }, + { + size: 10 * k * k, + want: []chunkSize{ + {0, 128 * k}, + {128 * k, 256 * k}, + {384 * k, 384 * k}, + {768 * k, 512 * k}, + {1280 * k, 640 * k}, + {1920 * k, 768 * k}, + {2688 * k, 896 * k}, + {3584 * k, 1024 * k}, + {4608 * k, 1024 * k}, + {5632 * k, 1024 * k}, + {6656 * k, 1024 * k}, + {7680 * k, 1024 * k}, + {8704 * k, 1024 * k}, + {9728 * k, 512 * k}, + }, + }, + } { + got := getChunkSizes(test.size) + if !reflect.DeepEqual(test.want, got) { + t.Errorf("incorrect chunks for size %d: want %#v, got %#v", test.size, test.want, got) + + } + } +}