mirror of
https://github.com/Maschell/JNUSLib.git
synced 2024-11-26 01:44:17 +01:00
- The NUSTitleLoaders now don't inherit anymore from a class, but use a static function from NUSTitleLoader. The NUSDataProvider is supplier by a Supplier argument. This simplifies the config.
- Remove the global common key. Now the common key needs to be provided to every loading instance. This way loading with different common keys is much easier (and more robust)
This commit is contained in:
parent
d08a42719a
commit
651e32e7ba
@ -1,5 +1,5 @@
|
|||||||
/****************************************************************************
|
/****************************************************************************
|
||||||
* Copyright (C) 2016-2018 Maschell
|
* Copyright (C) 2016-2019 Maschell
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -17,23 +17,11 @@
|
|||||||
package de.mas.wiiu.jnus;
|
package de.mas.wiiu.jnus;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.Ticket;
|
import de.mas.wiiu.jnus.entities.Ticket;
|
||||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
|
|
||||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDGIPartitionTitle;
|
|
||||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDGamePartition;
|
|
||||||
import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfo;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class NUSTitleConfig {
|
public class NUSTitleConfig {
|
||||||
private String inputPath;
|
|
||||||
private WUDGamePartition WUDGamePartition = null;
|
|
||||||
private WUDGIPartitionTitle WUDGIPartitionTitle = null;
|
|
||||||
private WUDInfo WUDInfo;
|
|
||||||
private Ticket ticket;
|
private Ticket ticket;
|
||||||
|
|
||||||
private int version = Settings.LATEST_TMD_VERSION;
|
|
||||||
private long titleID = 0x0L;
|
|
||||||
|
|
||||||
private WoomyInfo woomyInfo;
|
|
||||||
private boolean noDecryption;
|
private boolean noDecryption;
|
||||||
|
private byte[] commonKey;
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,13 @@
|
|||||||
****************************************************************************/
|
****************************************************************************/
|
||||||
package de.mas.wiiu.jnus;
|
package de.mas.wiiu.jnus;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.TMD;
|
import de.mas.wiiu.jnus.entities.TMD;
|
||||||
import de.mas.wiiu.jnus.entities.Ticket;
|
import de.mas.wiiu.jnus.entities.Ticket;
|
||||||
@ -25,19 +30,19 @@ import de.mas.wiiu.jnus.entities.content.Content;
|
|||||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
import de.mas.wiiu.jnus.entities.fst.FST;
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
||||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||||
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
import de.mas.wiiu.jnus.utils.cryptography.AESDecryption;
|
import de.mas.wiiu.jnus.utils.cryptography.AESDecryption;
|
||||||
import lombok.extern.java.Log;
|
|
||||||
|
|
||||||
@Log
|
public class NUSTitleLoader {
|
||||||
abstract class NUSTitleLoader {
|
private NUSTitleLoader() {
|
||||||
protected NUSTitleLoader() {
|
|
||||||
// should be empty
|
// should be empty
|
||||||
}
|
}
|
||||||
|
|
||||||
public NUSTitle loadNusTitle(NUSTitleConfig config) throws Exception {
|
public static NUSTitle loadNusTitle(NUSTitleConfig config, Supplier<NUSDataProvider> dataProviderFunction)
|
||||||
|
throws IOException, ParseException {
|
||||||
NUSTitle result = new NUSTitle();
|
NUSTitle result = new NUSTitle();
|
||||||
|
|
||||||
NUSDataProvider dataProvider = getDataProvider(result, config);
|
NUSDataProvider dataProvider = dataProviderFunction.get();
|
||||||
result.setDataProvider(dataProvider);
|
result.setDataProvider(dataProvider);
|
||||||
|
|
||||||
byte[] tmdData = dataProvider.getRawTMD().orElseThrow(() -> new ParseException("No TMD data found", 0));
|
byte[] tmdData = dataProvider.getRawTMD().orElseThrow(() -> new ParseException("No TMD data found", 0));
|
||||||
@ -79,6 +84,4 @@ abstract class NUSTitleLoader {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config);
|
|
||||||
}
|
}
|
||||||
|
@ -16,35 +16,34 @@
|
|||||||
****************************************************************************/
|
****************************************************************************/
|
||||||
package de.mas.wiiu.jnus;
|
package de.mas.wiiu.jnus;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.ParseException;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.Ticket;
|
import de.mas.wiiu.jnus.entities.Ticket;
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocal;
|
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocal;
|
||||||
|
|
||||||
public final class NUSTitleLoaderLocal extends NUSTitleLoader {
|
public final class NUSTitleLoaderLocal {
|
||||||
|
|
||||||
private NUSTitleLoaderLocal() {
|
public static NUSTitle loadNUSTitle(String inputPath, byte[] commonKey) throws Exception {
|
||||||
super();
|
return loadNUSTitle(inputPath, null, commonKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(String inputPath) throws Exception {
|
public static NUSTitle loadNUSTitle(String inputPath, Ticket ticket) throws IOException, ParseException {
|
||||||
return loadNUSTitle(inputPath, null);
|
return loadNUSTitle(inputPath, ticket, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(String inputPath, Ticket ticket) throws Exception {
|
public static NUSTitle loadNUSTitle(String inputPath, Ticket ticket, byte[] commonKey) throws IOException, ParseException {
|
||||||
NUSTitleLoader loader = new NUSTitleLoaderLocal();
|
|
||||||
NUSTitleConfig config = new NUSTitleConfig();
|
NUSTitleConfig config = new NUSTitleConfig();
|
||||||
|
|
||||||
|
config.setCommonKey(commonKey);
|
||||||
|
|
||||||
if (ticket != null) {
|
if (ticket != null) {
|
||||||
config.setTicket(ticket);
|
config.setTicket(ticket);
|
||||||
}
|
} else if (commonKey == null) {
|
||||||
config.setInputPath(inputPath);
|
throw new IOException("Ticket was null and no commonKey was given");
|
||||||
|
|
||||||
return loader.loadNusTitle(config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocal(inputPath));
|
||||||
protected NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config) {
|
|
||||||
return new NUSDataProviderLocal(title, config.getInputPath());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,25 +18,14 @@
|
|||||||
package de.mas.wiiu.jnus;
|
package de.mas.wiiu.jnus;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.Ticket;
|
import de.mas.wiiu.jnus.entities.Ticket;
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocalBackup;
|
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocalBackup;
|
||||||
|
|
||||||
public final class NUSTitleLoaderLocalBackup extends NUSTitleLoader {
|
public final class NUSTitleLoaderLocalBackup {
|
||||||
|
|
||||||
private NUSTitleLoaderLocalBackup() {
|
private NUSTitleLoaderLocalBackup() {
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(String inputPath) throws Exception {
|
|
||||||
return loadNUSTitle(inputPath, (short) Settings.LATEST_TMD_VERSION);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(String inputPath, short titleVersion) throws Exception {
|
|
||||||
return loadNUSTitle(inputPath, titleVersion, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(String inputPath, short titleVersion, Ticket ticket) throws Exception {
|
public static NUSTitle loadNUSTitle(String inputPath, short titleVersion, Ticket ticket) throws Exception {
|
||||||
NUSTitleLoader loader = new NUSTitleLoaderLocalBackup();
|
|
||||||
NUSTitleConfig config = new NUSTitleConfig();
|
NUSTitleConfig config = new NUSTitleConfig();
|
||||||
|
|
||||||
if (ticket != null) {
|
if (ticket != null) {
|
||||||
@ -45,15 +34,7 @@ public final class NUSTitleLoaderLocalBackup extends NUSTitleLoader {
|
|||||||
config.setNoDecryption(true);
|
config.setNoDecryption(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.setVersion(titleVersion);
|
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocalBackup(inputPath, titleVersion));
|
||||||
config.setInputPath(inputPath);
|
|
||||||
|
|
||||||
return loader.loadNusTitle(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config) {
|
|
||||||
return new NUSDataProviderLocalBackup(title, config.getInputPath(), (short) config.getVersion());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,22 +16,23 @@
|
|||||||
****************************************************************************/
|
****************************************************************************/
|
||||||
package de.mas.wiiu.jnus;
|
package de.mas.wiiu.jnus;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.ParseException;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.entities.Ticket;
|
import de.mas.wiiu.jnus.entities.Ticket;
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote;
|
import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote;
|
||||||
|
|
||||||
public final class NUSTitleLoaderRemote extends NUSTitleLoader {
|
public final class NUSTitleLoaderRemote {
|
||||||
|
|
||||||
private NUSTitleLoaderRemote() {
|
private NUSTitleLoaderRemote() {
|
||||||
super();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(long titleID) throws Exception {
|
public static NUSTitle loadNUSTitle(long titleID, byte[] commonKey) throws Exception {
|
||||||
return loadNUSTitle(titleID, Settings.LATEST_TMD_VERSION, null);
|
return loadNUSTitle(titleID, Settings.LATEST_TMD_VERSION, commonKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(long titleID, int version) throws Exception {
|
public static NUSTitle loadNUSTitle(long titleID, int version, byte[] commonKey) throws Exception {
|
||||||
return loadNUSTitle(titleID, version, null);
|
return loadNUSTitle(titleID, version, null, false, commonKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(long titleID, Ticket ticket) throws Exception {
|
public static NUSTitle loadNUSTitle(long titleID, Ticket ticket) throws Exception {
|
||||||
@ -39,24 +40,20 @@ public final class NUSTitleLoaderRemote extends NUSTitleLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(long titleID, int version, Ticket ticket) throws Exception {
|
public static NUSTitle loadNUSTitle(long titleID, int version, Ticket ticket) throws Exception {
|
||||||
return loadNUSTitle(titleID, version, ticket, false);
|
return loadNUSTitle(titleID, version, ticket, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(long titleID, int version, Ticket ticket, boolean noEncryption) throws Exception {
|
public static NUSTitle loadNUSTitle(long titleID, int version, Ticket ticket, boolean noEncryption, byte[] commonKey) throws IOException, ParseException {
|
||||||
NUSTitleLoader loader = new NUSTitleLoaderRemote();
|
|
||||||
NUSTitleConfig config = new NUSTitleConfig();
|
NUSTitleConfig config = new NUSTitleConfig();
|
||||||
|
|
||||||
config.setVersion(version);
|
|
||||||
config.setTitleID(titleID);
|
|
||||||
config.setTicket(ticket);
|
config.setTicket(ticket);
|
||||||
config.setNoDecryption(noEncryption);
|
config.setNoDecryption(noEncryption);
|
||||||
|
config.setCommonKey(commonKey);
|
||||||
return loader.loadNusTitle(config);
|
if (ticket == null && !noEncryption && commonKey == null) {
|
||||||
|
throw new IOException("Ticket was null and no commonKey was given");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderRemote(version, titleID));
|
||||||
protected NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config) {
|
|
||||||
return new NUSDataProviderRemote(title, config.getVersion(), config.getTitleID());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/****************************************************************************
|
/****************************************************************************
|
||||||
* Copyright (C) 2016-2018 Maschell
|
* Copyright (C) 2016-2019 Maschell
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -17,32 +17,29 @@
|
|||||||
package de.mas.wiiu.jnus;
|
package de.mas.wiiu.jnus;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.ParseException;
|
||||||
|
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProvider;
|
|
||||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderWoomy;
|
import de.mas.wiiu.jnus.implementations.NUSDataProviderWoomy;
|
||||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
|
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
|
||||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyParser;
|
import de.mas.wiiu.jnus.implementations.woomy.WoomyParser;
|
||||||
import lombok.extern.java.Log;
|
|
||||||
|
|
||||||
@Log
|
public final class NUSTitleLoaderWoomy {
|
||||||
public final class NUSTitleLoaderWoomy extends NUSTitleLoader {
|
|
||||||
|
|
||||||
public static NUSTitle loadNUSTitle(String inputFile) throws Exception {
|
private NUSTitleLoaderWoomy() {
|
||||||
NUSTitleLoaderWoomy loader = new NUSTitleLoaderWoomy();
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NUSTitle loadNUSTitle(String inputFile) throws IOException, ParserConfigurationException, SAXException, ParseException {
|
||||||
NUSTitleConfig config = new NUSTitleConfig();
|
NUSTitleConfig config = new NUSTitleConfig();
|
||||||
|
|
||||||
WoomyInfo woomyInfo = WoomyParser.createWoomyInfo(new File(inputFile));
|
WoomyInfo woomyInfo = WoomyParser.createWoomyInfo(new File(inputFile));
|
||||||
if (woomyInfo == null) {
|
|
||||||
log.info("Created woomy is null.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
config.setWoomyInfo(woomyInfo);
|
|
||||||
return loader.loadNusTitle(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWoomy(woomyInfo));
|
||||||
protected NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config) {
|
|
||||||
return new NUSDataProviderWoomy(title, config.getWoomyInfo());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,5 @@ public class Settings {
|
|||||||
public static final String USER_AGENT = "Mozilla/5.0 (Nintendo WiiU) AppleWebKit/536.28 (KHTML, like Gecko) NX/3.0.3.12.12 NintendoBrowser/3.0.0.9561.US";
|
public static final String USER_AGENT = "Mozilla/5.0 (Nintendo WiiU) AppleWebKit/536.28 (KHTML, like Gecko) NX/3.0.3.12.12 NintendoBrowser/3.0.0.9561.US";
|
||||||
public static final boolean ALLOW_PARALLELISATION = true;
|
public static final boolean ALLOW_PARALLELISATION = true;
|
||||||
|
|
||||||
public static byte[] commonKey = new byte[0x10];
|
|
||||||
public static int WIIU_DECRYPTED_AREA_OFFSET = 0x18000;
|
public static int WIIU_DECRYPTED_AREA_OFFSET = 0x18000;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ import java.nio.ByteBuffer;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import de.mas.wiiu.jnus.Settings;
|
|
||||||
import de.mas.wiiu.jnus.utils.Utils;
|
import de.mas.wiiu.jnus.utils.Utils;
|
||||||
import de.mas.wiiu.jnus.utils.cryptography.AESDecryption;
|
import de.mas.wiiu.jnus.utils.cryptography.AESDecryption;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -44,17 +43,17 @@ public final class Ticket {
|
|||||||
this.IV = IV;
|
this.IV = IV;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Ticket parseTicket(File ticket) throws IOException {
|
public static Ticket parseTicket(File ticket, byte[] commonKey) throws IOException {
|
||||||
if (ticket == null || !ticket.exists()) {
|
if (ticket == null || !ticket.exists()) {
|
||||||
log.warning("Ticket input file null or doesn't exist.");
|
log.warning("Ticket input file null or doesn't exist.");
|
||||||
return null;
|
throw new IOException("Ticket input file null or doesn't exist.");
|
||||||
}
|
}
|
||||||
return parseTicket(Files.readAllBytes(ticket.toPath()));
|
return parseTicket(Files.readAllBytes(ticket.toPath()), commonKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Ticket parseTicket(byte[] ticket) throws IOException {
|
public static Ticket parseTicket(byte[] ticket, byte[] commonKey) throws IOException {
|
||||||
if (ticket == null) {
|
if (ticket == null) {
|
||||||
return null;
|
throw new IOException("Ticket input file null or doesn't exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(ticket.length);
|
ByteBuffer buffer = ByteBuffer.allocate(ticket.length);
|
||||||
@ -69,20 +68,20 @@ public final class Ticket {
|
|||||||
buffer.position(POSITION_TITLEID);
|
buffer.position(POSITION_TITLEID);
|
||||||
long titleID = buffer.getLong();
|
long titleID = buffer.getLong();
|
||||||
|
|
||||||
Ticket result = createTicket(encryptedKey, titleID);
|
Ticket result = createTicket(encryptedKey, titleID, commonKey);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Ticket createTicket(byte[] encryptedKey, long titleID) {
|
public static Ticket createTicket(byte[] encryptedKey, long titleID, byte[] commonKey) {
|
||||||
byte[] IV = ByteBuffer.allocate(0x10).putLong(titleID).array();
|
byte[] IV = ByteBuffer.allocate(0x10).putLong(titleID).array();
|
||||||
byte[] decryptedKey = calculateDecryptedKey(encryptedKey, IV);
|
byte[] decryptedKey = calculateDecryptedKey(encryptedKey, IV, commonKey);
|
||||||
|
|
||||||
return new Ticket(encryptedKey, decryptedKey, IV);
|
return new Ticket(encryptedKey, decryptedKey, IV);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] calculateDecryptedKey(byte[] encryptedKey, byte[] IV) {
|
private static byte[] calculateDecryptedKey(byte[] encryptedKey, byte[] IV, byte[] commonKey) {
|
||||||
AESDecryption decryption = new AESDecryption(Settings.commonKey, IV);
|
AESDecryption decryption = new AESDecryption(commonKey, IV);
|
||||||
return decryption.decrypt(encryptedKey);
|
return decryption.decrypt(encryptedKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,4 +106,5 @@ public final class Ticket {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return "Ticket [encryptedKey=" + Utils.ByteArrayToString(encryptedKey) + ", decryptedKey=" + Utils.ByteArrayToString(decryptedKey) + "]";
|
return "Ticket [encryptedKey=" + Utils.ByteArrayToString(encryptedKey) + ", decryptedKey=" + Utils.ByteArrayToString(decryptedKey) + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user