rmega 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|