Implement Array.splice method
[vadmium-streams.git] / meganfs.py
blob468c0b7243c692e11cb4c4e6fbb4ab61c9affca6
1 #! /usr/bin/env python3
3 import subprocess
4 import sys
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
12 import selectors
13 import fuse
14 import re
15 from errno import ENOENT
16 from base64 import urlsafe_b64encode
17 from datacopy import TerminalLog, Progress
18 from io import BytesIO
20 def main():
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)
26 self.dl = None
27 self.v = args.v
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])
40 else:
41 [req, headers, body] = self.megan.get_folder_request()
42 headers = dict(headers)
43 self.api.request('POST', f'/{req}', headers=headers, body=body)
44 if self.v:
45 log = TerminalLog()
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"])
50 if self.v:
51 log.write(f'{resp.status} {resp.reason} {size} B\n')
52 recv.callback(Progress.close, log)
53 prog = Progress(log, size)
54 data = bytearray()
55 while len(data) < size:
56 chunk = resp.read(64*1024)
57 data.extend(chunk)
58 if self.v:
59 prog.update(len(data))
60 self.megan.handle_folder_response(resp.msg, data)
62 if self.megan.file is None:
63 req = None
64 else:
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)
70 if len(frag) > 1:
71 url = f'{url}#/{frag[1]}'
72 if req is None:
73 self.dl_node = None
74 else:
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']
84 if not self.name:
85 self.name = self.megan.name
86 print(self.name)
88 if self.megan.folder is None or self.megan.file is not None:
89 self.make_file()
90 else:
91 self.dirs = dict()
92 for [handle, node] in self.megan.nodes.items():
93 if handle == self.megan.root:
94 continue
95 if node['parent'] == self.megan.root:
96 dir = fuse.ROOT_ID
97 else:
98 dir = make_nodeid(node['parent'])
99 self.dirs.setdefault(dir, list()).append((handle, node))
100 self.make_dir()
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,
105 self.handle_request)
106 while True:
107 for [key, events] in self.selector.select():
108 try:
109 current = self.selector.get_key(key.fileobj)
110 except KeyError:
111 continue
112 if not current.events & key.events:
113 continue
114 key.data()
116 class Filesystem(fuse.Filesystem):
117 def close(self):
118 try:
119 if self.dl is not None:
120 self.dl.close()
121 finally:
122 super().close()
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)
131 def statfs(self):
132 result = {
133 'bsize': 0x80000,
134 'frsize': 16,
135 'namelen': len(self.megan.name),
137 result['files'] = len(self.megan.nodes)
138 result['blocks'] = 0
139 for n in self.megan.nodes.values():
140 result['blocks'] += ceildiv(n.get('size', 0), 16)
141 return result
143 def getattr(self, node):
144 node = self.megan.nodes[self.decode_nodeid(node)]
145 result = {
146 'type': TYPE_MAP[node['type']],
147 'size': node.get('size', 0),
149 if 'ts' in node:
150 result['ctime'] = node['ts']
151 result['mtime'] = node['ts']
152 return result
154 def lookup(self, dir, name):
155 node = self.get_dir_map(dir).get(name)
156 if node is None:
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):
164 if i < offset:
165 continue
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):
171 map = dict()
173 keys = bytes().join(
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
180 keys = BytesIO(keys)
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)
185 self.dirs[dir] = map
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())
197 self.g = resp['g']
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:
203 return b''
204 if self.dl is not None and self.conn_offset != offset:
205 self.dl.close()
206 self.dl = None
207 if self.dl is None:
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'
216 b'Host: ')
217 writer.write(host.encode('ascii'))
218 writer.write(b'\r\n'
219 b'Range: bytes=')
220 writer.write(b'%d' % offset)
221 writer.write(b'-\r\n'
222 b'User-Agent: meganfs\r\n'
223 b'\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)
233 if self.conn_offset:
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)
241 else:
242 return self.finish_read_sync()
243 else:
244 self.resp_buf = bytearray(0x10000)
245 self.resp_pos = 0
246 self.selector.register(self.dl, selectors.EVENT_READ, self.recv_header)
247 return ...
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)
256 if end < 0:
257 assert self.resp_pos < len(self.resp_buf)
258 return
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])
274 chunk = min(
275 self.resp_pos - end - 4,
276 len(self.buffer) - self.padding,
277 self.resp_len,
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)
285 return
287 self.finish_read()
289 def recv_body(self):
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)):
294 return
296 self.finish_read()
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,
302 self.handle_request)
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):
316 if node & 1 << 48:
317 node &= bitmask(48)
318 return urlsafe_b64encode(node.to_bytes(6, 'big')).decode('ascii')
319 else:
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')
326 TYPE_MAP = {
327 NodeType.FILE: fuse.Filesystem.DT_REG,
328 NodeType.FOLDER: fuse.Filesystem.DT_DIR,
331 if __name__ == '__main__':
332 with fuse.handle_termination():
333 main()