1 #! /usr/bin/env python3
5 from http
.client
import HTTPSConnection
, HTTP_PORT
6 from contextlib
import ExitStack
7 from megan
import Megan
, NodeType
, decrypt_name
, base64_decode
8 from megan
import make_request
, decode_response
, make_encryptor
, split_key
9 from megan
import bitmask
, ceildiv
10 from http
import HTTPStatus
11 from socket
import create_connection
, IPPROTO_TCP
, TCP_NODELAY
15 from errno
import ENOENT
16 from base64
import urlsafe_b64encode
17 from datacopy
import TerminalLog
, Progress
18 from io
import BytesIO
21 parser
= fuse
.ArgumentParser("https://mega.nz/#key {mnt,''}", '')
22 parser
.add_argument('-v',
23 action
='store_true', help='verbose messages')
24 [url
, name
, args
] = parser
.parse_args()
25 self
= Filesystem(name
, args
)
29 self
.megan
= Megan(url
)
30 url
= re
.sub('^[^:/]*:', '', url
, 1)
31 with
ExitStack() as cleanup
:
32 cleanup
.callback(self
.close
)
34 self
.api
= HTTPSConnection('g.api.mega.co.nz')
35 cleanup
.callback(self
.api
.close
)
37 if self
.megan
.folder
is None:
38 req
= self
.megan
.get_file_request()
39 url
= '!'.join(url
.split('!', 2)[:2])
41 [req
, headers
, body
] = self
.megan
.get_folder_request()
42 headers
= dict(headers
)
43 self
.api
.request('POST', f
'/{req}', headers
=headers
, body
=body
)
46 log
.write(f
'POST {req} {body!r}: ')
47 with
ExitStack() as recv
:
48 resp
= recv
.enter_context(self
.api
.getresponse())
49 size
= int(resp
.msg
["Content-Length"])
51 log
.write(f
'{resp.status} {resp.reason} {size} B\n')
52 recv
.callback(Progress
.close
, log
)
53 prog
= Progress(log
, size
)
55 while len(data
) < size
:
56 chunk
= resp
.read(64*1024)
59 prog
.update(len(data
))
60 self
.megan
.handle_folder_response(resp
.msg
, data
)
62 if self
.megan
.file is None:
65 req
= self
.megan
.get_folder_file_request(self
.megan
.file)
66 [self
.megan
.key
, req
] = req
68 [url
, frag
] = url
.split('#', 1)
69 frag
= frag
.split("/", 1)
71 url
= f
'{url}#/{frag[1]}'
75 [req
, headers
, body
] = req
76 headers
= dict(headers
)
77 self
.api
.request('POST', f
'/{req}', headers
=headers
, body
=body
)
78 with self
.api
.getresponse() as resp
:
79 resp
= self
.megan
.handle_file_response(resp
.msg
, resp
.read())
80 [self
.K
, self
.b
] = resp
81 self
.dl_node
= fuse
.ROOT_ID
82 self
.g
= self
.megan
.resp
['g']
83 self
.dl_size
= self
.megan
.resp
['s']
85 self
.name
= self
.megan
.name
88 if self
.megan
.folder
is None or self
.megan
.file is not None:
92 for [handle
, node
] in self
.megan
.nodes
.items():
93 if handle
== self
.megan
.root
:
95 if node
['parent'] == self
.megan
.root
:
98 dir = make_nodeid(node
['parent'])
99 self
.dirs
.setdefault(dir, list()).append((handle
, node
))
101 self
.mount("meganfs", url
)
102 self
.selector
= selectors
.DefaultSelector()
103 cleanup
.callback(self
.selector
.close
)
104 self
.selector
.register(self
.fuse
, selectors
.EVENT_READ
,
107 for [key
, events
] in self
.selector
.select():
109 current
= self
.selector
.get_key(key
.fileobj
)
112 if not current
.events
& key
.events
:
116 class Filesystem(fuse
.Filesystem
):
119 if self
.dl
is not None:
124 def handle_request(self
):
125 unique
= fuse
.Filesystem
.handle_request(self
)
126 if unique
is not None:
127 self
.read_unique
= unique
128 # Changing a selector's "events" mask to 0 raises ValueError
129 self
.selector
.unregister(self
.fuse
)
135 'namelen': len(self
.megan
.name
),
137 result
['files'] = len(self
.megan
.nodes
)
139 for n
in self
.megan
.nodes
.values():
140 result
['blocks'] += ceildiv(n
.get('size', 0), 16)
143 def getattr(self
, node
):
144 node
= self
.megan
.nodes
[self
.decode_nodeid(node
)]
146 'type': TYPE_MAP
[node
['type']],
147 'size': node
.get('size', 0),
150 result
['ctime'] = node
['ts']
151 result
['mtime'] = node
['ts']
154 def lookup(self
, dir, name
):
155 node
= self
.get_dir_map(dir).get(name
)
157 raise OSError(ENOENT
, None)
158 [handle
, node
] = node
159 return make_nodeid(handle
)
161 def readdir(self
, dir, offset
):
162 dir = self
.get_dir_map(dir).items()
163 for [i
, [name
, [handle
, node
]]] in enumerate(dir):
166 type = TYPE_MAP
[node
['type']]
167 yield (make_nodeid(handle
), name
, type, i
+ 1)
169 def get_dir_map(self
, dir):
170 if isinstance(self
.dirs
[dir], list):
174 node
['key'] for [handle
, node
] in self
.dirs
[dir])
175 with
make_encryptor('-aes-128-ecb', '-d', self
.megan
.key
,
176 stdout
=subprocess
.PIPE
) as dec
:
177 [keys
, err
] = dec
.communicate(keys
)
178 assert dec
.returncode
== 0
181 for [handle
, node
] in self
.dirs
[dir]:
182 [k
, b
] = split_key(keys
.read(len(node
['key'])))
183 name
= decrypt_name(node
['a'], k
)
184 map[name
.encode('ascii')] = (handle
, node
)
186 return self
.dirs
[dir]
188 def read(self
, nodeid
, offset
, size
):
189 if nodeid
!= self
.dl_node
:
190 node
= self
.decode_nodeid(nodeid
)
191 [key
, req
] = self
.megan
.get_folder_file_request(node
)
192 [req
, headers
, body
] = req
193 headers
= dict(headers
)
194 self
.api
.request('POST', f
'/{req}', headers
=headers
, body
=body
)
195 with self
.api
.getresponse() as resp
:
196 resp
= decode_response(resp
.msg
, resp
.read())
198 [self
.K
, self
.b
] = split_key(key
)
199 self
.dl_node
= nodeid
200 self
.dl_size
= resp
['s']
201 self
.conn_offset
= None
202 if offset
>= self
.dl_size
:
204 if self
.dl
is not None and self
.conn_offset
!= offset
:
208 assert self
.g
.startswith('http://')
209 [host
, path
] = self
.g
[len('http://'):].split('/', 1)
210 self
.dl
= create_connection((host
, HTTP_PORT
))
211 self
.dl
.setsockopt(IPPROTO_TCP
, TCP_NODELAY
, 1)
212 with self
.dl
.makefile('wb') as writer
:
213 writer
.write(b
'GET /')
214 writer
.write(path
.encode('ascii'))
215 writer
.write(b
' HTTP/1.1\r\n'
217 writer
.write(host
.encode('ascii'))
220 writer
.write(b
'%d' % offset
)
221 writer
.write(b
'-\r\n'
222 b
'User-Agent: meganfs\r\n'
224 self
.conn_offset
= None
226 self
.pending_read
= (offset
, size
)
227 return self
.start_recv()
229 def start_recv(self
):
230 [self
.offset
, size
] = self
.pending_read
231 self
.padding
= self
.offset
% 16
232 self
.buffer = bytearray(self
.padding
+ size
)
234 chunk
= min(len(self
.resp_buf
), self
.resp_len
, size
)
235 self
.resp_pos
= self
.padding
+ chunk
236 self
.buffer[self
.padding
:self
.resp_pos
] = self
.resp_buf
[:chunk
]
237 del self
.resp_buf
[:chunk
]
239 if chunk
< min(self
.resp_len
, size
):
240 self
.selector
.register(self
.dl
, selectors
.EVENT_READ
, self
.recv_body
)
242 return self
.finish_read_sync()
244 self
.resp_buf
= bytearray(0x10000)
246 self
.selector
.register(self
.dl
, selectors
.EVENT_READ
, self
.recv_header
)
249 def recv_header(self
):
250 if self
.v
and not self
.resp_pos
:
251 sys
.stderr
.write('Starting to receive HTTP response\n')
252 searched
= max(self
.resp_pos
- 3, 0)
253 with
memoryview(self
.resp_buf
) as view
, view
[self
.resp_pos
:] as view
:
254 self
.resp_pos
+= self
.dl
.recv_into(view
)
255 end
= self
.resp_buf
.find(b
'\r\n\r\n', searched
, self
.resp_pos
)
257 assert self
.resp_pos
< len(self
.resp_buf
)
260 assert self
.resp_buf
.startswith(b
'HTTP/1.1 ')
261 p
= len(b
'HTTP/1.1 ')
262 if int(self
.resp_buf
[p
: p
+ 3]) != HTTPStatus
.PARTIAL_CONTENT
:
263 msg
= self
.resp_buf
[p
: self
.resp_buf
.index(b
'\r\n', p
+ 3)]
264 raise Exception(msg
.decode('ascii'))
265 assert self
.resp_buf
.find(b
'\r\nTransfer-Encoding:', p
+ 6, end
) < 0
267 start
= b
'\r\nContent-Length:'
268 self
.resp_len
= self
.resp_buf
.find(start
, p
+ 6, end
)
269 assert self
.resp_len
>= 0
270 self
.resp_len
+= len(start
)
271 len_end
= self
.resp_buf
.find(b
'\n', self
.resp_len
, end
+ 2)
272 self
.resp_len
= int(self
.resp_buf
[self
.resp_len
:len_end
])
275 self
.resp_pos
- end
- 4,
276 len(self
.buffer) - self
.padding
,
279 filled
= self
.padding
+ chunk
280 self
.buffer[self
.padding
:filled
] = self
.resp_buf
[end
+ 4 : end
+ 4 + chunk
]
281 self
.resp_buf
= self
.resp_buf
[end
+ 4 + chunk
: self
.resp_pos
]
282 self
.resp_pos
= filled
283 if chunk
< self
.resp_len
and filled
< len(self
.buffer):
284 self
.selector
.modify(self
.dl
, selectors
.EVENT_READ
, self
.recv_body
)
290 with
memoryview(self
.buffer) as view
, \
291 view
[self
.resp_pos
:self
.padding
+ self
.resp_len
] as view
:
292 self
.resp_pos
+= self
.dl
.recv_into(view
)
293 if self
.resp_pos
< min(self
.padding
+ self
.resp_len
, len(self
.buffer)):
298 def finish_read(self
):
299 self
.selector
.unregister(self
.dl
)
300 self
.reply(self
.read_unique
, self
.finish_read_sync())
301 self
.selector
.register(self
.fuse
, selectors
.EVENT_READ
,
304 def finish_read_sync(self
):
305 self
.resp_len
-= self
.resp_pos
- self
.padding
306 self
.conn_offset
= self
.offset
+ self
.resp_pos
- self
.padding
307 iv
= (self
.b
& ~
0 << 64) + (self
.offset
// 16 & bitmask(128))
308 del self
.buffer[self
.resp_pos
:]
309 with
make_encryptor('-aes-128-ctr', '-d', self
.K
, iv
,
310 stdout
=subprocess
.PIPE
) as decryptor
:
311 [buffer, err
] = decryptor
.communicate(self
.buffer)
312 assert decryptor
.returncode
== 0
313 return buffer[self
.padding
:]
315 def decode_nodeid(self
, node
):
318 return urlsafe_b64encode(node
.to_bytes(6, 'big')).decode('ascii')
320 assert node
== fuse
.ROOT_ID
321 return self
.megan
.root
323 def make_nodeid(handle
):
324 return 1 << 48 ^
int.from_bytes(base64_decode(handle
, 48), 'big')
327 NodeType
.FILE
: fuse
.Filesystem
.DT_REG
,
328 NodeType
.FOLDER
: fuse
.Filesystem
.DT_DIR
,
331 if __name__
== '__main__':
332 with fuse
.handle_termination():