updated portal

This commit is contained in:
CaramelKat
2020-04-25 00:00:42 -05:00
committed by jay.poff@outlook.com
commit dcd3c3c445
21 changed files with 1664 additions and 0 deletions

0
src/api/index.js Normal file
View File

13
src/config.json Normal file
View File

@@ -0,0 +1,13 @@
{
"http": {
"port": 8080
},
"mongoose": {
"uri": "mongodb://localhost:27017",
"database": "Miiverse",
"options": {
"useNewUrlParser": true,
"useUnifiedTopology": true
}
}
}

66
src/database.js Normal file
View File

@@ -0,0 +1,66 @@
const mongoose = require('mongoose');
const { mongoose: mongooseConfig } = require('./config.json');
const { TOPIC } = require('./models/topic');
const { ENDPOINT } = require('./models/endpoint');
const { uri, database, options } = mongooseConfig;
let connection;
async function connect() {
await mongoose.connect(`${uri}/${database}`, options);
connection = mongoose.connection;
connection.on('error', console.error.bind(console, 'connection error:'));
}
function verifyConnected() {
if (!connection) {
throw new Error('Cannot make database requets without being connected');
}
}
async function getTopicByName(topicName) {
verifyConnected();
if (typeof topicName !== 'string') {
return null;
}
return TOPIC.findOne({
name: topicName
});
}
async function getTopicByCommunityID(communityID) {
verifyConnected();
if (typeof communityID !== 'string') {
return null;
}
return TOPIC.findOne({
community_id: communityID
});
}
async function getDiscoveryHosts() {
verifyConnected();
return ENDPOINT.findOne({
version: 1
});
}
async function getServerConfig() {
verifyConnected();
return ENDPOINT.findOne({
type: "config"
});
}
module.exports = {
connect,
getTopicByName,
getTopicByCommunityID,
getDiscoveryHosts,
getServerConfig
};

52
src/logger.js Normal file
View File

@@ -0,0 +1,52 @@
const fs = require('fs-extra');
require('colors');
const root = __dirname;
fs.ensureDirSync(`${root}/logs`);
const streams = {
latest: fs.createWriteStream(`${root}/logs/latest.log`),
success: fs.createWriteStream(`${root}/logs/success.log`),
error: fs.createWriteStream(`${root}/logs/error.log`),
warn: fs.createWriteStream(`${root}/logs/warn.log`),
info: fs.createWriteStream(`${root}/logs/info.log`)
};
function success(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
streams.success.write(`${input}\n`);
console.log(`${input}`.green.bold);
}
function error(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
streams.error.write(`${input}\n`);
console.log(`${input}`.red.bold);
}
function warn(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
streams.warn.write(`${input}\n`);
console.log(`${input}`.yellow.bold);
}
function info(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
streams.info.write(`${input}\n`);
console.log(`${input}`.cyan.bold);
}
module.exports = {
success,
error,
warn,
info
};

View File

@@ -0,0 +1,38 @@
const xmlbuilder = require('xmlbuilder');
const VALID_CLIENT_ID_SECRET_PAIRS = {
// 'Key' is the client ID, 'Value' is the client secret
'a2efa818a34fa16b8afbc8a74eba3eda': 'c91cdb5658bd4954ade78533a339cf9a', // Possibly WiiU exclusive?
'daf6227853bcbdce3d75baee8332b': '3eff548eac636e2bf45bb7b375e7b6b0', // Possibly 3DS exclusive?
'ea25c66c26b403376b4c5ed94ab9cdea': 'd137be62cb6a2b831cad8c013b92fb55', // Possibly 3DS exclusive?
};
function nintendoClientHeaderCheck(request, response, next) {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
const {headers} = request;
if (
!headers['x-nintendo-client-id'] ||
!headers['x-nintendo-client-secret'] ||
!VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] ||
headers['x-nintendo-client-secret'] !== VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']]
) {
return response.send(xmlbuilder.create({
errors: {
error: {
cause: 'client_id',
code: '0004',
message: 'API application invalid or incorrect application credentials'
}
}
}).end());
}
return next();
}
module.exports = nintendoClientHeaderCheck;

50
src/middleware/pnid.js Normal file
View File

@@ -0,0 +1,50 @@
const xmlbuilder = require('xmlbuilder');
const database = require('../database');
async function PNIDMiddleware(request, response, next) {
const { headers } = request;
if (!headers.authorization || !(headers.authorization.startsWith('Bearer') || headers.authorization.startsWith('Basic'))) {
return next();
}
const [type, token] = headers.authorization.split(' ');
let user;
if (type === 'Basic') {
user = await database.getUserBasic(token);
} else {
user = await database.getUserBearer(token);
}
if (!user) {
response.status(401);
if (type === 'Bearer') {
return response.send(xmlbuilder.create({
errors: {
error: {
cause: 'access_token',
code: '0005',
message: 'Invalid access token'
}
}
}).end());
}
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1105',
message: 'Email address, username, or password, is not valid'
}
}
}).end());
}
request.pnid = user;
return next();
}
module.exports = PNIDMiddleware;

23
src/middleware/session.js Normal file
View File

@@ -0,0 +1,23 @@
// super basic and there's probably a much better way to do this
// this will only be used during the registration process, to track the progress of the user
// express-session uses cookies which the WiiU does not support during the registration process
// temp, in-memory session storage
const sessionStore = {};
function sessionMiddlware(request, response, next) {
const ip = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
if (!sessionStore[ip]) {
sessionStore[ip] = {};
}
const session = sessionStore[ip];
request.session = session;
return next();
}
module.exports = sessionMiddlware;

View File

@@ -0,0 +1,35 @@
const { document: xmlParser } = require('xmlbuilder2');
function XMLMiddleware(request, response, next) {
if (request.method == 'POST' || request.method == 'PUT') {
const headers = request.headers;
let body = '';
if (
!headers['content-type'] ||
!headers['content-type'].toLowerCase().includes('xml')
) {
return next();
}
request.setEncoding('utf-8');
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
try {
request.body = xmlParser(body);
request.body = request.body.toObject();
} catch (error) {
return next();
}
next();
});
} else {
next();
}
}
module.exports = XMLMiddleware;

27
src/models/endpoint.js Normal file
View File

@@ -0,0 +1,27 @@
const { Schema, model } = require('mongoose');
const endpointSchema = new Schema({
has_error: Number,
version: Number,
endpoint: {
host: String,
api_host: String,
portal_host: String,
n3ds_host: String
}
});
endpointSchema.methods.updateHosts = async function({host, api_host, portal_host, n3ds_host}) {
this.set('endpoint.host', host);
this.set('endpoint.api_host', api_host);
this.set('endpoint.portal_host', portal_host);
this.set('endpoint.n3ds_host', n3ds_host);
await this.save();
};
const ENDPOINT = model('ENDPOINT', endpointSchema);
module.exports = {
endpointSchema,
ENDPOINT
};

71
src/models/post.js Normal file
View File

@@ -0,0 +1,71 @@
const { Schema, model } = require('mongoose');
//just testing pull requests
const PostSchema = new Schema({
body: String,
community_id: String,
country_id: Number,
created_at: String,
feeling_id: Number,
id: Number,
is_autopost: {
type: Number,
default: 0
},
is_community_private_autopost: {
type: Number,
default: 0
},
is_spoiler: {
type: Number,
default: 0
},
is_app_jumpable: {
type: Number,
default: 0
},
empathy_count: {
type: Number,
default: 0
},
language_id: {
type: Number,
default: 1
},
mii: String,
mii_face_url: String,
number: {
type: Number,
default: 1
},
pid: Number,
platform_id: Number,
region_id: Number,
reply_count: {
type: Number,
default: 0
},
screen_name: String,
title_id: Number,
});
PostSchema.methods.upEmpathy = async function() {
const empathy = this.get('empathy_count');
this.set('empathy_count', empathy + 1);
await this.save();
};
PostSchema.methods.downEmpathy = async function() {
const empathy = this.get('empathy_count');
this.set('empathy_count', empathy - 1);
await this.save();
};
const MiiversePost = model('MiiversePost', PostSchema);
module.exports = {
PostSchema,
MiiversePost
};

79
src/models/topic.js Normal file
View File

@@ -0,0 +1,79 @@
const { Schema, model } = require('mongoose');
const { PostSchema } = require('./post');
const titleIdsSchema = new Schema({
title_id: String,
});
const personSchema = new Schema({
posts: {
type: [PostSchema],
default: undefined
}
});
const TopicSchema = new Schema({
empathy_count: {
type: Number,
default: 0
},
has_shop_page: {
type: Number,
default: 0
},
icon: String,
title_ids: {
type: [titleIdsSchema],
default: undefined
},
title_id: String,
community_id: String,
is_recommended: {
type: Number,
default: 0
},
name: String,
people: {
type: [personSchema],
default: undefined
},
});
TopicSchema.methods.upEmpathy = async function() {
const empathy = this.get('empathy_count');
this.set('empathy_count', empathy + 1);
await this.save();
};
TopicSchema.methods.downEmpathy = async function() {
const empathy = this.get('empathy_count');
this.set('empathy_count', empathy - 1);
await this.save();
};
TopicSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next();
}
this.set('usernameLower', this.get('username').toLowerCase());
await this.generatePID();
await this.generateNEXPassword();
await this.generateEmailValidationCode();
await this.generateEmailValidationToken();
const primaryHash = util.nintendoPasswordHash(this.get('password'), this.get('pid'));
const hash = bcrypt.hashSync(primaryHash, 10);
this.set('password', hash);
next();
});
const MiiverseTopic = model('MiiverseTopic', PostSchema);
module.exports = {
TopicSchema,
MiiverseTopic
};

59
src/server.js Normal file
View File

@@ -0,0 +1,59 @@
process.title = 'Pretendo - Miiverse';
const express = require('express');
const morgan = require('morgan');
const xmlparser = require('./middleware/xml-parser');
const database = require('./database');
const logger = require('./logger');
const config = require('./config.json');
const { http: { port } } = config;
const app = express();
const miiverse = require('./services/miiverse-api');
app.set('etag', false);
app.disable('x-powered-by');
// Create router
logger.info('Setting up Middleware');
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
app.use(xmlparser);
// import the servers into one
app.use(miiverse);
// 404 handler
logger.info('Creating 404 status handler');
app.use((request, response) => {
logger.warn(request.protocol + '://' + request.get('host') + request.originalUrl);
response.status(404);
response.send();
});
// non-404 error handler
logger.info('Creating non-404 status handler');
app.use((error, request, response) => {
const status = error.status || 500;
response.status(status);
response.json({
app: 'api',
status,
error: error.message
});
});
// Starts the server
logger.info('Starting server');
database.connect().then(() => {
app.listen(port, () => {
logger.success(`Server started on port ${port}`);
});
});

View File

@@ -0,0 +1,31 @@
const express = require('express');
const subdomain = require('express-subdomain');
const sessionMiddleware = require('../../middleware/session');
const pnidMiddleware = require('../../middleware/pnid');
const logger = require('../../logger');
const routes = require('./routes');
// Main router for endpointsindex.js
const router = express.Router();
// Router to handle the subdomain restriction
const miiverse = express.Router();
const portal = express.Router();
// Create subdomains
logger.info('[MIIVERSE] Creating \'discovery\' subdomain');
router.use(subdomain('discovery.olv', miiverse));
logger.info('[MIIVERSE] Creating \'portal\' subdomain');
router.use(subdomain('portal.olv', portal));
router.use(subdomain('ctr-portal.olv', portal));
logger.info('[MIIVERSE] Importing middleware');
miiverse.use(sessionMiddleware);
miiverse.use(pnidMiddleware);
// Setup routes
miiverse.use('/v1/endpoint', routes.DISCOVERY);
portal.use('/', routes.PORTAL);
module.exports = router;

View File

@@ -0,0 +1,162 @@
var express = require('express');
var xml = require('object-to-xml');
const database = require('../../../database');
var router = express.Router();
/* GET discovery server. */
router.get('/', function (req, res) {
database.connect().then(async e => {
const discovery = await database.getDiscoveryHosts();
switch(discovery.has_error)
{
case 0 :
res.set("Content-Type", "application/xml");
response = {
result: {
has_error: 0,
version: discovery.version,
endpoint: {
host: discovery.endpoint.host,
api_host: discovery.endpoint.api_host,
portal_host: discovery.endpoint.portal_host,
n3ds_host: discovery.endpoint.n3ds_host
}
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
case 1 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 1,
message: "SYSTEM_UPDATE_REQUIRED"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
case 2 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 2,
message: "SETUP_NOT_COMPLETE"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
case 3 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 3,
message: "SERVICE_MAINTENANCE"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
case 4:
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 4,
message: "SERVICE_CLOSED"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
case 5 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 5,
message: "PARENTAL_CONTROLS_ENABLED"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
case 6 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 6,
message: "POSTING_LIMITED_PARENTAL_CONTROLS"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
case 7 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 7,
message: "PNID_BANNED"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
default :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 15,
message: "SERVER_ERROR"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
}
}).catch(error => {
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 15,
message: "SERVER_ERROR"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
});
});
router.post('/posts', function (req, res) {
res.sendStatus(200);
});
module.exports = router;

View File

@@ -0,0 +1,4 @@
module.exports = {
DISCOVERY: require('./discovery'),
PORTAL: require('./portal'),
};

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>Miiverse Testing</title>
</head>
<body>
<h1>Hello from the Pretendo Network!</h1>
<p>Did I win yet? :p</p>
<script>
wiiuBrowser.endStartUp();
cave.snd_playBgm(BGM_CAVE_MAIN);
cave.transition_end();
cave.toolbar_setVisible(visibility = true);
</script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
var express = require('express');
var xml = require('object-to-xml');
const database = require('../../../database');
var router = express.Router();
var path = require("path");
/* GET discovery server. */
router.get('/', function (req, res) {
res.sendFile(path.join(__dirname + '/portal.html'));
});
router.post('/posts', function (req, res) {
res.sendStatus(200);
});
module.exports = router;

23
src/testingDatabase.js Normal file
View File

@@ -0,0 +1,23 @@
const { ENDPOINT } = require('./models/endpoint');
const database = require('./database');
const doc = {
has_error: 0,
version: 1,
endpoint: {
host: "host1",
api_host: "host2",
portal_host: "host3",
n3ds_host: "host4"
}
};
const newEndpoint = new ENDPOINT(doc);
database.connect().then(async yeet => {
const temp = await database.getServerConfig();
console.log(temp);
});
//database.getDiscoveryHosts().then(r => console.log(r.endpoint));
//newEndpoint.save().catch(error => {console.log(error);});