rmega 0.0.6 → 0.1.0

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