rmega 0.0.6 → 0.1.0

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.
data/README.md CHANGED
@@ -1,94 +1,147 @@
1
1
  # Rmega
2
2
 
3
- A ruby library for the Mega.co.nz.
4
- Tested using ruby 1.9.3+ (OpenSSL 0.9.8r+)
5
- This work is the result of a reverse engineering of the Mega's Javascript code.
3
+ A ruby library for MEGA ([https://mega.co.nz/](https://mega.co.nz/))
4
+ Requirements: Ruby 1.9.3+ and OpenSSL 0.9.8r+
6
5
 
7
- ## Installation
8
-
9
- Rmega is distributed via rubygems, so if you have ruby 1.9.3+ installed
10
- system wide, just type `gem install rmega`.
11
6
 
12
- ## Usage
7
+ This is the result of a reverse engineering of the MEGA javascript code.
8
+ This is a work in progress, further functionality are coming.
13
9
 
14
- $ irb -r rmega
15
10
 
16
- ### Login and retrive all the files and folders
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
17
19
 
18
- ```ruby
19
- storage = Rmega.login 'your_email', 'your_password'
20
20
 
21
- # Fetch all the nodes (files, folders, ecc.)
22
- nodes = storage.nodes
23
- ```
21
+ ## Installation
24
22
 
23
+ **Rmega** is distributed via [rubygems.org](https://rubygems.org/).
24
+ If you have ruby installed system wide, just type `gem install rmega`.
25
25
 
26
- ### Download a file or a folder
26
+ ## Usage
27
27
 
28
28
  ```ruby
29
- file = storage.nodes_by_name(/document1/i).first
30
- file.name # => "MyDocument1.pdf"
31
- file.download '~/Downloads'
32
-
33
- folder = storage.nodes_by_name(/photos/i).first
34
- folder.download '~/Downloads/MyAlbums'
29
+ require 'rmega'
35
30
  ```
36
31
 
37
-
38
- ### Download a file using a public url
32
+ ### Login
39
33
 
40
34
  ```ruby
41
- storage.download 'https://mega.co.nz/#!cER0GYbD!ZCHruEA08Xl4a_bUZYMI', '~/Downloads'
35
+ storage = Rmega.login('your@email.com', 'your_p4ssw0rd')
42
36
  ```
43
37
 
44
-
45
- ### Upload a file
38
+ ### Browsing
46
39
 
47
40
  ```ruby
48
- # Upload a file (to the root folder)
49
- storage.upload '~/Downloads/my_file.zip'
50
-
51
- # Upload a file to a specific folder
52
- document_folder = storage.nodes_by_name(/photos/i).first
53
- storage.upload '~/Downloads/my_file.zip', document_folder
41
+ # Print the name of the files in the root folder
42
+ storage.root.files.each { |file| puts file.name }
43
+
44
+ # Print the number of files in each folder
45
+ storage.root.folders.each do |folder|
46
+ puts "Folder #{folder.name} contains #{folder.files.size} files."
47
+ end
48
+
49
+ # Print the name and the size of the files in the recyble bin
50
+ storage.trash.files.each { |file| puts "#{file.name} of #{file.size} bytes" }
51
+
52
+ # Print the name of all the files (everywhere)
53
+ storage.nodes.each do |node|
54
+ next unless node.type == :file
55
+ puts node.name
56
+ end
57
+
58
+ # Print all the nodes (files, folders, etc.) within a spefic folder
59
+ folder = storage.root.folders[12]
60
+ folder.children.each do |node|
61
+ puts "Node #{node.name} (#{node.type})"
62
+ end
54
63
  ```
55
64
 
56
- ### Other operations
65
+ ### Searching
57
66
 
58
67
  ```ruby
59
- # Trash a file or a folder
60
- my_node.trash
61
-
62
- # Gets the public url (the sharable one) of a file
63
- my_node.public_url
68
+ # Search for a file within a specific folder
69
+ folder = storage.root.folders[2]
70
+ folder.files.find { |file| file.name == "to_find.txt" }
64
71
 
65
- # See the attributes of a node
66
- my_node.attributes
72
+ # Search for a file everywhere
73
+ storage.nodes.find { |node| node.type == :file and node.name =~ /my_file/i }
67
74
 
68
- # Create a folder
69
- parent_folder = storage.nodes_by_name(/photos/i).first
70
- folder_node = storage.create_folder parent_folder, "london"
75
+ # Note: A node can be of type :file, :folder, :root, :inbox and :trash
71
76
  ```
72
77
 
73
- ## Todo
78
+ ### Download
74
79
 
75
- * Handle connection errors during upload/download
76
-
77
-
78
- ## Installation
80
+ ```ruby
81
+ # Download a single file
82
+ file = storage.root.files.first
83
+ file.download("~/Downloads")
84
+ # => Download in progress 15.0MB of 15.0MB (100.0%)
85
+
86
+ # Download a folder and all its content recursively
87
+ folder = storage.nodes.find do |node|
88
+ node.type == :folder and node.name == 'my_folder'
89
+ end
90
+ folder.download("~/Downloads/my_folder")
91
+
92
+ # Download a file by url
93
+ publid_url = 'https://mega.co.nz/#!MAkg2Iab!bc9Y2U6d93IlRRKVYpcC9hLZjS4G278OPdH6nTFPDNQ'
94
+ storage.download(public_url, '~/Downloads')
95
+ ```
79
96
 
80
- Add this line to your application's Gemfile:
97
+ ### Upload
81
98
 
82
- gem 'rmega'
99
+ ```ruby
100
+ # Upload a file to a specific folder
101
+ folder = storage.root.folders[3]
102
+ folder.upload("~/Downloads/my_file.txt")
83
103
 
84
- And then execute:
104
+ # Upload a file to the root folder
105
+ storage.root.upload("~/Downloads/my_other_file.txt")
106
+ ```
85
107
 
86
- $ bundle
108
+ ### Creating a folder
87
109
 
88
- Or install it yourself as:
110
+ ```ruby
111
+ # Create a subfolder of the root folder
112
+ new_folder = storage.root.create_folder("my_documents")
113
+
114
+ # Create a subfolder of an existing folder
115
+ folder = storage.nodes.find do |node|
116
+ node.type == :folder and node.name == 'my_folder'
117
+ end
118
+ folder.create_folder("my_documents")
119
+ ```
89
120
 
90
- $ gem install rmega
121
+ ### Deleting
91
122
 
123
+ ```ruby
124
+ # Delete a folder
125
+ folder = storage.root.folders[4]
126
+ folder.delete
127
+
128
+ # Move a folder to the recyle bin
129
+ folder = storage.root.folders[4]
130
+ folder.trash
131
+
132
+ # Delete a file
133
+ file = storage.root.folders[3].files.find { |f| f.name =~ /document1/ }
134
+ file.delete
135
+
136
+ # Move a file to the recyle bin
137
+ file = storage.root.files.last
138
+ file.trash
139
+
140
+ # Empty the trash
141
+ unless storage.trash.empty?
142
+ storage.trash.empty!
143
+ end
144
+ ```
92
145
 
93
146
  ## Contributing
94
147
 
@@ -1,3 +1,5 @@
1
+ require 'openssl'
2
+
1
3
  module Rmega
2
4
  module Crypto
3
5
  module Aes
@@ -8,10 +10,10 @@ module Rmega
8
10
  end
9
11
 
10
12
  def cipher
11
- @cipher ||= OpenSSL::Cipher::AES.new 128, :CBC
13
+ @cipher ||= OpenSSL::Cipher::AES.new(128, :CBC)
12
14
  end
13
15
 
14
- def encrypt key, data
16
+ def encrypt(key, data)
15
17
  cipher.reset
16
18
  cipher.padding = 0
17
19
  cipher.encrypt
@@ -20,7 +22,7 @@ module Rmega
20
22
  result.unpack packing
21
23
  end
22
24
 
23
- def decrypt key, data
25
+ def decrypt(key, data)
24
26
  cipher.reset
25
27
  cipher.padding = 0
26
28
  cipher.decrypt
@@ -1,9 +1,12 @@
1
+ require 'rmega/utils'
2
+ require 'rmega/crypto/aes'
3
+
1
4
  module Rmega
2
5
  module Crypto
3
6
  module AesCtr
4
7
  extend self
5
8
 
6
- def decrypt key, nonce, data
9
+ def decrypt(key, nonce, data)
7
10
  raise "invalid nonce" if nonce.size != 4 or !nonce.respond_to?(:pack)
8
11
  raise "invalid key" if key.size != 4 or !key.respond_to?(:pack)
9
12
 
@@ -47,7 +50,7 @@ module Rmega
47
50
  {data: decrypted_data, mac: mac}
48
51
  end
49
52
 
50
- def encrypt key, nonce, data
53
+ def encrypt(key, nonce, data)
51
54
  raise "invalid nonce" if nonce.size != 4 or !nonce.respond_to?(:pack)
52
55
  raise "invalid key" if key.size != 4 or !key.respond_to?(:pack)
53
56
 
@@ -1,3 +1,8 @@
1
+ require 'rmega/utils'
2
+ require 'rmega/crypto/aes'
3
+ require 'rmega/crypto/aes_ctr'
4
+ require 'rmega/crypto/rsa'
5
+
1
6
  module Rmega
2
7
  module Crypto
3
8
  extend self
@@ -6,7 +11,7 @@ module Rmega
6
11
  Array.new(6).map { rand(0..0xFFFFFFFF) }
7
12
  end
8
13
 
9
- def prepare_key ary
14
+ def prepare_key(ary)
10
15
  pkey = [0x93C467E3,0x7DB0C7A4,0xD1BE3F81,0x0152CB56]
11
16
  65536.times do
12
17
  0.step(ary.size-1, 4) do |j|
@@ -20,7 +25,7 @@ module Rmega
20
25
  pkey
21
26
  end
22
27
 
23
- def decrypt_sid key, csid, privk
28
+ def decrypt_sid(key, csid, privk)
24
29
  # if csid ...
25
30
  t = Utils.mpi2b Utils.base64urldecode(csid)
26
31
  privk = Utils.a32_to_str decrypt_key(key, Utils.base64_to_a32(privk))
@@ -40,7 +45,7 @@ module Rmega
40
45
  Utils.base64urlencode Utils.b2s(decrypted_t)[0..42]
41
46
  end
42
47
 
43
- def encrypt_attributes key, attributes_hash
48
+ def encrypt_attributes(key, attributes_hash)
44
49
  a32key = key.dup
45
50
  if a32key.size > 4
46
51
  a32key = [a32key[0] ^ a32key[4], a32key[1] ^ a32key[5], a32key[2] ^ a32key[6], a32key[3] ^ a32key[7]]
@@ -50,7 +55,7 @@ module Rmega
50
55
  Crypto::Aes.encrypt a32key, Utils.str_to_a32(attributes_str)
51
56
  end
52
57
 
53
- def decrypt_attributes key, attributes_base64
58
+ def decrypt_attributes(key, attributes_base64)
54
59
  a32key = key.dup
55
60
  if a32key.size > 4
56
61
  a32key = [a32key[0] ^ a32key[4], a32key[1] ^ a32key[5], a32key[2] ^ a32key[6], a32key[3] ^ a32key[7]]
@@ -60,11 +65,11 @@ module Rmega
60
65
  JSON.parse attributes.gsub(/^MEGA/, '').rstrip
61
66
  end
62
67
 
63
- def prepare_key_pw password_str
68
+ def prepare_key_pw(password_str)
64
69
  prepare_key Utils.str_to_a32(password_str)
65
70
  end
66
71
 
67
- def stringhash aes_key, string
72
+ def stringhash(aes_key, string)
68
73
  s32 = Utils::str_to_a32 string
69
74
  h32 = [0,0,0,0]
70
75
 
@@ -74,7 +79,7 @@ module Rmega
74
79
  Utils::a32_to_base64 [h32[0],h32[2]]
75
80
  end
76
81
 
77
- def encrypt_key key, data
82
+ def encrypt_key(key, data)
78
83
  return Aes.encrypt(key, data) if data.size == 4
79
84
  x = []
80
85
  (0..data.size).step(4) do |i|
@@ -85,7 +90,7 @@ module Rmega
85
90
  x
86
91
  end
87
92
 
88
- def decrypt_key key, data
93
+ def decrypt_key(key, data)
89
94
  return Aes.decrypt(key, data) if data.size == 4
90
95
  x = []
91
96
  (0..data.size).step(4) do |i|
@@ -1,3 +1,5 @@
1
+ require 'execjs'
2
+
1
3
  module Rmega
2
4
  module Crypto
3
5
  module Rsa
@@ -11,7 +13,7 @@ module Rmega
11
13
  @context ||= ExecJS.compile File.read(script_path)
12
14
  end
13
15
 
14
- def decrypt t, privk
16
+ def decrypt(t, privk)
15
17
  context.call "RSAdecrypt", t, privk[2], privk[0], privk[1], privk[3]
16
18
  end
17
19
  end
@@ -1,3 +1,8 @@
1
+ require 'rmega/loggable'
2
+ require 'rmega/utils'
3
+ require 'rmega/pool'
4
+ require 'rmega/progress'
5
+
1
6
  module Rmega
2
7
  class Downloader
3
8
  include Loggable
@@ -5,7 +10,7 @@ module Rmega
5
10
  attr_reader :pool, :base_url, :filesize, :local_path
6
11
 
7
12
  def initialize(params)
8
- @pool = Thread.pool(params[:threads] || 5)
13
+ @pool = Pool.new(params[:threads])
9
14
  @filesize = params[:filesize]
10
15
  @base_url = params[:base_url]
11
16
  @local_path = params[:local_path]
@@ -17,7 +22,7 @@ module Rmega
17
22
  `dd if=/dev/zero of="#{local_path}" bs=1 count=0 seek=#{filesize} > /dev/null 2>&1`
18
23
  raise "Unable to create file #{local_path}" if File.size(local_path) != filesize
19
24
 
20
- File.open(local_path, 'r+b').tap { |f| f.rewind }
25
+ ::File.open(local_path, 'r+b').tap { |f| f.rewind }
21
26
  end
22
27
 
23
28
  # Downloads a part of the remote file, starting from the start-n byte
@@ -25,38 +30,30 @@ module Rmega
25
30
  def download_chunk(start, size)
26
31
  stop = start + size - 1
27
32
  url = "#{base_url}/#{start}-#{stop}"
28
- # logger.debug "#{Thread.current} downloading chunk @ #{start}"
29
33
  HTTPClient.new.get_content(url)
30
34
  end
31
35
 
32
36
  # Writes a buffer in the local file, starting from the start-n byte.
33
37
  def write_chunk(start, buffer)
34
- # logger.debug "#{Thread.current} writing chunk @ #{position}"
35
38
  @local_file.seek(start)
36
39
  @local_file.write(buffer)
37
40
  end
38
41
 
39
- # Shows the progress bar in console
40
- def show_progress(increment)
41
- Utils.show_progress(:download, filesize, increment)
42
- end
43
-
44
42
  def chunks
45
- Storage.chunks(filesize)
43
+ Utils.chunks(filesize)
46
44
  end
47
45
 
48
- # TODO: checksum check
49
46
  def download(&block)
50
47
  @local_file = allocate
51
48
 
52
- show_progress(0)
49
+ progress = Progress.new(filesize: filesize, verb: 'download')
53
50
 
54
51
  chunks.each do |start, size|
55
52
  pool.defer do
56
- buffer = download_chunk(start, size)
57
- buffer = yield(start, buffer) if block_given?
58
- show_progress(size)
59
- pool.synchronize { write_chunk(start, buffer) }
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) }
60
57
  end
61
58
  end
62
59
 
@@ -1,11 +1,18 @@
1
+ require 'logger'
2
+
1
3
  module Rmega
2
4
  module Loggable
3
5
  def logger
4
- Rmega.logger
6
+ @@logger ||= begin
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
5
12
  end
6
13
 
7
14
  def self.included(base)
8
- base.send :extend, self
15
+ base.send(:extend, self)
9
16
  end
10
17
  end
11
18
  end
@@ -0,0 +1,16 @@
1
+ require 'rmega/utils'
2
+ require 'rmega/crypto/crypto'
3
+
4
+ module Rmega
5
+ module Nodes
6
+ module Deletable
7
+ def delete
8
+ request(a: 'd', n: handle)
9
+ end
10
+
11
+ def trash
12
+ request(a: 'm', n: handle, t: storage.trash.handle)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,64 @@
1
+ require 'rmega/utils'
2
+ require 'rmega/uploader'
3
+ require 'rmega/crypto/crypto'
4
+
5
+ module Rmega
6
+ module Nodes
7
+ module Expandable
8
+ def create_folder(name)
9
+ key = Crypto.random_key
10
+ encrypted_attributes = Utils.a32_to_base64 Crypto.encrypt_attributes(key[0..3], {n: name.strip})
11
+ encrypted_key = Utils.a32_to_base64 Crypto.encrypt_key(session.master_key, key)
12
+ n = [{h: 'xxxxxxxx', t: 1, a: encrypted_attributes, k: encrypted_key}]
13
+ data = session.request a: 'p', t: parent_handle, n: n
14
+ Folder.new(session, data['f'][0])
15
+ end
16
+
17
+ def upload_url(filesize)
18
+ session.request(a: 'u', s: filesize)['p']
19
+ 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
+ end
63
+ end
64
+ end
@@ -0,0 +1,41 @@
1
+ require 'rmega/nodes/node'
2
+ require 'rmega/nodes/file'
3
+ require 'rmega/nodes/folder'
4
+ require 'rmega/nodes/inbox'
5
+ require 'rmega/nodes/root'
6
+ require 'rmega/nodes/trash'
7
+
8
+ module Rmega
9
+ module Nodes
10
+ module Factory
11
+ extend self
12
+
13
+ def build(session, data)
14
+ type_name = type(data['t'])
15
+ node_class = Nodes.const_get("#{type_name.to_s.capitalize}")
16
+ node_class.new(session, data)
17
+ end
18
+
19
+ # TODO: support other node types than File
20
+ def build_from_url(session, url)
21
+ public_handle, key = url.strip.split('!')[1, 2]
22
+ data = session.request(a: 'g', g: 1, p: public_handle)
23
+
24
+ Nodes::File.new(session, data).tap { |n| n.public_url = url }
25
+ end
26
+
27
+ def mega_url?(url)
28
+ !!(url.to_s =~ /^https:\/\/mega\.co\.nz\/#!.*$/i)
29
+ end
30
+
31
+ def type(number)
32
+ founded_type = types.find { |k, v| number == v }
33
+ founded_type.first if founded_type
34
+ end
35
+
36
+ def types
37
+ {file: 0, folder: 1, root: 2, inbox: 3, trash: 4}
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ require 'rmega/downloader'
2
+ require 'rmega/nodes/node'
3
+ require 'rmega/nodes/deletable'
4
+
5
+ module Rmega
6
+ module Nodes
7
+ class File < Node
8
+ include Deletable
9
+
10
+ def storage_url
11
+ @storage_url ||= data['g'] || request(a: 'g', g: 1, n: handle)['g']
12
+ end
13
+
14
+ def size
15
+ data['s']
16
+ end
17
+
18
+ def download(path)
19
+ path = ::File.expand_path(path)
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
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ require 'rmega/crypto/crypto'
2
+ require 'rmega/utils'
3
+ require 'rmega/nodes/node'
4
+ require 'rmega/nodes/expandable'
5
+ require 'rmega/nodes/traversable'
6
+ require 'rmega/nodes/deletable'
7
+
8
+ module Rmega
9
+ module Nodes
10
+ class Folder < Node
11
+ include Expandable
12
+ include Traversable
13
+ include Deletable
14
+
15
+ def download(path)
16
+ children.each do |node|
17
+ if node.type == :file
18
+ node.download path
19
+ elsif node.type == :folder
20
+ subfolder = ::File.expand_path ::File.join(path, node.name)
21
+ Dir.mkdir(subfolder) unless Dir.exists?(subfolder)
22
+ node.download subfolder
23
+ end
24
+ end
25
+
26
+ nil
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,8 @@
1
+ require 'rmega/nodes/node'
2
+
3
+ module Rmega
4
+ module Nodes
5
+ class Inbox < Node
6
+ end
7
+ end
8
+ end