rmega 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +0 -11
- data/TODO.md +2 -2
- data/lib/rmega/crypto/aes_ctr.rb +2 -0
- data/lib/rmega/crypto/crypto.rb +8 -4
- data/lib/rmega/errors.rb +55 -0
- data/lib/rmega/loggable.rb +16 -8
- data/lib/rmega/nodes/downloadable.rb +71 -0
- data/lib/rmega/nodes/expandable.rb +3 -43
- data/lib/rmega/nodes/file.rb +4 -19
- data/lib/rmega/nodes/node.rb +47 -17
- data/lib/rmega/nodes/uploadable.rb +83 -0
- data/lib/rmega/options.rb +2 -0
- data/lib/rmega/pool.rb +35 -40
- data/lib/rmega/session.rb +27 -10
- data/lib/rmega/storage.rb +14 -1
- data/lib/rmega/version.rb +1 -1
- data/spec/integration/login_spec.rb +1 -1
- data/spec/integration/rmega_account.yml.example +2 -0
- metadata +8 -5
- data/lib/rmega/downloader.rb +0 -70
- data/lib/rmega/request_error.rb +0 -35
- data/lib/rmega/uploader.rb +0 -58
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 461e31696b324da1fdd11d8164b9284f9d0ead32
|
4
|
+
data.tar.gz: 6edaa46bdd45cee1447af2bd569bdd812fddb207
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c38cbd4d4510fa270bf269f57fe3d6fef4730ed421065ffee8d0762ecbd8d47acd2a84311f699193635b7cb6a1bc31c5519778cd1c2f4ac3c9779035ebfe2dd
|
7
|
+
data.tar.gz: 1d731406b6c5096f9cb12240ce3af9016b2fd5d728e2594af69156d559c557f7581b0d6c08b566b6a4a98c206bba6dbcd3c1fb40b4c6fcd7c7cd8ddd39a9fd1e
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
## 0.1.6
|
2
|
+
|
3
|
+
### New Features
|
4
|
+
|
5
|
+
* \#7 Rmega now supports shared folders and files.
|
6
|
+
|
7
|
+
* The method `Storage#folders` can now be used to get a list of your
|
8
|
+
first-level folders + first-level shared folders.
|
9
|
+
|
10
|
+
* The error codes list is now updated (up to the number -22).
|
11
|
+
|
12
|
+
|
13
|
+
### Changes
|
14
|
+
|
15
|
+
* Rmega will now attempt a request up to 10 times (in case of SocketError
|
16
|
+
or if the server is temporary busy) before raising the error.
|
17
|
+
|
18
|
+
* Fixed a race condition that cause corrupted downloads
|
data/README.md
CHANGED
@@ -5,17 +5,6 @@ Requirements: Ruby 1.9.3+ and OpenSSL 0.9.8r+
|
|
5
5
|
|
6
6
|
|
7
7
|
This is the result of a reverse engineering of the MEGA javascript code.
|
8
|
-
Work in progress, further functionality are coming.
|
9
|
-
|
10
|
-
|
11
|
-
Supported features are:
|
12
|
-
* Login
|
13
|
-
* Searching and browsing
|
14
|
-
* Creating folders
|
15
|
-
* Download of files and folders (multi-thread)
|
16
|
-
* Download with public links (multi-thread)
|
17
|
-
* Upload of files (multi-thread)
|
18
|
-
* Deleting and trashing
|
19
8
|
|
20
9
|
|
21
10
|
## Installation
|
data/TODO.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
##
|
1
|
+
## TODO
|
2
2
|
|
3
3
|
* Handle timeouts in download/upload
|
4
4
|
* Handle error code -3 response, that is "A temporary congestion or server malfunction prevented your request from being processed.". I noticed that this occurs quite often when creating/deleting a folder node :/
|
5
5
|
* Sometimes uploaded data is corrupted
|
6
6
|
* Search for TODO in the project for other minor tasks
|
7
|
-
*
|
7
|
+
* Refactor the pool class
|
data/lib/rmega/crypto/aes_ctr.rb
CHANGED
@@ -10,6 +10,8 @@ module Rmega
|
|
10
10
|
raise "invalid nonce" if nonce.size != 4 or !nonce.respond_to?(:pack)
|
11
11
|
raise "invalid key" if key.size != 4 or !key.respond_to?(:pack)
|
12
12
|
|
13
|
+
nonce = nonce.dup
|
14
|
+
|
13
15
|
mac = [nonce[0], nonce[1], nonce[0], nonce[1]]
|
14
16
|
enc = nil
|
15
17
|
a32 = Utils.str_to_a32 data
|
data/lib/rmega/crypto/crypto.rb
CHANGED
@@ -25,12 +25,9 @@ module Rmega
|
|
25
25
|
pkey
|
26
26
|
end
|
27
27
|
|
28
|
-
def
|
29
|
-
# if csid ...
|
30
|
-
t = Utils.mpi2b Utils.base64urldecode(csid)
|
28
|
+
def decrypt_rsa_privk(key, privk)
|
31
29
|
privk = Utils.a32_to_str decrypt_key(key, Utils.base64_to_a32(privk))
|
32
30
|
rsa_privk = Array.new 4
|
33
|
-
# else if tsid (todo)
|
34
31
|
|
35
32
|
# Decompose private key
|
36
33
|
4.times do |i|
|
@@ -39,6 +36,13 @@ module Rmega
|
|
39
36
|
privk = privk[l..-1]
|
40
37
|
end
|
41
38
|
|
39
|
+
rsa_privk
|
40
|
+
end
|
41
|
+
|
42
|
+
def decrypt_sid(rsa_privk, csid)
|
43
|
+
# if csid ...
|
44
|
+
t = Utils.mpi2b Utils.base64urldecode(csid)
|
45
|
+
|
42
46
|
# TODO - remove execjs and build the key using the ruby lib
|
43
47
|
# rsa_key = Crypto::Rsa.build_rsa_key rsa_privk
|
44
48
|
decrypted_t = Rsa.decrypt t, rsa_privk
|
data/lib/rmega/errors.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rmega
|
2
|
+
module Errors
|
3
|
+
|
4
|
+
# Check out the error codes list at https://mega.co.nz/#doc (section 11)
|
5
|
+
CODES = {
|
6
|
+
-1 => 'An internal error has occurred. Please submit a bug report, detailing the exact circumstances in which this error occurred.',
|
7
|
+
-2 => 'You have passed invalid arguments to this command.',
|
8
|
+
-3 => 'A temporary congestion or server malfunction prevented your request from being processed. No data was altered. Retry. Retries must be spaced with exponential backoff.',
|
9
|
+
-4 => 'You have exceeded your command weight per time quota. Please wait a few seconds, then try again (this should never happen in sane real-life applications).',
|
10
|
+
-5 => 'The upload failed. Please restart it from scratch.',
|
11
|
+
-6 => 'Too many concurrent IP addresses are accessing this upload target URL.',
|
12
|
+
-7 => 'The upload file packet is out of range or not starting and ending on a chunk boundary.',
|
13
|
+
-8 => 'The upload target URL you are trying to access has expired. Please request a fresh one.',
|
14
|
+
-9 => 'Object (typically, node or user) not found',
|
15
|
+
-10 => 'Circular linkage attempted',
|
16
|
+
-11 => 'Access violation (e.g., trying to write to a read-only share)',
|
17
|
+
-12 => 'Trying to create an object that already exists',
|
18
|
+
-13 => 'Trying to access an incomplete resource',
|
19
|
+
-14 => 'A decryption operation failed (never returned by the API)',
|
20
|
+
-15 => 'Invalid or expired user session, please relogin',
|
21
|
+
-16 => 'User blocked',
|
22
|
+
-17 => 'Request over quota',
|
23
|
+
-18 => 'Resource temporarily not available, please try again later',
|
24
|
+
-19 => 'Too many connections on this resource',
|
25
|
+
-20 => 'Write failed',
|
26
|
+
-21 => 'Read failed',
|
27
|
+
-22 => 'Invalid application key; request not processed',
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
class ServerError < StandardError
|
31
|
+
attr_reader :code, :message
|
32
|
+
attr_accessor :temporary
|
33
|
+
|
34
|
+
def initialize(value, opts = {})
|
35
|
+
if msg = Errors::CODES[value.to_i]
|
36
|
+
@code = value.to_i
|
37
|
+
@message = msg || "Error code #{@code}"
|
38
|
+
else
|
39
|
+
@message = value
|
40
|
+
end
|
41
|
+
|
42
|
+
self.temporary = opts[:temporary] || false
|
43
|
+
|
44
|
+
super(@message)
|
45
|
+
end
|
46
|
+
|
47
|
+
def temporary?
|
48
|
+
self.temporary || [-3, -18, -6].include?(@code)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Backward compatibility
|
54
|
+
RequestError = Errors::ServerError
|
55
|
+
end
|
data/lib/rmega/loggable.rb
CHANGED
@@ -1,18 +1,26 @@
|
|
1
1
|
require 'logger'
|
2
|
+
require 'active_support/concern'
|
2
3
|
|
3
4
|
module Rmega
|
5
|
+
def self.logger
|
6
|
+
@logger ||= begin
|
7
|
+
logger = Logger.new($stdout)
|
8
|
+
logger.level = Logger::ERROR
|
9
|
+
logger
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
4
13
|
module Loggable
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
5
16
|
def logger
|
6
|
-
|
7
|
-
Logger.new($stdout).tap do |l|
|
8
|
-
l.formatter = Proc.new { | severity, time, progname, msg| "#{msg}\n" }
|
9
|
-
l.level = Logger::ERROR
|
10
|
-
end
|
11
|
-
end
|
17
|
+
Rmega.logger
|
12
18
|
end
|
13
19
|
|
14
|
-
|
15
|
-
|
20
|
+
module ClassMethods
|
21
|
+
def logger
|
22
|
+
Rmega.logger
|
23
|
+
end
|
16
24
|
end
|
17
25
|
end
|
18
26
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'rmega/pool'
|
2
|
+
require 'rmega/utils'
|
3
|
+
|
4
|
+
module Rmega
|
5
|
+
module Nodes
|
6
|
+
module Downloadable
|
7
|
+
|
8
|
+
# Creates the local file allocating filesize-n bytes (of /dev/zero) for it.
|
9
|
+
# Opens the local file to start writing from the beginning of it.
|
10
|
+
def allocate(path)
|
11
|
+
`dd if=/dev/zero of="#{path}" bs=1 count=0 seek=#{filesize} > /dev/null 2>&1`
|
12
|
+
raise "Unable to create file #{path}" if ::File.size(path) != filesize
|
13
|
+
|
14
|
+
::File.open(path, 'r+b').tap { |f| f.rewind }
|
15
|
+
end
|
16
|
+
|
17
|
+
# Downloads a part of the remote file, starting from the start-n byte
|
18
|
+
# and ending after size-n bytes.
|
19
|
+
def download_chunk(start, size)
|
20
|
+
stop = start + size - 1
|
21
|
+
url = "#{storage_url}/#{start}-#{stop}"
|
22
|
+
HTTPClient.new.get_content(url)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Writes a buffer in the local file, starting from the start-n byte.
|
26
|
+
def write_chunk(file, start, buffer)
|
27
|
+
file.seek(start)
|
28
|
+
file.write(buffer)
|
29
|
+
end
|
30
|
+
|
31
|
+
def decrypt_chunk(start, encrypted_buffer)
|
32
|
+
k = decrypted_file_key
|
33
|
+
nonce = [k[4], k[5], (start/0x1000000000) >> 0, (start/0x10) >> 0]
|
34
|
+
decrypt_key = [k[0] ^ k[4], k[1] ^ k[5], k[2] ^ k[6], k[3] ^ k[7]]
|
35
|
+
Crypto::AesCtr.decrypt(decrypt_key, nonce, encrypted_buffer)[:data]
|
36
|
+
end
|
37
|
+
|
38
|
+
def download(path)
|
39
|
+
path = ::File.expand_path(path)
|
40
|
+
path = Dir.exists?(path) ? ::File.join(path, name) : path
|
41
|
+
|
42
|
+
logger.info "Download #{name} (#{filesize} bytes) => #{path}"
|
43
|
+
|
44
|
+
pool = Pool.new
|
45
|
+
write_mutex = Mutex.new
|
46
|
+
file = allocate(path)
|
47
|
+
|
48
|
+
progress = Progress.new(total: filesize, caption: 'Download')
|
49
|
+
|
50
|
+
Utils.chunks(filesize).each do |start, size|
|
51
|
+
pool.defer do
|
52
|
+
encrypted_buffer = download_chunk(start, size)
|
53
|
+
|
54
|
+
write_mutex.synchronize do
|
55
|
+
clean_buffer = decrypt_chunk(start, encrypted_buffer)
|
56
|
+
progress.increment(size)
|
57
|
+
write_chunk(file, start, clean_buffer)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# waits for the last running threads to finish
|
63
|
+
pool.wait_done
|
64
|
+
|
65
|
+
file.flush
|
66
|
+
ensure
|
67
|
+
file.close rescue nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -1,10 +1,12 @@
|
|
1
1
|
require 'rmega/utils'
|
2
|
-
require 'rmega/
|
2
|
+
require 'rmega/nodes/uploadable'
|
3
3
|
require 'rmega/crypto/crypto'
|
4
4
|
|
5
5
|
module Rmega
|
6
6
|
module Nodes
|
7
7
|
module Expandable
|
8
|
+
include Uploadable
|
9
|
+
|
8
10
|
def create_folder(name)
|
9
11
|
key = Crypto.random_key
|
10
12
|
encrypted_attributes = Utils.a32_to_base64 Crypto.encrypt_attributes(key[0..3], {n: name.strip})
|
@@ -17,48 +19,6 @@ module Rmega
|
|
17
19
|
def upload_url(filesize)
|
18
20
|
session.request(a: 'u', s: filesize)['p']
|
19
21
|
end
|
20
|
-
|
21
|
-
def upload(local_path)
|
22
|
-
local_path = ::File.expand_path(local_path)
|
23
|
-
filesize = ::File.size(local_path)
|
24
|
-
|
25
|
-
ul_key = Crypto.random_key
|
26
|
-
aes_key = ul_key[0..3]
|
27
|
-
nonce = ul_key[4..5]
|
28
|
-
|
29
|
-
file_mac = [0, 0, 0, 0]
|
30
|
-
|
31
|
-
uploader = Uploader.new(filesize: filesize, base_url: upload_url(filesize), local_path: local_path)
|
32
|
-
|
33
|
-
uploader.upload do |start, clean_buffer|
|
34
|
-
# TODO: should be (chunk_start/0x1000000000) >>> 0, (chunk_start/0x10) >>> 0
|
35
|
-
nonce = [nonce[0], nonce[1], (start/0x1000000000) >> 0, (start/0x10) >> 0]
|
36
|
-
|
37
|
-
encrypted = Crypto::AesCtr.encrypt(aes_key, nonce, clean_buffer)
|
38
|
-
chunk_mac = encrypted[:mac]
|
39
|
-
|
40
|
-
file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1],
|
41
|
-
file_mac[2] ^ chunk_mac[2], file_mac[3] ^ chunk_mac[3]]
|
42
|
-
file_mac = Crypto::Aes.encrypt(ul_key[0..3], file_mac)
|
43
|
-
|
44
|
-
encrypted[:data]
|
45
|
-
end
|
46
|
-
|
47
|
-
file_handle = uploader.last_result
|
48
|
-
|
49
|
-
attribs = {n: ::File.basename(local_path)}
|
50
|
-
encrypt_attribs = Utils.a32_to_base64 Crypto.encrypt_attributes(ul_key[0..3], attribs)
|
51
|
-
|
52
|
-
meta_mac = [file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]]
|
53
|
-
|
54
|
-
key = [ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5], ul_key[2] ^ meta_mac[0],
|
55
|
-
ul_key[3] ^ meta_mac[1], ul_key[4], ul_key[5], meta_mac[0], meta_mac[1]]
|
56
|
-
|
57
|
-
encrypted_key = Utils.a32_to_base64 Crypto.encrypt_key(session.master_key, key)
|
58
|
-
session.request a: 'p', t: handle, n: [{h: file_handle, t: 0, a: encrypt_attribs, k: encrypted_key}]
|
59
|
-
|
60
|
-
attribs[:n]
|
61
|
-
end
|
62
22
|
end
|
63
23
|
end
|
64
24
|
end
|
data/lib/rmega/nodes/file.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
-
require 'rmega/downloader'
|
2
1
|
require 'rmega/nodes/node'
|
3
2
|
require 'rmega/nodes/deletable'
|
3
|
+
require 'rmega/nodes/downloadable'
|
4
4
|
|
5
5
|
module Rmega
|
6
6
|
module Nodes
|
7
7
|
class File < Node
|
8
8
|
include Deletable
|
9
|
+
include Downloadable
|
9
10
|
|
10
11
|
def storage_url
|
11
12
|
@storage_url ||= data['g'] || request(a: 'g', g: 1, n: handle)['g']
|
@@ -15,24 +16,8 @@ module Rmega
|
|
15
16
|
data['s']
|
16
17
|
end
|
17
18
|
|
18
|
-
def
|
19
|
-
|
20
|
-
path = Dir.exists?(path) ? ::File.join(path, name) : path
|
21
|
-
|
22
|
-
logger.info "Download #{name} (#{size} bytes) => #{path}"
|
23
|
-
|
24
|
-
k = decrypted_file_key
|
25
|
-
k = [k[0] ^ k[4], k[1] ^ k[5], k[2] ^ k[6], k[3] ^ k[7]]
|
26
|
-
nonce = decrypted_file_key[4..5]
|
27
|
-
|
28
|
-
donwloader = Downloader.new(base_url: storage_url, filesize: size, local_path: path)
|
29
|
-
|
30
|
-
donwloader.download do |start, buffer|
|
31
|
-
nonce = [nonce[0], nonce[1], (start/0x1000000000) >> 0, (start/0x10) >> 0]
|
32
|
-
Crypto::AesCtr.decrypt(k, nonce, buffer)[:data]
|
33
|
-
end
|
34
|
-
|
35
|
-
path
|
19
|
+
def filesize
|
20
|
+
size
|
36
21
|
end
|
37
22
|
end
|
38
23
|
end
|
data/lib/rmega/nodes/node.rb
CHANGED
@@ -11,7 +11,7 @@ module Rmega
|
|
11
11
|
|
12
12
|
attr_reader :data, :session
|
13
13
|
|
14
|
-
delegate :storage, :request, :to => :session
|
14
|
+
delegate :storage, :request, :shared_keys, :rsa_privk, :to => :session
|
15
15
|
|
16
16
|
def initialize(session, data)
|
17
17
|
@session = session
|
@@ -20,7 +20,7 @@ module Rmega
|
|
20
20
|
|
21
21
|
def public_url
|
22
22
|
@public_url ||= begin
|
23
|
-
b64_dec_key = Utils.a32_to_base64
|
23
|
+
b64_dec_key = Utils.a32_to_base64(decrypted_file_key[0..7])
|
24
24
|
"https://mega.co.nz/#!#{public_handle}!#{b64_dec_key}"
|
25
25
|
end
|
26
26
|
end
|
@@ -41,35 +41,65 @@ module Rmega
|
|
41
41
|
data['p']
|
42
42
|
end
|
43
43
|
|
44
|
-
def
|
45
|
-
|
44
|
+
def name
|
45
|
+
attributes['n'] if attributes
|
46
46
|
end
|
47
47
|
|
48
|
-
def
|
49
|
-
return
|
48
|
+
def file_keys
|
49
|
+
return {} unless data['k']
|
50
|
+
|
51
|
+
pairs = data['k'].split('/')
|
52
|
+
pairs.inject({}) do |hash, pair|
|
53
|
+
h, k = pair.split(':')
|
54
|
+
hash[h] = k
|
55
|
+
hash
|
56
|
+
end
|
50
57
|
end
|
51
58
|
|
52
59
|
def file_key
|
53
|
-
|
60
|
+
file_keys.values.first
|
54
61
|
end
|
55
62
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
63
|
+
def shared_root?
|
64
|
+
data['su'] && data['sk'] && data['k']
|
65
|
+
end
|
66
|
+
|
67
|
+
def process_shared_key
|
68
|
+
h = (shared_keys.keys & file_keys.keys).first
|
69
|
+
return [h, shared_keys[h]] if h
|
70
|
+
|
71
|
+
sk = data['sk']
|
72
|
+
|
73
|
+
return unless sk
|
74
|
+
|
75
|
+
shared_key = if sk.size > 22
|
76
|
+
sk = Rmega::Utils.mpi2b(Rmega::Utils.base64urldecode(sk))
|
77
|
+
dec_sk = Rmega::Crypto::Rsa.decrypt(sk, rsa_privk)
|
78
|
+
Utils.str_to_a32(Rmega::Utils.b2s(dec_sk)[0..15])
|
59
79
|
else
|
60
|
-
Utils.base64_to_a32
|
80
|
+
Crypto.decrypt_key session.master_key, Utils.base64_to_a32(data['sk'])
|
61
81
|
end
|
82
|
+
|
83
|
+
shared_keys[handle] = shared_key
|
84
|
+
[handle, shared_key]
|
62
85
|
end
|
63
86
|
|
64
|
-
def
|
65
|
-
|
87
|
+
def decrypted_file_key
|
88
|
+
h, shared_key = *process_shared_key
|
89
|
+
|
90
|
+
if shared_key
|
91
|
+
Crypto.decrypt_key(shared_key, Utils.base64_to_a32(file_keys[h]))
|
92
|
+
elsif file_key
|
93
|
+
Crypto.decrypt_key(session.master_key, Utils.base64_to_a32(file_key))
|
94
|
+
else
|
95
|
+
Utils.base64_to_a32(public_url.split('!').last)
|
96
|
+
end
|
66
97
|
end
|
67
98
|
|
68
99
|
def attributes
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
100
|
+
encrypted = data['a'] || data['at']
|
101
|
+
return if !encrypted or encrypted.empty?
|
102
|
+
Crypto.decrypt_attributes(decrypted_file_key, encrypted)
|
73
103
|
end
|
74
104
|
|
75
105
|
def type
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'rmega/utils'
|
2
|
+
require 'rmega/pool'
|
3
|
+
require 'rmega/progress'
|
4
|
+
|
5
|
+
module Rmega
|
6
|
+
module Nodes
|
7
|
+
module Uploadable
|
8
|
+
def upload_chunk(base_url, start, buffer)
|
9
|
+
size = buffer.length
|
10
|
+
stop = start + size - 1
|
11
|
+
url = "#{base_url}/#{start}-#{stop}"
|
12
|
+
|
13
|
+
HTTPClient.new.post(url, buffer).body
|
14
|
+
end
|
15
|
+
|
16
|
+
def read_chunk(file, start, size)
|
17
|
+
file.seek(start)
|
18
|
+
file.read(size)
|
19
|
+
end
|
20
|
+
|
21
|
+
def encrypt_chunck(rnd_key, file_mac, start, clean_buffer)
|
22
|
+
nonce = [rnd_key[4], rnd_key[5], (start/0x1000000000) >> 0, (start/0x10) >> 0]
|
23
|
+
|
24
|
+
encrypted = Crypto::AesCtr.encrypt(rnd_key[0..3], nonce, clean_buffer)
|
25
|
+
chunk_mac, data = encrypted[:mac], encrypted[:data]
|
26
|
+
|
27
|
+
file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1],
|
28
|
+
file_mac[2] ^ chunk_mac[2], file_mac[3] ^ chunk_mac[3]]
|
29
|
+
|
30
|
+
file_mac = Crypto::Aes.encrypt(rnd_key[0..3], file_mac)
|
31
|
+
|
32
|
+
data
|
33
|
+
end
|
34
|
+
|
35
|
+
def upload(path)
|
36
|
+
path = ::File.expand_path(path)
|
37
|
+
filesize = ::File.size(path)
|
38
|
+
file = ::File.open(path, 'rb')
|
39
|
+
|
40
|
+
ul_key = Crypto.random_key
|
41
|
+
file_mac = [0, 0, 0, 0]
|
42
|
+
file_handle = nil
|
43
|
+
base_url = upload_url(filesize)
|
44
|
+
|
45
|
+
pool = Pool.new
|
46
|
+
read_mutex = Mutex.new
|
47
|
+
|
48
|
+
progress = Progress.new(total: filesize, caption: 'Upload')
|
49
|
+
|
50
|
+
Utils.chunks(filesize).each do |start, size|
|
51
|
+
pool.defer do
|
52
|
+
encrypted_buffer = nil
|
53
|
+
|
54
|
+
read_mutex.synchronize do
|
55
|
+
clean_buffer = read_chunk(file, start, size)
|
56
|
+
encrypted_buffer = encrypt_chunck(ul_key, file_mac, start, clean_buffer)
|
57
|
+
end
|
58
|
+
|
59
|
+
file_handle = upload_chunk(base_url, start, encrypted_buffer)
|
60
|
+
progress.increment(size)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
pool.wait_done
|
65
|
+
|
66
|
+
attribs = {n: ::File.basename(path)}
|
67
|
+
encrypt_attribs = Utils.a32_to_base64(Crypto.encrypt_attributes(ul_key[0..3], attribs))
|
68
|
+
|
69
|
+
meta_mac = [file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]]
|
70
|
+
|
71
|
+
key = [ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5], ul_key[2] ^ meta_mac[0],
|
72
|
+
ul_key[3] ^ meta_mac[1], ul_key[4], ul_key[5], meta_mac[0], meta_mac[1]]
|
73
|
+
|
74
|
+
encrypted_key = Utils.a32_to_base64 Crypto.encrypt_key(session.master_key, key)
|
75
|
+
request(a: 'p', t: handle, n: [{h: file_handle, t: 0, a: encrypt_attribs, k: encrypted_key}])
|
76
|
+
|
77
|
+
attribs[:n]
|
78
|
+
ensure
|
79
|
+
file.close
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/rmega/options.rb
CHANGED
data/lib/rmega/pool.rb
CHANGED
@@ -2,63 +2,58 @@ require 'thread'
|
|
2
2
|
|
3
3
|
module Rmega
|
4
4
|
class Pool
|
5
|
-
MAX =
|
5
|
+
MAX = 4
|
6
6
|
|
7
|
-
def initialize(max)
|
8
|
-
max ||= MAX
|
7
|
+
def initialize(max = MAX)
|
9
8
|
Thread.abort_on_exception = true
|
9
|
+
|
10
10
|
@mutex = Mutex.new
|
11
|
-
@
|
12
|
-
|
11
|
+
@resource = ConditionVariable.new
|
12
|
+
@max = max || MAX
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
def available_slot
|
17
|
-
@threads.each_with_index do |thread, index|
|
18
|
-
return index if thread.nil? or !thread.alive?
|
19
|
-
end
|
20
|
-
nil
|
14
|
+
@running = []
|
15
|
+
@queue = []
|
21
16
|
end
|
22
17
|
|
23
|
-
def
|
24
|
-
@
|
18
|
+
def defer(&block)
|
19
|
+
synchronize { @queue << block }
|
20
|
+
process_queue
|
25
21
|
end
|
26
22
|
|
27
|
-
|
28
|
-
|
29
|
-
def done?
|
30
|
-
@threads.each { |thread| return false if thread and thread.alive? }
|
31
|
-
true
|
23
|
+
def wait_done
|
24
|
+
synchronize { @resource.wait(@mutex) }
|
32
25
|
end
|
33
26
|
|
34
|
-
|
35
|
-
|
36
|
-
|
27
|
+
private
|
28
|
+
|
29
|
+
def synchronize(&block)
|
30
|
+
@mutex.synchronize(&block)
|
37
31
|
end
|
38
32
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
return index if index
|
46
|
-
sleep 0.01
|
33
|
+
def process_queue
|
34
|
+
synchronize do
|
35
|
+
if @running.size < @max
|
36
|
+
proc = @queue.shift
|
37
|
+
@running << Thread.new(&thread_proc(&proc)) if proc
|
38
|
+
end
|
47
39
|
end
|
48
40
|
end
|
49
41
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
42
|
+
def done?
|
43
|
+
synchronize { @queue.empty? && @running.empty? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def signal_done
|
47
|
+
synchronize { @resource.signal }
|
54
48
|
end
|
55
49
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
50
|
+
def thread_proc(&block)
|
51
|
+
Proc.new do
|
52
|
+
block.call
|
53
|
+
@running.reject! { |thread| thread == Thread.current }
|
54
|
+
process_queue
|
55
|
+
signal_done if done?
|
56
|
+
end
|
62
57
|
end
|
63
58
|
end
|
64
59
|
end
|
data/lib/rmega/session.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'rmega/storage'
|
2
|
-
require 'rmega/
|
2
|
+
require 'rmega/errors'
|
3
3
|
require 'rmega/crypto/crypto'
|
4
4
|
require 'rmega/utils'
|
5
5
|
|
@@ -11,11 +11,12 @@ module Rmega
|
|
11
11
|
class Session
|
12
12
|
include Loggable
|
13
13
|
|
14
|
-
attr_reader :email, :request_id, :sid, :master_key
|
14
|
+
attr_reader :email, :request_id, :sid, :master_key, :shared_keys, :rsa_privk
|
15
15
|
|
16
16
|
def initialize(email, password)
|
17
17
|
@email = email
|
18
18
|
@request_id = random_request_id
|
19
|
+
@shared_keys = {}
|
19
20
|
|
20
21
|
login(password)
|
21
22
|
end
|
@@ -25,6 +26,7 @@ module Rmega
|
|
25
26
|
end
|
26
27
|
|
27
28
|
delegate :api_url, :api_request_timeout, to: :options
|
29
|
+
delegate :max_retries, :retry_interval, to: :options
|
28
30
|
|
29
31
|
def storage
|
30
32
|
@storage ||= Storage.new(self)
|
@@ -39,7 +41,8 @@ module Rmega
|
|
39
41
|
@master_key = Crypto.decrypt_key(encrypted_key, Utils.base64_to_a32(resp['k']))
|
40
42
|
|
41
43
|
# Generates the session id
|
42
|
-
@
|
44
|
+
@rsa_privk = Crypto.decrypt_rsa_privk(@master_key, resp['privk'])
|
45
|
+
@sid = Crypto.decrypt_sid(@rsa_privk, resp['csid'])
|
43
46
|
end
|
44
47
|
|
45
48
|
def random_request_id
|
@@ -52,14 +55,28 @@ module Rmega
|
|
52
55
|
end
|
53
56
|
end
|
54
57
|
|
55
|
-
def request(
|
58
|
+
def request(content, retries = max_retries)
|
56
59
|
@request_id += 1
|
57
|
-
logger.debug "POST #{request_url}
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
60
|
+
logger.debug "POST #{request_url} #{content.inspect}"
|
61
|
+
|
62
|
+
response = HTTPClient.new.post(request_url, [content].to_json, timeout: api_request_timeout)
|
63
|
+
code, body = response.code.to_i, response.body
|
64
|
+
|
65
|
+
logger.debug("#{code} #{body}")
|
66
|
+
|
67
|
+
if code == 500 && body.to_s.empty?
|
68
|
+
raise Errors::ServerError.new("Server too busy", temporary: true)
|
69
|
+
else
|
70
|
+
json = JSON.parse(body).first
|
71
|
+
raise Errors::ServerError.new(json) if json.to_s =~ /\A\-\d+\z/
|
72
|
+
json
|
73
|
+
end
|
74
|
+
rescue SocketError, Errors::ServerError => error
|
75
|
+
raise(error) if retries < 0
|
76
|
+
raise(error) if error.respond_to?(:temporary?) && !error.temporary?
|
77
|
+
retries -= 1
|
78
|
+
sleep(retry_interval)
|
79
|
+
retry
|
63
80
|
end
|
64
81
|
end
|
65
82
|
end
|
data/lib/rmega/storage.rb
CHANGED
@@ -24,7 +24,20 @@ module Rmega
|
|
24
24
|
|
25
25
|
def nodes
|
26
26
|
results = session.request(a: 'f', c: 1)['f']
|
27
|
-
|
27
|
+
|
28
|
+
results.map do |node_data|
|
29
|
+
node = Nodes::Factory.build(session, node_data)
|
30
|
+
node.process_shared_key if node.shared_root?
|
31
|
+
node
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def folders
|
36
|
+
list = nodes
|
37
|
+
root_handle = list.find { |node| node.type == :root }.handle
|
38
|
+
list.select do |node|
|
39
|
+
node.shared_root? || (node.type == :folder && node.parent_handle == root_handle)
|
40
|
+
end
|
28
41
|
end
|
29
42
|
|
30
43
|
def trash
|
data/lib/rmega/version.rb
CHANGED
@@ -12,7 +12,7 @@ describe 'Login' do
|
|
12
12
|
|
13
13
|
context 'when email and password are invalid' do
|
14
14
|
it 'raises an error' do
|
15
|
-
expect { Rmega.login('a@apple.com', 'b') }.to raise_error(Rmega::
|
15
|
+
expect { Rmega.login('a@apple.com', 'b') }.to raise_error(Rmega::Errors::ServerError)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rmega
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniele Molteni
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pry
|
@@ -102,6 +102,7 @@ extensions: []
|
|
102
102
|
extra_rdoc_files: []
|
103
103
|
files:
|
104
104
|
- .gitignore
|
105
|
+
- CHANGELOG.md
|
105
106
|
- Gemfile
|
106
107
|
- LICENSE
|
107
108
|
- README.md
|
@@ -113,9 +114,10 @@ files:
|
|
113
114
|
- lib/rmega/crypto/crypto.rb
|
114
115
|
- lib/rmega/crypto/rsa.rb
|
115
116
|
- lib/rmega/crypto/rsa_mega.js
|
116
|
-
- lib/rmega/
|
117
|
+
- lib/rmega/errors.rb
|
117
118
|
- lib/rmega/loggable.rb
|
118
119
|
- lib/rmega/nodes/deletable.rb
|
120
|
+
- lib/rmega/nodes/downloadable.rb
|
119
121
|
- lib/rmega/nodes/expandable.rb
|
120
122
|
- lib/rmega/nodes/factory.rb
|
121
123
|
- lib/rmega/nodes/file.rb
|
@@ -125,13 +127,12 @@ files:
|
|
125
127
|
- lib/rmega/nodes/root.rb
|
126
128
|
- lib/rmega/nodes/trash.rb
|
127
129
|
- lib/rmega/nodes/traversable.rb
|
130
|
+
- lib/rmega/nodes/uploadable.rb
|
128
131
|
- lib/rmega/options.rb
|
129
132
|
- lib/rmega/pool.rb
|
130
133
|
- lib/rmega/progress.rb
|
131
|
-
- lib/rmega/request_error.rb
|
132
134
|
- lib/rmega/session.rb
|
133
135
|
- lib/rmega/storage.rb
|
134
|
-
- lib/rmega/uploader.rb
|
135
136
|
- lib/rmega/utils.rb
|
136
137
|
- lib/rmega/version.rb
|
137
138
|
- rmega.gemspec
|
@@ -139,6 +140,7 @@ files:
|
|
139
140
|
- spec/integration/file_upload_spec.rb
|
140
141
|
- spec/integration/folder_operations_spec.rb
|
141
142
|
- spec/integration/login_spec.rb
|
143
|
+
- spec/integration/rmega_account.yml.example
|
142
144
|
- spec/integration_spec_helper.rb
|
143
145
|
- spec/rmega/lib/crypto/aes_spec.rb
|
144
146
|
- spec/rmega/lib/crypto/crypto_spec.rb
|
@@ -173,6 +175,7 @@ test_files:
|
|
173
175
|
- spec/integration/file_upload_spec.rb
|
174
176
|
- spec/integration/folder_operations_spec.rb
|
175
177
|
- spec/integration/login_spec.rb
|
178
|
+
- spec/integration/rmega_account.yml.example
|
176
179
|
- spec/integration_spec_helper.rb
|
177
180
|
- spec/rmega/lib/crypto/aes_spec.rb
|
178
181
|
- spec/rmega/lib/crypto/crypto_spec.rb
|
data/lib/rmega/downloader.rb
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
require 'rmega/loggable'
|
2
|
-
require 'rmega/utils'
|
3
|
-
require 'rmega/pool'
|
4
|
-
require 'rmega/progress'
|
5
|
-
|
6
|
-
module Rmega
|
7
|
-
class Downloader
|
8
|
-
include Loggable
|
9
|
-
|
10
|
-
attr_reader :pool, :base_url, :filesize, :local_path
|
11
|
-
|
12
|
-
def initialize(params)
|
13
|
-
@pool = Pool.new(params[:threads])
|
14
|
-
@filesize = params[:filesize]
|
15
|
-
@base_url = params[:base_url]
|
16
|
-
@local_path = params[:local_path]
|
17
|
-
end
|
18
|
-
|
19
|
-
# Creates the local file allocating filesize-n bytes (of /dev/zero) for it.
|
20
|
-
# Opens the local file to start writing from the beginning of it.
|
21
|
-
def allocate
|
22
|
-
`dd if=/dev/zero of="#{local_path}" bs=1 count=0 seek=#{filesize} > /dev/null 2>&1`
|
23
|
-
raise "Unable to create file #{local_path}" if File.size(local_path) != filesize
|
24
|
-
|
25
|
-
::File.open(local_path, 'r+b').tap { |f| f.rewind }
|
26
|
-
end
|
27
|
-
|
28
|
-
# Downloads a part of the remote file, starting from the start-n byte
|
29
|
-
# and ending after size-n bytes.
|
30
|
-
def download_chunk(start, size)
|
31
|
-
stop = start + size - 1
|
32
|
-
url = "#{base_url}/#{start}-#{stop}"
|
33
|
-
HTTPClient.new.get_content(url)
|
34
|
-
end
|
35
|
-
|
36
|
-
# Writes a buffer in the local file, starting from the start-n byte.
|
37
|
-
def write_chunk(start, buffer)
|
38
|
-
@local_file.seek(start)
|
39
|
-
@local_file.write(buffer)
|
40
|
-
end
|
41
|
-
|
42
|
-
def chunks
|
43
|
-
Utils.chunks(filesize)
|
44
|
-
end
|
45
|
-
|
46
|
-
def download(&block)
|
47
|
-
@local_file = allocate
|
48
|
-
|
49
|
-
progress = Progress.new(total: filesize, caption: 'Download')
|
50
|
-
|
51
|
-
chunks.each do |start, size|
|
52
|
-
pool.defer do
|
53
|
-
encrypted_buffer = download_chunk(start, size)
|
54
|
-
clean_buffer = yield(start, encrypted_buffer)
|
55
|
-
progress.increment(size)
|
56
|
-
pool.synchronize { write_chunk(start, clean_buffer) }
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
# waits for the last running threads to finish
|
61
|
-
pool.wait_done
|
62
|
-
|
63
|
-
@local_file.flush
|
64
|
-
|
65
|
-
pool.shutdown
|
66
|
-
ensure
|
67
|
-
@local_file.close rescue nil
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
data/lib/rmega/request_error.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
module Rmega
|
2
|
-
class RequestError < StandardError
|
3
|
-
def initialize(error_code)
|
4
|
-
message = self.class.errors[error_code]
|
5
|
-
super("Error #{error_code}: #{message}")
|
6
|
-
end
|
7
|
-
|
8
|
-
def self.error_code?(number)
|
9
|
-
number.respond_to?(:to_i) and number.to_i < 0
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.errors
|
13
|
-
{
|
14
|
-
-1 => 'An internal error has occurred. Please submit a bug report, detailing the exact circumstances in which this error occurred.',
|
15
|
-
-2 => 'You have passed invalid arguments to this command.',
|
16
|
-
-3 => 'A temporary congestion or server malfunction prevented your request from being processed. No data was altered. Retry. Retries must be spaced with exponential backoff.',
|
17
|
-
-4 => 'You have exceeded your command weight per time quota. Please wait a few seconds, then try again (this should never happen in sane real-life applications).',
|
18
|
-
-5 => 'The upload failed. Please restart it from scratch.',
|
19
|
-
-6 => 'Too many concurrent IP addresses are accessing this upload target URL.',
|
20
|
-
-7 => 'The upload file packet is out of range or not starting and ending on a chunk boundary.',
|
21
|
-
-8 => 'The upload target URL you are trying to access has expired. Please request a fresh one.',
|
22
|
-
-9 => 'Object (typically, node or user) not found',
|
23
|
-
-10 => 'Circular linkage attempted',
|
24
|
-
-11 => 'Access violation (e.g., trying to write to a read-only share)',
|
25
|
-
-12 => 'Trying to create an object that already exists',
|
26
|
-
-13 => 'Trying to access an incomplete resource',
|
27
|
-
-14 => 'A decryption operation failed (never returned by the API)',
|
28
|
-
-15 => 'Invalid or expired user session, please relogin',
|
29
|
-
-16 => 'User blocked',
|
30
|
-
-17 => 'Request over quota',
|
31
|
-
-18 => 'Resource temporarily not available, please try again later'
|
32
|
-
}
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
data/lib/rmega/uploader.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
require 'rmega/loggable'
|
2
|
-
require 'rmega/utils'
|
3
|
-
require 'rmega/pool'
|
4
|
-
require 'rmega/progress'
|
5
|
-
|
6
|
-
module Rmega
|
7
|
-
class Uploader
|
8
|
-
include Loggable
|
9
|
-
|
10
|
-
attr_reader :pool, :base_url, :filesize, :local_path, :last_result
|
11
|
-
|
12
|
-
def initialize(params)
|
13
|
-
@pool = Pool.new(params[:threads])
|
14
|
-
@filesize = params[:filesize]
|
15
|
-
@base_url = params[:base_url]
|
16
|
-
@local_path = params[:local_path]
|
17
|
-
@last_result = nil
|
18
|
-
end
|
19
|
-
|
20
|
-
def upload_chunk(start, buffer)
|
21
|
-
size = buffer.length
|
22
|
-
stop = start + size - 1
|
23
|
-
url = "#{base_url}/#{start}-#{stop}"
|
24
|
-
|
25
|
-
HTTPClient.new.post(url, buffer).body
|
26
|
-
end
|
27
|
-
|
28
|
-
def read_chunk(start, size)
|
29
|
-
@local_file.seek(start)
|
30
|
-
@local_file.read(size)
|
31
|
-
end
|
32
|
-
|
33
|
-
def chunks
|
34
|
-
Utils.chunks(filesize)
|
35
|
-
end
|
36
|
-
|
37
|
-
def upload(&block)
|
38
|
-
@local_file = ::File.open(local_path, 'rb')
|
39
|
-
|
40
|
-
progress = Progress.new(total: filesize, caption: 'Upload')
|
41
|
-
|
42
|
-
chunks.each do |start, size|
|
43
|
-
|
44
|
-
pool.defer do
|
45
|
-
clean_buffer = pool.synchronize { read_chunk(start, size) }
|
46
|
-
encrypted_buffer = yield(start, clean_buffer)
|
47
|
-
@last_result = upload_chunk(start, encrypted_buffer)
|
48
|
-
progress.increment(clean_buffer.size)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
pool.wait_done
|
53
|
-
pool.shutdown
|
54
|
-
ensure
|
55
|
-
@local_file.close
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|