diff --git a/Server/Controller/Api.py b/Server/Controller/Api.py
new file mode 100644
index 0000000..2e86198
--- /dev/null
+++ b/Server/Controller/Api.py
@@ -0,0 +1,228 @@
+import json
+import Server
+import socket
+import struct
+import time
+import urllib.parse
+import requests
+import sys
+import string
+
+rootPath = '.'
+
+try:
+ from PIL import Image
+except ImportError:
+ import Image
+import Server
+import os
+
+def success(request, response, s):
+ response.write(json.dumps({'success': True, 'result': s}))
+
+def error(request, response, s):
+ response.write(json.dumps({'success': False, 'result': s}))
+
+def getUser(request, response):
+ response.write(json.dumps(request.user.__dict__))
+
+def getSearch(request, response):
+ response.write(json.dumps([]))
+
+def getTitles(request, response):
+ response.write(json.dumps([]))
+
+
+def serveFile(response, path, filename = None, start = None, end = None):
+ try:
+ if start is not None:
+ start = int(start)
+
+ if end is not None:
+ end = int(end)
+
+ if not filename:
+ filename = os.path.basename(path)
+
+ response.attachFile(filename)
+
+ chunkSize = 0x400000
+
+ with open(path, "rb") as f:
+ f.seek(0, 2)
+ size = f.tell()
+ if start and end:
+ if end == None:
+ end = size - 1
+ else:
+ end = int(end)
+
+ if start == None:
+ start = size - end
+ else:
+ start = int(start)
+
+ if start >= size or start < 0 or end <= 0:
+ return Server.Response400(request, response, 'Invalid range request %d - %d' % (start, end))
+
+ response.setStatus(206)
+
+ else:
+ if start == None:
+ start = 0
+ if end == None:
+ end = size
+
+ if end >= size:
+ end = size
+
+ if end <= start:
+ response.write(b'')
+ return
+
+ print('ranged request for %d - %d' % (start, end))
+ f.seek(start, 0)
+
+ response.setMime(path)
+ response.setHeader('Accept-Ranges', 'bytes')
+ response.setHeader('Content-Range', 'bytes %s-%s/%s' % (start, end-1, size))
+ response.setHeader('Content-Length', str(end - start))
+ response.sendHeader()
+
+ if not response.head:
+ size = end - start
+
+ i = 0
+ #status = Status.create(size, 'Downloading ' + os.path.basename(path))
+
+ while i < size:
+ chunk = f.read(min(size-i, chunkSize))
+ i += len(chunk)
+
+ #status.add(len(chunk))
+
+ if chunk:
+ pass
+ response.write(chunk)
+ else:
+ break
+ #status.close()
+ except BaseException as e:
+ print('File download exception: ' + str(e))
+
+ if response.bytesSent == 0:
+ response.write(b'')
+
+def isWindows():
+ if "win" in sys.platform[:3].lower():
+ return True
+ else:
+ return False
+
+def listDrives():
+ drives = []
+ if isWindows():
+ import string
+ import ctypes
+ kernel32 = ctypes.windll.kernel32
+ bitmask = kernel32.GetLogicalDrives()
+ for letter in string.ascii_uppercase:
+ if bitmask & 1:
+ drives.append(letter)
+ bitmask >>= 1
+ return drives
+ else:
+ return ['root'];
+
+def isBlocked(path):
+ path = path.lower()
+
+ whitelist = ['.nro', '.xci', '.nsp', '.conf', '.json', '.db', '.tfl', '.jpg', '.gif', '.png', '.bin', '.enc', '.ini', '.ips', '.txt']
+
+ for ext in whitelist:
+ if path.endswith(ext):
+ return False
+
+ return True
+
+def cleanPath(path = None):
+ if not path:
+ return None
+ bits = path.replace('\\', '/').split('/')
+ drive = bits[0]
+ bits = bits[1:]
+
+ if isWindows():
+ path = os.path.abspath(os.path.join(drive+':/', '/'.join(bits)))
+ else:
+ path = os.path.abspath('/'.join(bits))
+
+ #if not path.startswith(os.path.abspath(rootPath)):
+ # raise IOError('invalid path requested: ' + path)
+ return path
+
+def getDirectoryList(request, response):
+ r = {'dirs': [], 'files': []}
+ try:
+ path = ''
+ for i in request.bits[2:]:
+ path = os.path.join(path, i)
+
+ if path:
+ path = cleanPath(path)
+
+ r = {'dirs': [], 'files': []}
+
+ if not path:
+ for d in listDrives():
+ r['dirs'].append({'name': d})
+ response.write(json.dumps(r))
+ return
+
+ for name in os.listdir(path):
+ abspath = os.path.join(path, name)
+
+ if os.path.isdir(abspath):
+ r['dirs'].append({'name': name})
+ elif os.path.isfile(abspath):
+ if not isBlocked(abspath):
+ r['files'].append({'name': name, 'size': os.path.getsize(abspath), 'mtime': os.path.getmtime(abspath)})
+ except:
+ raise
+ raise IOError('dir list access denied')
+ response.write(json.dumps(r))
+
+def getFile(request, response, start = None, end = None):
+ path = ''
+ for i in request.bits[2:]:
+ path = os.path.join(path, i)
+ path = cleanPath(path)
+
+ if isBlocked(path):
+ raise IOError('read access denied')
+
+ if 'Range' in request.headers:
+ start, end = request.headers.get('Range').strip().strip('bytes=').split('-')
+
+ if end != '':
+ end = int(end) + 1
+
+ if start != '':
+ start = int(start)
+
+ return serveFile(response, path, start = start, end = end)
+
+def getFileSize(request, response):
+ t = {}
+ path = ''
+ for i in request.bits[2:]:
+ path = os.path.join(path, i)
+ path = cleanPath(path)
+ try:
+ t['size'] = os.path.getsize(path);
+ t['mtime'] = os.path.getmtime(path);
+ response.write(json.dumps(t))
+ except BaseException as e:
+ response.write(json.dumps({'success': False, 'message': str(e)}))
+
+
diff --git a/Server/Controller/__init__.py b/Server/Controller/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Server/__init__.py b/Server/__init__.py
new file mode 100644
index 0000000..d56fed0
--- /dev/null
+++ b/Server/__init__.py
@@ -0,0 +1,268 @@
+import http.server
+import threading
+import socket
+import socketserver
+import time
+import sys
+import os
+from os import listdir
+import re
+import urllib
+import base64
+from urllib.parse import urlparse
+from urllib.parse import parse_qs
+
+import Server.Controller.Api
+
+
+global httpd
+global sock
+global addr
+global mimes
+
+mimes = {}
+httpd = None
+sock = None
+addr = None
+threads = []
+
+mappings = {'api': Server.Controller.Api}
+
+mimes = {
+ '.css': 'text/css',
+ '.js': 'application/javascript',
+ '.html': 'text/html',
+ '.png': 'image/png',
+ '.nsx': 'application/octet-stream',
+ '.nsp': 'application/octet-stream',
+ '.jpg': 'image/jpeg'
+ }
+
+class Thread(threading.Thread):
+ def __init__(self, i, ctx):
+ self.ctx = ctx
+ threads.append(self)
+ threading.Thread.__init__(self)
+ self.i = i
+ self.daemon = True
+ self.start()
+ def run(self):
+ httpd = http.server.HTTPServer(addr, NutHandler, False)
+
+ httpd.socket = sock
+ httpd.server_bind = self.server_close = lambda self: None
+
+ httpd.serve_forever()
+
+def run(ctx):
+ global httpd
+ global sock
+ global addr
+
+ print(time.asctime() + ' Server Starts - %s:%s' % (ctx.host_ip, 9000))
+ try:
+ addr = (ctx.host_ip, 9000)
+ sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind(addr)
+ sock.listen(5)
+
+ [Thread(i, ctx) for i in range(16)]
+ for thread in threads:
+ thread.join()
+ except KeyboardInterrupt:
+ pass
+
+ print(time.asctime() + ' Server Stops - %s:%s' % (ctx.host_ip, 9000))
+
+class NutRequest:
+ def __init__(self, handler):
+ self.handler = handler
+ self.headers = handler.headers
+ self.path = handler.path
+ self.head = False
+ self.url = urlparse(self.path)
+
+ try:
+ length = int(self.headers['Content-Length'])
+ if not length:
+ self.post = None
+ else:
+ self.post = handler.rfile.read(length)
+ #Print.info('reading %s bytes from post' % self.headers['Content-Length'])
+ except:
+ self.post = None
+
+ self.bits = [urllib.parse.unquote(x) for x in self.url.path.split('/') if x]
+ self.query = parse_qs(self.url.query)
+
+ try:
+ for k,v in self.query.items():
+ self.query[k] = v[0];
+ except:
+ pass
+
+ self.user = None
+
+ def setHead(self, h):
+ self.head = h
+
+class NutResponse:
+ def __init__(self, handler):
+ self.handler = handler
+ self.bytesSent = 0
+ self.status = 200
+ self.head = False
+ self.headersSent = False
+ self.headers = {'Content-type': 'text/html'}
+
+ def setHead(self, h):
+ self.head = h
+
+ def setStatus(self, s):
+ self.status = s
+
+ def setHeader(self, k, v):
+ self.headers[k] = v
+
+ def setMime(self, fileName):
+ try:
+ name, ext = os.path.splitext(fileName)
+
+ if ext in mimes:
+ self.headers['Content-type'] = mimes[ext]
+ else:
+ raise IOError('Mime not found')
+ except:
+ pass
+
+ def attachFile(self, fileName):
+ #Print.info('Attaching file ' + fileName)
+ self.setMime(fileName)
+ self.headers['Content-Disposition'] = 'attachment; filename=' + fileName
+
+ def sendHeader(self):
+ self.handler.send_response(self.status)
+
+ for k,v in self.headers.items():
+ self.handler.send_header(k, v)
+
+ self.handler.end_headers()
+ self.headersSent = True
+
+ def write(self, data):
+ if self.bytesSent == 0 and not self.headersSent:
+ self.sendHeader()
+
+ if type(data) == str:
+ data = data.encode('utf-8')
+
+ self.bytesSent += len(data)
+
+ return self.handler.wfile.write(data)
+
+def Response400(request, response, error='400'):
+ response.setStatus(400)
+ response.write(error)
+
+def Response404(request, response):
+ response.setStatus(404)
+ response.write('404')
+
+def Response500(request, response):
+ response.setStatus(500)
+ response.write('500')
+
+def Response401(request, response):
+ response.setStatus(401)
+ response.headers['WWW-Authenticate'] = 'Basic realm=\"Nut\"'
+ response.write('401')
+
+def route(request, response, verb = 'get'):
+ try:
+ print('routing')
+ if len(request.bits) > 0 and request.bits[0] in mappings:
+ i = request.bits[1]
+ methodName = verb + i[0].capitalize() + i[1:]
+ print('routing to ' + methodName)
+ method = getattr(mappings[request.bits[0]], methodName, Response404)
+ method(request, response, **request.query)
+ return True
+ except BaseException as e:
+ print(str(e))
+ return None
+ return False
+
+class NutHandler(http.server.BaseHTTPRequestHandler):
+ def __init__(self, *args):
+ self.basePath = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
+ super(NutHandler, self).__init__(*args)
+
+ def do_HEAD(self):
+ request = NutRequest(self)
+ response = NutResponse(self)
+ request.setHead(True)
+ response.setHead(True)
+
+ if self.headers['Authorization'] == None:
+ return Response401(request, response)
+
+ id, password = base64.b64decode(self.headers['Authorization'].split(' ')[1]).decode().split(':')
+
+ #request.user = Users.auth(id, password, self.client_address[0])
+
+ #if not request.user:
+ # return Response401(request, response)
+
+ try:
+ if len(request.bits) > 0 and request.bits[0] in mappings:
+ i = request.bits[1]
+ methodName = 'get' + i[0].capitalize() + i[1:]
+ method = getattr(mappings[request.bits[0]], methodName, Response404)
+ method(request, response, **request.query)
+ else:
+ self.handleFile(request, response)
+ except BaseException as e:
+ self.wfile.write(Response500(request, response))
+
+ def do(self, verb = 'get'):
+ request = NutRequest(self)
+ response = NutResponse(self)
+
+ if self.headers['Authorization'] == None:
+ return Response401(request, response)
+
+ id, password = base64.b64decode(self.headers['Authorization'].split(' ')[1]).decode().split(':')
+
+ #request.user = Users.auth(id, password, self.client_address[0])
+
+ #if not request.user:
+ # return Response401(request, response)
+
+ try:
+ if not route(request, response, verb):
+ self.handleFile(request, response)
+ except BaseException as e:
+ self.wfile.write(Response500(request, response))
+
+ def do_GET(self):
+ self.do('get')
+
+
+ def do_POST(self):
+ self.do('post')
+
+
+ def handleFile(self, request, response):
+ path = os.path.abspath(self.basePath + '/public_html' + self.path)
+ if not path.startswith(self.basePath):
+ raise IOError('invalid path requested: ' + self.basePath + ' vs ' + path)
+
+ if os.path.isdir(path):
+ path += '/index.html'
+
+ if not os.path.isfile(path):
+ return Response404(request, response)
+ response.setMime(path)
+ with open(path, 'rb') as f:
+ response.write(f.read())
\ No newline at end of file
diff --git a/fluffy.pyw b/fluffy.pyw
index 0ed36cc..4c563f2 100755
--- a/fluffy.pyw
+++ b/fluffy.pyw
@@ -24,7 +24,6 @@ along with this program. If not, see .
# Imports
import signal
import time
-import socket
import base64
import os
import sys
@@ -33,50 +32,72 @@ import struct
import random
import re
import configparser
-try:
- if "win" in sys.platform[:3].lower():
- initial_dir = os.getcwd() + "/"
- elif "linux" in sys.platform.lower():
- if not os.path.exists(os.path.expanduser('~') + "/.fluffy"):
- os.makedirs(os.path.expanduser('~') + "/.fluffy")
- initial_dir = os.path.expanduser('~') + "/.fluffy/"
- else: # MacOS. A little help here would be great.
- initial_dir = os.getcwd() + "/"
-except:
- initial_dir = os.getcwd() + "/"
- pass
-try:
- import logging
- if os.path.isfile(initial_dir + 'fluffy.log'):
- os.remove(initial_dir + 'fluffy.log')
- LOG_FILENAME = initial_dir + 'fluffy.log'
- logging.basicConfig(filename=LOG_FILENAME, level=logging.DEBUG)
- logging.debug("Fluffy Log: If you see nothing here. Good!")
- is_logging = True
-except:
- is_logging = False
- print('Error: Logging not possible. Possible permission issue.')
- pass
+import goldleaf
+import tinfoil
+import nut
+
+class Context:
+ def __init__(self):
+ try:
+ if "win" in sys.platform[:3].lower():
+ self.initial_dir = os.getcwd() + "/"
+ elif "linux" in sys.platform.lower():
+ if not os.path.exists(os.path.expanduser('~') + "/.fluffy"):
+ os.makedirs(os.path.expanduser('~') + "/.fluffy")
+ self.initial_dir = os.path.expanduser('~') + "/.fluffy/"
+ else: # MacOS. A little help here would be great.
+ self.initial_dir = os.getcwd() + "/"
+ except:
+ self.initial_dir = os.getcwd() + "/"
+ pass
+
+ try:
+ import logging
+ if os.path.isfile(self.initial_dir + 'fluffy.log'):
+ os.remove(self.initial_dir + 'fluffy.log')
+ LOG_FILENAME = self.initial_dir + 'fluffy.log'
+ logging.basicConfig(filename=LOG_FILENAME, level=logging.DEBUG)
+ logging.debug("Fluffy Log: If you see nothing here. Good!")
+ self.is_logging = True
+ except:
+ self.is_logging = False
+ print('Error: Logging not possible. Possible permission issue.')
+ pass
+
+ self.is_installing = False
+ self.switch_ip = "0.0.0.0"
+ self.host_ip = "0.0.0.0"
+
+ self.qresponse = False
+ self.needresponse = False
+ self.qrespnum = 0
+ self.haveresponse = False
+ self.allow_access_non_nsp = 0
+ self.ignore_warning_prompt = 0
+ self.global_dev = None
+ self.global_in = None
+ self.global_out = None
+ self.task_canceled = False
+ self.usb_success = False
+
+ self.installer_type = 0
+
+ctx = Context()
+
+
try:
from tkinter import filedialog
import tkinter as tk
root = tk.Tk()
root.withdraw()
except Exception as e:
- if is_logging:
+ if ctx.is_logging:
logging.error(e, exc_info=True)
logging.debug("Error: Failed to import Tkinter.")
print('Error: Failed to import Tkinter.')
print(str(e))
sys.exit()
-try:
- from SimpleHTTPServer import SimpleHTTPRequestHandler
- from SocketServer import TCPServer
- from urllib import quote
-except ImportError:
- from http.server import SimpleHTTPRequestHandler
- from socketserver import TCPServer
- from urllib.parse import quote
+
try:
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt, QThread, QByteArray
@@ -85,7 +106,7 @@ try:
app = QtWidgets.QApplication(sys.argv)
window = QMainWindow()
except Exception as e:
- if is_logging:
+ if ctx.is_logging:
logging.error(e, exc_info=True)
logging.debug("Error: Failed to import PyQt5.")
print('Error: Failed to import PyQt5.')
@@ -97,7 +118,7 @@ try:
except:
logging.debug("Error: Failed to import modules required for USB install. Defaulting to Network Mode.")
print('Error: Failed to import modules required for USB install. Defaulting to Network Mode.')
- usb_success = False
+ ctx.usb_success = False
pass
# Variables
@@ -114,11 +135,10 @@ inlaypixmap = QPixmap()
dinlaypixmap = QPixmap()
aboutpixmap = QPixmap()
transfer_rate = 0
-is_installing = False
+ctx.is_installing = False
last_error = "NA"
is_done = False
is_network = False
-is_goldleaf = False
selected_dir = None
selected_files = None
sent_header = False
@@ -130,48 +150,55 @@ end_progress = 0
cur_nsp_count = 1
total_nsp = 0
cur_nsp_name = "NA"
-switch_ip = "0.0.0.0"
-host_ip = "0.0.0.0"
+
language = 0
-qresponse = False
-needresponse = False
-qrespnum = 0
-haveresponse = False
-allow_access_non_nsp = 0
-ignore_warning_prompt = 0
-global_dev = None
-global_in = None
-global_out = None
-task_canceled = False
-usb_success = False
+
+def is_goldleaf():
+ return ctx.installer_type == 2
+
+def is_tinfoil():
+ return ctx.installer_type == 0
+
+def is_nut():
+ return ctx.installer_type == 1
+
+def set_goldleaf():
+ ctx.installer_type = 2
+
+def set_tinfoil():
+ ctx.installer_type = 0
+
+def set_nut():
+ ctx.installer_type = 1
+
# Load Settings
-if os.path.isfile(initial_dir + 'fluffy.conf'):
+if os.path.isfile(ctx.initial_dir + 'fluffy.conf'):
try:
- with open(initial_dir + 'fluffy.conf') as cfgfile:
+ with open(ctx.initial_dir + 'fluffy.conf') as cfgfile:
configp = configparser.ConfigParser()
configp.read_file(cfgfile)
- switch_ip = configp.get('DEFAULT', 'switch_ip')
+ ctx.switch_ip = configp.get('DEFAULT', 'switch_ip')
dark_mode = int(configp.get('DEFAULT', 'dark_mode'))
language = int(configp.get('DEFAULT', 'language'))
- allow_access_non_nsp = int(configp.get('DEFAULT', 'allow_access_non_nsp'))
- ignore_warning_prompt = int(configp.get('DEFAULT', 'ignore_warning_prompt'))
- print("Successfully loaded config: \'" + str(initial_dir) + "fluffy.conf\'")
+ ctx.allow_access_non_nsp = int(configp.get('DEFAULT', 'allow_access_non_nsp'))
+ ctx.ignore_warning_prompt = int(configp.get('DEFAULT', 'ignore_warning_prompt'))
+ print("Successfully loaded config: \'" + str(ctx.initial_dir) + "fluffy.conf\'")
except:
- print("Config not found: \'" + str(initial_dir) + "fluffy.conf\'")
- switch_ip = "0.0.0.0"
+ print("Config not found: \'" + str(ctx.initial_dir) + "fluffy.conf\'")
+ ctx.switch_ip = "0.0.0.0"
dark_mode = 0
language = 0
- allow_access_non_nsp = 0
- ignore_warning_prompt = 0
+ ctx.allow_access_non_nsp = 0
+ ctx.ignore_warning_prompt = 0
pass
else:
- print("Config not found: \'" + str(initial_dir) + "fluffy.conf\'")
- switch_ip = "0.0.0.0"
+ print("Config not found: \'" + str(ctx.initial_dir) + "fluffy.conf\'")
+ ctx.switch_ip = "0.0.0.0"
dark_mode = 0
language = 0
- allow_access_non_nsp = 0
- ignore_warning_prompt = 0
+ ctx.allow_access_non_nsp = 0
+ ctx.ignore_warning_prompt = 0
@@ -602,8 +629,7 @@ class Language:
}
-
-
+
set_language(language)
# End Language
@@ -684,8 +710,8 @@ def set_dark_mode(v):
def turn_off_logging():
- global is_logging
- is_logging = False
+ global ctx
+ ctx.is_logging = False
def set_nca_name(v):
global cur_nca_name
@@ -696,8 +722,8 @@ def set_start_time():
start_time = time.time()
def set_canceled(x):
- global task_canceled
- task_canceled = x
+ global ctx
+ ctx.task_canceled = x
def set_cur_transfer_rate(v):
global cur_transfer_rate
@@ -712,29 +738,27 @@ def set_last_transfer_rate(v):
last_transfer_rate = v
def detach_switch():
- global global_dev
- global global_out
- global global_in
+ global ctx
+
try:
- usb.util.dispose_resources(global_dev)
- global_dev.reset()
+ usb.util.dispose_resources(ctx.global_dev)
+ ctx.global_dev.reset()
except:
pass
- global_in = None
- global_out = None
- global_dev = None
+ ctx.global_in = None
+ ctx.global_out = None
+ ctx.global_dev = None
def connect_switch():
- global global_dev
- global global_out
- global global_in
- global_dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
- if global_dev is not None:
+ global ctx
+
+ ctx.global_dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
+ if ctx.global_dev is not None:
try:
- global_dev.set_configuration()
- intf = global_dev.get_active_configuration()[(0,0)]
- global_out = usb.util.find_descriptor(intf,custom_match=lambda e:usb.util.endpoint_direction(e.bEndpointAddress)==usb.util.ENDPOINT_OUT)
- global_in = usb.util.find_descriptor(intf,custom_match=lambda e:usb.util.endpoint_direction(e.bEndpointAddress)==usb.util.ENDPOINT_IN)
+ ctx.global_dev.set_configuration()
+ intf = ctx.global_dev.get_active_configuration()[(0,0)]
+ ctx.global_out = usb.util.find_descriptor(intf,custom_match=lambda e:usb.util.endpoint_direction(e.bEndpointAddress)==usb.util.ENDPOINT_OUT)
+ ctx.global_in = usb.util.find_descriptor(intf,custom_match=lambda e:usb.util.endpoint_direction(e.bEndpointAddress)==usb.util.ENDPOINT_IN)
return True
except:
return False
@@ -745,12 +769,12 @@ def connect_switch():
def save_config():
try:
configp = configparser.ConfigParser()
- configp['DEFAULT'] = {'switch_ip': switch_ip,
+ configp['DEFAULT'] = {'switch_ip': ctx.switch_ip,
'language': language,
'dark_mode': dark_mode,
- 'allow_access_non_nsp': allow_access_non_nsp,
- 'ignore_warning_prompt': ignore_warning_prompt}
- with open(initial_dir + 'fluffy.conf', 'w') as cfgfile:
+ 'allow_access_non_nsp': ctx.allow_access_non_nsp,
+ 'ignore_warning_prompt': ctx.ignore_warning_prompt}
+ with open(ctx.initial_dir + 'fluffy.conf', 'w') as cfgfile:
configp.write(cfgfile)
except:
pass
@@ -760,25 +784,21 @@ def set_transfer_rate(v):
transfer_rate = TransferRateDict[v]
def get_response_qmessage(e):
- global needresponse
- global qrespnum
- needresponse = True
- qrespnum = e
+ global ctx
+ ctx.needresponse = True
+ ctx.qrespnum = e
def set_response_qmessage(x):
- global qresponse
- global haveresponse
- haveresponse = True
- qresponse = x
+ global ctx
+ ctx.haveresponse = True
+ ctx.qresponse = x
def reset_response():
- global needresponse
- global qresponse
- global haveresponse
- needresponse = False
- qresponse = False
- haveresponse = False
+ global ctx
+ ctx.needresponse = False
+ ctx.qresponse = False
+ ctx.haveresponse = False
def set_dir(d):
@@ -820,11 +840,11 @@ def complete_install():
is_done = True
def complete_goldleaf_transfer():
- global is_installing
- is_installing = False
+ global ctx
+ ctx.is_installing = False
def reset_install():
- global is_installing
+ global ctx
global sent_header
global is_done
global cur_progress
@@ -846,13 +866,14 @@ def reset_install():
usb_radio.setEnabled(True)
txt_port.setEnabled(True)
tin_radio.setEnabled(True)
+ nut_radio.setEnabled(True)
gold_radio.setEnabled(True)
l_nsp.setText("")
l_nsp.setStyleSheet("")
l_switch.setText("")
l_switch.setStyleSheet("")
l_status.setStyleSheet("")
- if is_goldleaf:
+ if is_goldleaf():
l_status.setText('')
progressbar.setValue(0)
cur_nsp_count = 1
@@ -863,7 +884,7 @@ def reset_install():
cur_transfer_rate = 0
last_transfer_rate = 0
is_done = False
- is_installing = False
+ ctx.is_installing = False
sent_header = False
cur_progress = 0
end_progress = 100
@@ -888,610 +909,36 @@ def reset_last_error():
last_error = "NA"
def complete_loading():
- global is_installing
- is_installing = True
+ global ctx
+ ctx.is_installing = True
def set_network(v):
global is_network
is_network = v
def set_ip(v, n):
- global switch_ip
- global host_ip
+ global ctx
+
if n == 0:
- switch_ip = v
+ ctx.switch_ip = v
else:
- host_ip = v
+ ctx.host_ip = v
-def set_goldleaf(v):
- global is_goldleaf
- is_goldleaf = v
def set_usb_success(v):
- global usb_success
- usb_success = v
-
-# Goldleaf
-class GoldleafCommandId:
- ListSystemDrives = 0
- GetEnvironmentPaths = 1
- GetPathType = 2
- ListDirectories = 3
- ListFiles = 4
- GetFileSize = 5
- FileRead = 6
- FileWrite = 7
- CreateFile = 8
- CreateDirectory = 9
- DeleteFile = 10
- DeleteDirectory = 11
- RenameFile = 12
- RenameDirectory = 13
- GetDriveTotalSpace = 14
- GetDriveFreeSpace = 15
- GetNSPContents = 16
- Max = 17
-
-class GoldleafCommandReadResult:
- Success = 0
- InvalidMagic = 1
- InvalidGoldleafCommandId = 2
-
-class Goldleaf:
- GLUC = b"GLUC"
- magic = b"GLUC"
- cmd_id = 0
- drives = {}
- FW_DENIED = 0
- FW_ACCEPTED = 1
- FW_NOSTATUS = 2
- fw_status = FW_NOSTATUS
+ global ctx
+ ctx.usb_success = v
- def init(self):
- try:
- detach_switch()
- connect_switch()
- self.goldleaf_usb()
- except Exception as e:
- if is_logging:
- logging.error(e, exc_info=True)
- throw_error(0)
- sys.exit()
-
- def write(self,buffer):
- try:
- global_out.write(buffer,timeout=3000)
- except:
- pass
- def read(self,length):
- return global_in.read(length,timeout=0).tobytes()
-
- def write_u32(self,x):
- try:
- global_out.write(struct.pack(">= 1
- else:
- self.drives["ROOT"] = "/"
- self.write_u32(len(self.drives))
- for d in self.drives:
- try:
- self.write_string(drive_labels[d])
- except KeyError:
- self.write_string(d)
- self.write_string(d)
- elif self.is_id(GoldleafCommandId.GetEnvironmentPaths):
- env_paths = {x:os.path.expanduser("~/"+x) for x in ["Desktop", "Documents"]}
- for arg in sys.argv[1:]:
- folder = os.path.abspath(arg)
- if os.path.isfile(folder):
- folder = os.path.dirname(folder)
- env_paths[os.path.basename(folder)] = folder
- env_paths = {x:env_paths[x] for x in env_paths if os.path.exists(env_paths[x])}
- self.write_u32(len(env_paths))
- for env in env_paths:
- env_paths[env] = env_paths[env].replace("\\", "/")
- self.write_string(env)
- if env_paths[env][1:3] != ":/":
- env_paths[env] = "ROOT:" + env_paths[env]
- self.write_string(env_paths[env])
- elif self.is_id(GoldleafCommandId.GetPathType):
- ptype = 0
- path = self.read_path()
- if os.path.isfile(path):
- ptype = 1
- elif os.path.isdir(path):
- ptype = 2
- self.write_u32(ptype)
- elif self.is_id(GoldleafCommandId.ListDirectories):
- path = self.read_path()
- ents = [x for x in os.listdir(path) if os.path.isdir(os.path.join(path, x))]
- n_ents = []
- for e in ents:
- try:
- test = os.listdir(os.path.join(path, e))
- n_ents.append(e)
- except:
- pass
- self.write_u32(len(n_ents))
- for name in n_ents:
- self.write_string(name)
- elif self.is_id(GoldleafCommandId.ListFiles):
- self.fw_status = self.FW_NOSTATUS
- if is_installing:
- complete_goldleaf_transfer()
- path = self.read_path()
- ents = [x for x in os.listdir(path) if os.path.isfile(os.path.join(path, x))]
- if not allow_access_non_nsp:
- len_nsps = 0
- for f in ents:
- if f.lower().endswith('.nsp'):
- len_nsps = len_nsps+1
- self.write_u32(len_nsps)
- for name in ents:
- if name.lower().endswith('.nsp'):
- self.write_string(name)
- else:
- self.write_u32(len(ents))
- for name in ents:
- self.write_string(name)
- elif self.is_id(GoldleafCommandId.GetFileSize):
- path = self.read_path()
- self.write_u64(os.path.getsize(path))
- elif self.is_id(GoldleafCommandId.FileRead):
- can_read = True
- offset = self.read_u64()
- size = self.read_u64()
- path = self.read_path()
- if not os.path.basename(path).lower().endswith('.nsp'):
- if allow_access_non_nsp:
- can_read = True
- else:
- can_read = False
- if can_read:
- with open(path, "rb") as f:
- f.seek(offset)
- data = f.read(size)
- self.write_u64(len(data))
- self.write(data)
- try:
- if self.fw_status != self.FW_DENIED:
- complete_loading()
- set_cur_nsp(str(os.path.basename(path)))
- set_progress(int(offset), int(os.path.getsize(path)))
- elapsed_time = time.time() - start_time
- if elapsed_time >= 1:
- set_cur_transfer_rate(int(offset) - last_transfer_rate)
- set_last_transfer_rate(int(offset))
- set_start_time()
- else:
- complete_goldleaf_transfer()
- except:
- pass
- else:
- logging.debug("Error: Access denied. \nReason: Goldleaf tried to access a non .NSP file(to bypass this default restriction, change \'allow_access_non_nsp\' to 1 in fluffy.conf).")
- print("Error: Access denied. \nReason: Goldleaf tried to access a non .NSP file(to bypass this default restriction, change \'allow_access_non_nsp\' to 1 in fluffy.conf).")
- cancel_task()
- sys.exit()
- elif self.is_id(GoldleafCommandId.FileWrite):
- offset = self.read_u64()
- size = self.read_u64()
- path = self.read_path()
- data = self.read(size)
- can_write = False
- if self.fw_status == self.FW_NOSTATUS:
- get_response_qmessage(1)
- while not haveresponse and global_dev is not None:
- time.sleep(1)
- if qresponse:
- self.fw_status = self.FW_ACCEPTED
- can_write = True
- else:
- self.fw_status = self.FW_DENIED
- elif self.fw_status == self.FW_ACCEPTED:
- can_write = True
- if can_write:
- cont = bytearray()
- try:
- with open(path, "rb") as f:
- cont=bytearray(f.read())
- except FileNotFoundError:
- pass
- cont[offset:offset + size] = data
- with open(path, "wb") as f:
- f.write(cont)
- reset_response()
- elif self.is_id(GoldleafCommandId.CreateFile):
- path = self.read_path()
- get_response_qmessage(2)
- while not haveresponse and global_dev is not None:
- time.sleep(1)
- if qresponse:
- open(path, "a").close()
- reset_response()
- elif self.is_id(GoldleafCommandId.CreateDirectory):
- path = self.read_path()
- get_response_qmessage(3)
- while not haveresponse and global_dev is not None:
- time.sleep(1)
- if qresponse:
- try:
- os.mkdir(path)
- except os.FileExistsError:
- pass
- reset_response()
- elif self.is_id(GoldleafCommandId.DeleteFile):
- path = self.read_path()
- get_response_qmessage(4)
- while not haveresponse and global_dev is not None:
- time.sleep(1)
- if qresponse:
- os.remove(path)
- reset_response()
- elif self.is_id(GoldleafCommandId.DeleteDirectory):
- path = self.read_path()
- get_response_qmessage(5)
- while not haveresponse and global_dev is not None:
- time.sleep(1)
- if qresponse:
- shutil.rmtree(path)
- reset_response()
- elif self.is_id(GoldleafCommandId.RenameFile):
- path = self.read_path()
- new_name = self.read_string()
- get_response_qmessage(6)
- while not haveresponse and global_dev is not None:
- time.sleep(1)
- if qresponse:
- os.rename(path, new_name)
- reset_response()
- elif self.is_id(GoldleafCommandId.RenameDirectory):
- path = self.read_path()
- new_name = self.read_path()
- get_response_qmessage(6)
- while not haveresponse and global_dev is not None:
- time.sleep(1)
- if qresponse:
- os.rename(path, new_name)
- reset_response()
- elif self.is_id(GoldleafCommandId.GetDriveTotalSpace):
- path = self.read_path()
- disk = os.statvfs(path)
- totalBytes = float(disk.f_bsize*disk.f_blocks)
- self.write_u64(int(totalspace))
- elif self.is_id(GoldleafCommandId.GetDriveFreeSpace):
- path = self.read_path()
- disk = os.statvfs(path)
- totalFreeSpace = float(disk.f_bsize*disk.f_bfree)
- self.write_u64(int(totalFreeSpace))
- sys.exit()
-
-# Tinfoil Network
-netrlist = []
-def reset_netrlist():
- global netrlist
- netrlist = None
- netrlist = []
-def append_netrlist(v, v2):
- global netrlist
- netrlist.append((v, v2))
-class TinfoilNetwork:
- def init(self):
- reset_netrlist()
- accepted_extension = ('.nsp')
- hostPort = random.randint(26490,26999)
- target_ip = switch_ip
- hostIp = host_ip
- target_path = str(selected_dir).strip()
- baseUrl = hostIp + ':' + str(hostPort) + '/'
- directory = target_path
- file_list_payload = ''
- for file in [file for file in next(os.walk(target_path))[2] if file.endswith(accepted_extension)]:
- for y in selected_files:
- if str(file).find(os.path.basename(y)) != -1:
- n = random.randint(1,10000000)
- fake_file = str(n) + ".nsp"
- append_netrlist(fake_file, str(y))
- file_list_payload += baseUrl + fake_file + '\n'
- file_list_payloadBytes = file_list_payload.encode('ascii')
- if directory and directory != '.':
- os.chdir(directory)
- server = TinfoilServer((host_ip, hostPort), TinfoilHTTPHandler)
- thread = threading.Thread(target=server.serve_forever)
- thread.daemon = True
- thread.start()
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.connect((target_ip, 2000))
- sock.sendall(struct.pack('!L', len(file_list_payloadBytes)) + file_list_payloadBytes)
- while len(sock.recv(1)) < 1:
- if task_canceled:
- server.force_stop()
- sys.exit()
- time.sleep(0.1)
- sock.close()
- except Exception as e:
- if is_logging:
- logging.error(e, exc_info=True)
- server.force_stop()
- throw_error(1)
- sys.exit()
- complete_install()
- server.force_stop()
- sys.exit()
-class TinfoilHTTPHandler(SimpleHTTPRequestHandler):
- def send_head(self):
- for s in range(len(netrlist)):
- if netrlist[s][0] == str(self.path)[1:]:
- path = netrlist[s][1]
- ctype = self.guess_type(path)
- if os.path.isdir(path):
- return SimpleHTTPRequestHandler.send_head(self)
- if not os.path.exists(path):
- return self.send_error(404, self.responses.get(404)[0])
- f = open(path, 'rb')
- fs = os.fstat(f.fileno())
- size = fs[6]
- start, end = 0, size - 1
- if 'Range' in self.headers:
- start, end = self.headers.get('Range').strip().strip('bytes=')\
- .split('-')
- if start == "":
- try:
- end = int(end)
- except ValueError as e:
- self.send_error(400, 'invalid range')
- start = size - end
- else:
- try:
- start = int(start)
- except ValueError as e:
- self.send_error(400, 'invalid range')
- if start >= size:
- self.send_error(416, self.responses.get(416)[0])
- if end == "":
- end = size - 1
- else:
- try:
- end = int(end)
- except ValueError as e:
- self.send_error(400, 'invalid range')
-
- start = max(start, 0)
- end = min(end, size - 1)
- self.range = (start, end)
- cont_length = end - start + 1
- if 'Range' in self.headers:
- self.send_response(206)
- else:
- self.send_response(200)
- self.send_header('Content-type', ctype)
- self.send_header('Accept-Ranges', 'bytes')
- self.send_header('Content-Range','bytes %s-%s/%s' % (start, end, size))
- self.send_header('Content-Length', str(cont_length))
- self.send_header('Last-Modified', self.date_time_string(fs.st_mtime))
- self.end_headers()
- return f
-
- def copyfile(self, infile, outfile):
- if 'Range' not in self.headers:
- SimpleHTTPRequestHandler.copyfile(self, infile, outfile)
- return
- complete_loading()
- set_cur_nsp(str(os.path.basename(infile.name)))
- start, end = self.range
- infile.seek(start)
- bufsize = 64 * 1024 # 64KB
- while True:
- if task_canceled: sys.exit()
- buf = infile.read(bufsize)
- if not buf:
- break
- try:
- outfile.write(buf)
- try:
- set_progress(int(infile.tell()), int(end))
- elapsed_time = time.time() - start_time
- if elapsed_time >= 1:
- set_cur_transfer_rate(int(infile.tell()) - last_transfer_rate)
- set_last_transfer_rate(int(infile.tell()))
- set_start_time()
- except:
- pass
- except BrokenPipeError:
- pass
-class TinfoilServer(TCPServer):
- stopped = False
- def server_bind(self):
- import socket
- self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- self.socket.bind(self.server_address)
- def serve_forever(self):
- while not self.stopped:
- if task_canceled: sys.exit()
- self.handle_request()
- sys.exit()
- def force_stop(self):
- self.server_close()
- self.stopped = True
- sys.exit()
-
-
-# Tinfoil USB
-class Tinfoil:
- CMD_ID_EXIT = 0
- CMD_ID_FILE_RANGE = 1
- CMD_TYPE_RESPONSE = 1
-
- def init(self):
- try:
- detach_switch()
- connect_switch()
- self.send_nsp_list()
- self.poll_commands()
- complete_install()
- sys.exit()
- except Exception as e:
- if is_logging:
- logging.error(e, exc_info=True)
- throw_error(2)
- sys.exit()
-
- def send_response_header(self, cmd_id, data_size):
- global_out.write(b'TUC0')
- global_out.write(struct.pack('= end_off:
- read_size = end_off - curr_off
- try:
- set_progress(int(end_off), int(end_off))
- except:
- pass
- buf = f.read(read_size)
- global_out.write(data=buf, timeout=0)
- curr_off += read_size
- try:
- set_progress(int(curr_off), int(end_off))
- elapsed_time = time.time() - start_time
- if elapsed_time >= 1:
- set_cur_transfer_rate(curr_off - last_transfer_rate)
- set_last_transfer_rate(curr_off)
- set_start_time()
- except:
- pass
-
- def poll_commands(self):
- while True:
- if task_canceled: sys.exit()
- cmd_header = bytes(global_in.read(0x20, timeout=0))
- magic = cmd_header[:4]
- if magic != b'TUC0':
- continue
- cmd_type = struct.unpack(' 13:
- if is_goldleaf:
+ if is_goldleaf():
l_status.setText("\"" + cur_nsp_name[:13] + "...\"")
else:
l_nsp.setText(Language.CurrentDict[7] + ": \"" + cur_nsp_name[:13] + "...\"")
else:
- if is_goldleaf:
+ if is_goldleaf():
l_status.setText("\"" + cur_nsp_name + "\"")
else:
l_nsp.setText(Language.CurrentDict[7] + ": \"" + cur_nsp_name + "\"")
@@ -1707,7 +1191,7 @@ class UI:
set_usb_success(True)
l_switch.setText(Language.CurrentDict[11]+"!")
l_switch.setStyleSheet(GREEN)
- if not is_goldleaf:
+ if is_tinfoil():
if list_nsp.count() > 0:
btn_header.setEnabled(True)
else:
@@ -1723,7 +1207,7 @@ class UI:
except:
pass
except Exception as e:
- if is_logging:
+ if ctx.is_logging:
logging.error(e, exc_info=True)
set_usb_success(False)
UI.check_usb_success()
@@ -1732,7 +1216,7 @@ class UI:
@staticmethod
def init_language():
l_nsp.setText("")
- if not is_goldleaf:
+ if is_tinfoil():
if list_nsp.count() > 0:
l_status.setText(str(total_nsp) + " " + Language.CurrentDict[14])
else:
@@ -1786,11 +1270,11 @@ class UI:
connect_switch()
set_usb_success(True)
except Exception as e:
- if is_logging:
+ if ctx.is_logging:
logging.error(e, exc_info=True)
set_usb_success(False)
pass
- if not usb_success:
+ if not ctx.usb_success:
UI.net_radio_cmd()
net_radio.setChecked(True)
usb_radio.setVisible(False)
@@ -1815,7 +1299,8 @@ try:
l_ip = QtWidgets.QLabel(Language.CurrentDict[2]+":")
l_port = QtWidgets.QLabel("Port:")
txt_ip = QtWidgets.QLineEdit("0.0.0.0")
- tin_radio = QtWidgets.QRadioButton("Tinfoil")
+ tin_radio = QtWidgets.QRadioButton("Tinfoil/Adubbz")
+ nut_radio = QtWidgets.QRadioButton("Tinfoil/Blawar")
gold_radio = QtWidgets.QRadioButton("Goldleaf")
split_check = QtWidgets.QCheckBox("Use Split NSP")
dark_check = QtWidgets.QCheckBox(Language.CurrentDict[20])
@@ -1852,7 +1337,7 @@ try:
except:
pass
try:
- txt_ip.setText(switch_ip)
+ txt_ip.setText(ctx.switch_ip)
except:
txt_ip.setText("0.0.0.0")
pass
@@ -1866,13 +1351,21 @@ try:
combo.addItem(Language.CurrentDict[6])
combo.addItem(Language.CurrentDict[5])
combo.setCurrentIndex(1)
+
tin_radio.setChecked(True)
tin_radio.toggled.connect(UI.tin_radio_cmd)
+
+ nut_radio.setChecked(False)
+ nut_radio.toggled.connect(UI.nut_radio_cmd)
+
gold_radio.setChecked(False)
gold_radio.toggled.connect(UI.gold_radio_cmd)
+
h_group.addButton(tin_radio)
+ h_group.addButton(nut_radio)
h_group.addButton(gold_radio)
h2_box.addWidget(tin_radio)
+ h2_box.addWidget(nut_radio)
h2_box.addWidget(gold_radio)
dark_check.stateChanged.connect(UI.dark_mode_cmd)
usb_radio.setChecked(True)
@@ -1983,51 +1476,52 @@ try:
else:
set_dark_mode(0)
dark_check.setChecked(False)
-
+
+ nut.startNetwork(ctx)
# Main loop
while True:
QApplication.processEvents()
# QMessage Response
- if needresponse:
- if ignore_warning_prompt == 0:
- print("To ignore future prompts, change \'ignore_warning_prompt\' to 1 in fluffy.conf.")
- if qrespnum == 0:
+ if ctx.needresponse:
+ if ctx.ignore_warning_prompt == 0:
+ print("To ignore future prompts, change \'ctx.ignore_warning_prompt\' to 1 in fluffy.conf.")
+ if ctx.qrespnum == 0:
re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to read a file that isn't an NSP.\nLet Goldleaf read this file?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if re == QMessageBox.No:
set_response_qmessage(False)
elif re == QMessageBox.Yes:
set_response_qmessage(True)
- elif qrespnum == 1:
+ elif ctx.qrespnum == 1:
re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to write a file.\nConfirm file write?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if re == QMessageBox.No:
set_response_qmessage(False)
elif re == QMessageBox.Yes:
set_response_qmessage(True)
- elif qrespnum == 2:
+ elif ctx.qrespnum == 2:
re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to create a file.\nConfirm creation?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if re == QMessageBox.No:
set_response_qmessage(False)
elif re == QMessageBox.Yes:
set_response_qmessage(True)
- elif qrespnum == 3:
+ elif ctx.qrespnum == 3:
re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to create a directory.\nConfirm creation?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if re == QMessageBox.No:
set_response_qmessage(False)
elif re == QMessageBox.Yes:
set_response_qmessage(True)
- elif qrespnum == 4:
+ elif ctx.qrespnum == 4:
re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to delete a file.\nConfirm deletion?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if re == QMessageBox.No:
set_response_qmessage(False)
elif re == QMessageBox.Yes:
set_response_qmessage(True)
- elif qrespnum == 5:
+ elif ctx.qrespnum == 5:
re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to delete a directory.\nConfirm deletion?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if re == QMessageBox.No:
set_response_qmessage(False)
elif re == QMessageBox.Yes:
set_response_qmessage(True)
- elif qrespnum == 6:
+ elif ctx.qrespnum == 6:
re = QMessageBox.warning(window, 'Warning!', "Goldleaf wants to rename a file or directory.\nConfirm rename?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if re == QMessageBox.No:
set_response_qmessage(False)
@@ -2035,20 +1529,20 @@ try:
set_response_qmessage(True)
else:
set_response_qmessage(True)
- while haveresponse:
+ while ctx.haveresponse:
time.sleep(1)
# Check If Any Errors
if last_error != "NA":
- if not task_canceled:
+ if not ctx.task_canceled:
msg_box = QMessageBox.critical(window, 'Error', last_error, QMessageBox.Ok)
reset_last_error()
cancel_task()
# Check Log Size
- if is_logging:
- if os.path.isfile(initial_dir + 'fluffy.log'):
- if os.path.getsize(initial_dir + 'fluffy.log') > 250000:
+ if ctx.is_logging:
+ if os.path.isfile(ctx.initial_dir + 'fluffy.log'):
+ if os.path.getsize(ctx.initial_dir + 'fluffy.log') > 250000:
logging.debug("Error: Log size reached, turning off logging.")
turn_off_logging()
@@ -2059,7 +1553,7 @@ try:
# Save config and close
if not window.isVisible():
try:
- switch_ip = txt_ip.text()
+ ctx.switch_ip = txt_ip.text()
except:
pass
save_config()
@@ -2068,11 +1562,11 @@ try:
# Switch Indicator
- if not is_installing and not is_network and usb_success and not sent_header:
+ if not ctx.is_installing and not is_network and ctx.usb_success and not sent_header:
UI.set_switch_text()
# Tinfoil Network Mode
- if not sent_header and not is_installing and is_network:
+ if not sent_header and not ctx.is_installing and is_network:
l_switch.setText(Language.CurrentDict[12])
l_switch.setStyleSheet(BLUE)
if list_nsp.count() > 0:
@@ -2086,7 +1580,7 @@ try:
if is_done:
UI.set_done_text()
else:
- if is_installing:
+ if ctx.is_installing:
UI.set_progress_text()
else:
l_status.setText(Language.CurrentDict[25])
@@ -2101,7 +1595,7 @@ try:
btn_header.setText(Language.CurrentDict[16])
# Installation in progress disable cancel
- #if sent_header and is_installing and not is_done:
+ #if sent_header and ctx.is_installing and not is_done:
#btn_header.setEnabled(False)
# Goldleaf & Tinfoil USB Mode
@@ -2110,7 +1604,7 @@ try:
if is_done:
UI.set_done_text()
else:
- if is_installing:
+ if ctx.is_installing:
UI.set_progress_text()
else:
UI.set_loading_text()
@@ -2118,7 +1612,8 @@ try:
pass
except Exception as e:
- if is_logging:
- logging.error(e, exc_info=True)
+ #if ctx.is_logging:
+ # logging.error(e, exc_info=True)
save_config()
+ raise
sys.exit()
diff --git a/goldleaf.py b/goldleaf.py
new file mode 100644
index 0000000..d88e8bf
--- /dev/null
+++ b/goldleaf.py
@@ -0,0 +1,338 @@
+# Goldleaf
+class GoldleafCommandId:
+ ListSystemDrives = 0
+ GetEnvironmentPaths = 1
+ GetPathType = 2
+ ListDirectories = 3
+ ListFiles = 4
+ GetFileSize = 5
+ FileRead = 6
+ FileWrite = 7
+ CreateFile = 8
+ CreateDirectory = 9
+ DeleteFile = 10
+ DeleteDirectory = 11
+ RenameFile = 12
+ RenameDirectory = 13
+ GetDriveTotalSpace = 14
+ GetDriveFreeSpace = 15
+ GetNSPContents = 16
+ Max = 17
+
+class GoldleafCommandReadResult:
+ Success = 0
+ InvalidMagic = 1
+ InvalidGoldleafCommandId = 2
+
+class Goldleaf:
+ GLUC = b"GLUC"
+ magic = b"GLUC"
+ cmd_id = 0
+ drives = {}
+ FW_DENIED = 0
+ FW_ACCEPTED = 1
+ FW_NOSTATUS = 2
+ fw_status = FW_NOSTATUS
+
+ def __init__(self, ctx):
+ self.ctx = ctx
+
+ def init(self):
+ try:
+ detach_switch()
+ connect_switch()
+ self.process()
+ except Exception as e:
+ if is_logging:
+ logging.error(e, exc_info=True)
+ throw_error(0)
+ sys.exit()
+
+ def write(self,buffer):
+ try:
+ self.ctx.global_out.write(buffer,timeout=3000)
+ except:
+ pass
+
+ def read(self,length):
+ return self.ctx.global_in.read(length,timeout=0).tobytes()
+
+ def write_u32(self,x):
+ try:
+ self.ctx.global_out.write(struct.pack(">= 1
+ else:
+ self.drives["ROOT"] = "/"
+ self.write_u32(len(self.drives))
+ for d in self.drives:
+ try:
+ self.write_string(drive_labels[d])
+ except KeyError:
+ self.write_string(d)
+ self.write_string(d)
+ elif self.is_id(GoldleafCommandId.GetEnvironmentPaths):
+ env_paths = {x:os.path.expanduser("~/"+x) for x in ["Desktop", "Documents"]}
+ for arg in sys.argv[1:]:
+ folder = os.path.abspath(arg)
+ if os.path.isfile(folder):
+ folder = os.path.dirname(folder)
+ env_paths[os.path.basename(folder)] = folder
+ env_paths = {x:env_paths[x] for x in env_paths if os.path.exists(env_paths[x])}
+ self.write_u32(len(env_paths))
+ for env in env_paths:
+ env_paths[env] = env_paths[env].replace("\\", "/")
+ self.write_string(env)
+ if env_paths[env][1:3] != ":/":
+ env_paths[env] = "ROOT:" + env_paths[env]
+ self.write_string(env_paths[env])
+ elif self.is_id(GoldleafCommandId.GetPathType):
+ ptype = 0
+ path = self.read_path()
+ if os.path.isfile(path):
+ ptype = 1
+ elif os.path.isdir(path):
+ ptype = 2
+ self.write_u32(ptype)
+ elif self.is_id(GoldleafCommandId.ListDirectories):
+ path = self.read_path()
+ ents = [x for x in os.listdir(path) if os.path.isdir(os.path.join(path, x))]
+ n_ents = []
+ for e in ents:
+ try:
+ test = os.listdir(os.path.join(path, e))
+ n_ents.append(e)
+ except:
+ pass
+ self.write_u32(len(n_ents))
+ for name in n_ents:
+ self.write_string(name)
+ elif self.is_id(GoldleafCommandId.ListFiles):
+ self.fw_status = self.FW_NOSTATUS
+ if is_installing:
+ complete_goldleaf_transfer()
+ path = self.read_path()
+ ents = [x for x in os.listdir(path) if os.path.isfile(os.path.join(path, x))]
+ if not allow_access_non_nsp:
+ len_nsps = 0
+ for f in ents:
+ if f.lower().endswith('.nsp'):
+ len_nsps = len_nsps+1
+ self.write_u32(len_nsps)
+ for name in ents:
+ if name.lower().endswith('.nsp'):
+ self.write_string(name)
+ else:
+ self.write_u32(len(ents))
+ for name in ents:
+ self.write_string(name)
+ elif self.is_id(GoldleafCommandId.GetFileSize):
+ path = self.read_path()
+ self.write_u64(os.path.getsize(path))
+ elif self.is_id(GoldleafCommandId.FileRead):
+ can_read = True
+ offset = self.read_u64()
+ size = self.read_u64()
+ path = self.read_path()
+ if not os.path.basename(path).lower().endswith('.nsp'):
+ if allow_access_non_nsp:
+ can_read = True
+ else:
+ can_read = False
+ if can_read:
+ with open(path, "rb") as f:
+ f.seek(offset)
+ data = f.read(size)
+ self.write_u64(len(data))
+ self.write(data)
+ try:
+ if self.fw_status != self.FW_DENIED:
+ complete_loading()
+ set_cur_nsp(str(os.path.basename(path)))
+ set_progress(int(offset), int(os.path.getsize(path)))
+ elapsed_time = time.time() - start_time
+ if elapsed_time >= 1:
+ set_cur_transfer_rate(int(offset) - last_transfer_rate)
+ set_last_transfer_rate(int(offset))
+ set_start_time()
+ else:
+ complete_goldleaf_transfer()
+ except:
+ pass
+ else:
+ logging.debug("Error: Access denied. \nReason: Goldleaf tried to access a non .NSP file(to bypass this default restriction, change \'allow_access_non_nsp\' to 1 in fluffy.conf).")
+ print("Error: Access denied. \nReason: Goldleaf tried to access a non .NSP file(to bypass this default restriction, change \'allow_access_non_nsp\' to 1 in fluffy.conf).")
+ cancel_task()
+ sys.exit()
+ elif self.is_id(GoldleafCommandId.FileWrite):
+ offset = self.read_u64()
+ size = self.read_u64()
+ path = self.read_path()
+ data = self.read(size)
+ can_write = False
+ if self.fw_status == self.FW_NOSTATUS:
+ get_response_qmessage(1)
+ while not haveresponse and global_dev is not None:
+ time.sleep(1)
+ if qresponse:
+ self.fw_status = self.FW_ACCEPTED
+ can_write = True
+ else:
+ self.fw_status = self.FW_DENIED
+ elif self.fw_status == self.FW_ACCEPTED:
+ can_write = True
+ if can_write:
+ cont = bytearray()
+ try:
+ with open(path, "rb") as f:
+ cont=bytearray(f.read())
+ except FileNotFoundError:
+ pass
+ cont[offset:offset + size] = data
+ with open(path, "wb") as f:
+ f.write(cont)
+ reset_response()
+ elif self.is_id(GoldleafCommandId.CreateFile):
+ path = self.read_path()
+ get_response_qmessage(2)
+ while not haveresponse and global_dev is not None:
+ time.sleep(1)
+ if qresponse:
+ open(path, "a").close()
+ reset_response()
+ elif self.is_id(GoldleafCommandId.CreateDirectory):
+ path = self.read_path()
+ get_response_qmessage(3)
+ while not haveresponse and global_dev is not None:
+ time.sleep(1)
+ if qresponse:
+ try:
+ os.mkdir(path)
+ except os.FileExistsError:
+ pass
+ reset_response()
+ elif self.is_id(GoldleafCommandId.DeleteFile):
+ path = self.read_path()
+ get_response_qmessage(4)
+ while not haveresponse and global_dev is not None:
+ time.sleep(1)
+ if qresponse:
+ os.remove(path)
+ reset_response()
+ elif self.is_id(GoldleafCommandId.DeleteDirectory):
+ path = self.read_path()
+ get_response_qmessage(5)
+ while not haveresponse and global_dev is not None:
+ time.sleep(1)
+ if qresponse:
+ shutil.rmtree(path)
+ reset_response()
+ elif self.is_id(GoldleafCommandId.RenameFile):
+ path = self.read_path()
+ new_name = self.read_string()
+ get_response_qmessage(6)
+ while not haveresponse and global_dev is not None:
+ time.sleep(1)
+ if qresponse:
+ os.rename(path, new_name)
+ reset_response()
+ elif self.is_id(GoldleafCommandId.RenameDirectory):
+ path = self.read_path()
+ new_name = self.read_path()
+ get_response_qmessage(6)
+ while not haveresponse and global_dev is not None:
+ time.sleep(1)
+ if qresponse:
+ os.rename(path, new_name)
+ reset_response()
+ elif self.is_id(GoldleafCommandId.GetDriveTotalSpace):
+ path = self.read_path()
+ disk = os.statvfs(path)
+ totalBytes = float(disk.f_bsize*disk.f_blocks)
+ self.write_u64(int(totalspace))
+ elif self.is_id(GoldleafCommandId.GetDriveFreeSpace):
+ path = self.read_path()
+ disk = os.statvfs(path)
+ totalFreeSpace = float(disk.f_bsize*disk.f_bfree)
+ self.write_u64(int(totalFreeSpace))
+ sys.exit()
\ No newline at end of file
diff --git a/nut.py b/nut.py
new file mode 100644
index 0000000..cb31342
--- /dev/null
+++ b/nut.py
@@ -0,0 +1,136 @@
+import usb.core
+import usb.util
+import struct
+import sys
+from binascii import hexlify as hx, unhexlify as uhx
+from pathlib import Path
+import Server
+import Server.Controller.Api
+import time
+from urllib.parse import urlparse
+from urllib.parse import parse_qs
+import Server.Controller.Api
+import threading
+
+global status
+status = 'initializing'
+
+def getFiles():
+ for k, t in Titles.items():
+ f = t.getLatestFile()
+ if f and f.hasValidTicket:
+ o.append({'id': t.id, 'name': t.name, 'version': int(f.version) if f.version else None , 'size': f.getFileSize(), 'mtime': f.getFileModified() })
+
+ return json.dumps(o)
+
+class UsbResponse(Server.NutResponse):
+ def __init__(self, packet):
+ super(UsbResponse, self).__init__(None)
+ self.packet = packet
+
+ def sendHeader(self):
+ pass
+
+ def write(self, data):
+ print('usbresponse write')
+ if self.bytesSent == 0 and not self.headersSent:
+ self.sendHeader()
+
+ if type(data) == str:
+ data = data.encode('utf-8')
+
+ self.bytesSent += len(data)
+ self.packet.payload = data
+ self.packet.send()
+
+ self.bytesSent += len(data)
+
+
+class UsbRequest(Server.NutRequest):
+ def __init__(self, url):
+ self.headers = {}
+ self.path = url
+ self.head = False
+ self.url = urlparse(self.path)
+
+ print('url ' + self.path);
+
+ self.bits = [x for x in self.url.path.split('/') if x]
+ print(self.bits)
+ self.query = parse_qs(self.url.query)
+
+ try:
+ for k,v in self.query.items():
+ self.query[k] = v[0];
+ except:
+ pass
+
+ self.user = None
+
+class Packet:
+ def __init__(self, i, o):
+ self.size = 0
+ self.payload = b''
+ self.command = 0
+ self.threadId = 0
+ self.packetIndex = 0
+ self.packetCount = 0
+ self.timestamp = 0
+ self.i = i
+ self.o = o
+
+ def recv(self, timeout = 60000):
+ print('begin recv')
+ header = bytes(self.i.read(32, timeout=timeout))
+ print('read complete')
+ magic = header[:4]
+ self.command = int.from_bytes(header[4:8], byteorder='little')
+ self.size = int.from_bytes(header[8:16], byteorder='little')
+ self.threadId = int.from_bytes(header[16:20], byteorder='little')
+ self.packetIndex = int.from_bytes(header[20:22], byteorder='little')
+ self.packetCount = int.from_bytes(header[22:24], byteorder='little')
+ self.timestamp = int.from_bytes(header[24:32], byteorder='little')
+
+ if magic != b'\x12\x12\x12\x12':
+ print('invalid magic! ' + str(magic));
+ return False
+
+ print('receiving %d bytes' % self.size)
+ self.payload = bytes(self.i.read(self.size, timeout=0))
+ return True
+
+ def send(self, timeout = 60000):
+ print('sending %d bytes' % len(self.payload))
+ self.o.write(b'\x12\x12\x12\x12', timeout=timeout)
+ self.o.write(struct.pack('= size:
+ self.send_error(416, self.responses.get(416)[0])
+ if end == "":
+ end = size - 1
+ else:
+ try:
+ end = int(end)
+ except ValueError as e:
+ self.send_error(400, 'invalid range')
+
+ start = max(start, 0)
+ end = min(end, size - 1)
+ self.range = (start, end)
+ cont_length = end - start + 1
+ if 'Range' in self.headers:
+ self.send_response(206)
+ else:
+ self.send_response(200)
+ self.send_header('Content-type', ctype)
+ self.send_header('Accept-Ranges', 'bytes')
+ self.send_header('Content-Range','bytes %s-%s/%s' % (start, end, size))
+ self.send_header('Content-Length', str(cont_length))
+ self.send_header('Last-Modified', self.date_time_string(fs.st_mtime))
+ self.end_headers()
+ return f
+
+ def copyfile(self, infile, outfile):
+ if 'Range' not in self.headers:
+ SimpleHTTPRequestHandler.copyfile(self, infile, outfile)
+ return
+ complete_loading()
+ set_cur_nsp(str(os.path.basename(infile.name)))
+ start, end = self.range
+ infile.seek(start)
+ bufsize = 64 * 1024 # 64KB
+ while True:
+ if task_canceled: sys.exit()
+ buf = infile.read(bufsize)
+ if not buf:
+ break
+ try:
+ outfile.write(buf)
+ try:
+ set_progress(int(infile.tell()), int(end))
+ elapsed_time = time.time() - start_time
+ if elapsed_time >= 1:
+ set_cur_transfer_rate(int(infile.tell()) - last_transfer_rate)
+ set_last_transfer_rate(int(infile.tell()))
+ set_start_time()
+ except:
+ pass
+ except BrokenPipeError:
+ pass
+class TinfoilServer(TCPServer):
+ stopped = False
+ def server_bind(self):
+ import socket
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.socket.bind(self.server_address)
+ def serve_forever(self):
+ while not self.stopped:
+ if task_canceled: sys.exit()
+ self.handle_request()
+ sys.exit()
+ def force_stop(self):
+ self.server_close()
+ self.stopped = True
+ sys.exit()
+
+
+# Tinfoil USB
+class Usb:
+ CMD_ID_EXIT = 0
+ CMD_ID_FILE_RANGE = 1
+ CMD_TYPE_RESPONSE = 1
+
+ def __init__(self, ctx):
+ self.ctx = ctx
+
+ def init(self):
+ try:
+ detach_switch()
+ connect_switch()
+ self.send_nsp_list()
+ self.process()
+ complete_install()
+ sys.exit()
+ except Exception as e:
+ if is_logging:
+ logging.error(e, exc_info=True)
+ throw_error(2)
+ sys.exit()
+
+ def send_response_header(self, cmd_id, data_size):
+ self.ctx.global_out.write(b'TUC0')
+ self.ctx.global_out.write(struct.pack('= end_off:
+ read_size = end_off - curr_off
+ try:
+ set_progress(int(end_off), int(end_off))
+ except:
+ pass
+ buf = f.read(read_size)
+ self.ctx.global_out.write(data=buf, timeout=0)
+ curr_off += read_size
+ try:
+ set_progress(int(curr_off), int(end_off))
+ elapsed_time = time.time() - start_time
+ if elapsed_time >= 1:
+ set_cur_transfer_rate(curr_off - last_transfer_rate)
+ set_last_transfer_rate(curr_off)
+ set_start_time()
+ except:
+ pass
+
+ def process(self):
+ while True:
+ if task_canceled: sys.exit()
+ cmd_header = bytes(self.ctx.global_in.read(0x20, timeout=0))
+ magic = cmd_header[:4]
+ if magic != b'TUC0':
+ continue
+ cmd_type = struct.unpack('