code import

This commit is contained in:
Funey
2021-01-06 14:18:45 +00:00
commit 09360bf4a9
10 changed files with 1207 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# Ignore node modules.
/node_modules
# Ignore Java version tracker.
/java.json
# Ignore cape cache/override.
/c_cache
/c_override
# Ignore skin cache/override.
/s_cache
/s_override
# Ignore MinecraftResources.
/resources.xml

BIN
defaults/.DS_Store vendored Normal file

Binary file not shown.

BIN
defaults/default_skin.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

560
index.js Normal file
View File

@@ -0,0 +1,560 @@
'use strict';
// ActionMC legacy skins server.
// Virtue 2021
const path = require('path');
const fs = require('fs');
const request = require('sync-request');
function downloadFileSync(url) {
return require('child_process')
.execFileSync('curl', ['--silent', '-L', url]); // remove options {encoding: 'utf8'}
}
// Variables.
// This is where the server stores caches of Mojang requests.
const Skin_CacheLocation = path.join(__dirname, "s_cache");
const Cape_CacheLocation = path.join(__dirname, "c_cache");
// This defines the amount of time an item should be cached for.
const Skin_CacheTime = 24; // 24 hours by default.
const Cape_CacheTime = 48; // 48 hours by default (capes don't change as much)
// This is where the server stores overrides - if a user has an entry in
// these folders, their Mojang request will be overriden with the contents
// of the file present in the override location.
const Skin_OverrideLocation = path.join(__dirname, "s_override");
const Cape_OverrideLocation = path.join(__dirname, "c_override");
// This is where the resource.xml file is stored - this tells a client
// where to find certain resources - may fix some sounds in-game.
const Resources_XML_Location = path.join(__dirname, "resources.xml")
// This is where the server stores default skins - if a user has no
// skin, their Mojang request will be overridden with the contents
// of the default file.
const Skin_DefaultFile = path.join(__dirname, 'defaults', 'default_skin.png')
const Cape_DefaultFile = path.join(__dirname, 'defaults', 'default_cape.png')
// This is where the information for the Java poll is stored. This
// must be a JSON file.
const Java_JSON = path.join(__dirname, "java.json");
// ACTUAL CODE BEYOND THIS POINT \\
// Initialise variables
// Time until cache clear for stats api.
var Skin_CacheWipeTime = Date.now() + (3600000 * Skin_CacheTime);
var Cape_CacheWipeTime = Date.now() + (3600000 * Cape_CacheTime);
// Actual distance between cache clear.
var Skin_CacheWipeDis = 3600000 * Skin_CacheTime;
var Cape_CacheWipeDis = 3600000 * Cape_CacheTime;
// Previous.
var Skin_PreviousCacheWipeTime = Date.now();
var Cape_PreviousCacheWipeTime = Date.now();
// Do pre-initialisation folder checks.
if (!fs.existsSync(Skin_CacheLocation)) {
console.log('[WARN] Skin cache directory does not exist, creating...');
try {
fs.mkdirSync(Skin_CacheLocation);
console.log('[INFO] Skin cache directory created successfully.')
} catch (exc) {
console.log(exc)
console.log('[ERR] Failure creating skin cache directory.')
process.exit(1);
}
}
if (!fs.existsSync(Cape_CacheLocation)) {
console.log('[WARN] Cape cache directory does not exist, creating...');
try {
fs.mkdirSync(Cape_CacheLocation);
console.log('[INFO] Cape cache directory created successfully.')
} catch (exc) {
console.log(exc)
console.log('[ERR] Failure creating Cape cache directory.')
process.exit(1);
}
}
if (!fs.existsSync(Skin_OverrideLocation)) {
console.log('[WARN] Skin override directory does not exist, creating...');
try {
fs.mkdirSync(Skin_OverrideLocation);
console.log('[INFO] Skin override directory created successfully.')
} catch (exc) {
console.log(exc)
console.log('[ERR] Failure creating skin override directory.')
process.exit(1);
}
}
if (!fs.existsSync(Cape_OverrideLocation)) {
console.log('[WARN] Cape override directory does not exist, creating...');
try {
fs.mkdirSync(Cape_OverrideLocation);
console.log('[INFO] Cape override directory created successfully.')
} catch {
console.log('[ERR] Failure creating cape override directory.')
process.exit(1);
}
}
if (!fs.existsSync(Resources_XML_Location)) {
console.log('[WARN] Resources XML does not exist. Obtaining...');
try {
let dl = downloadFileSync('http://s3.amazonaws.com/MinecraftResources');
fs.writeFileSync(Resources_XML_Location, dl);
console.log('[INFO] Resources XML created successfully!')
} catch {
console.log('[ERR] Failure creating resources XML.');
process.exit(1);
}
}
// Initialise functions.
// add_to_java_poll
// Purpose: Adds a new entry into the Java version data.
// Returns: nothing.
function add_to_java_poll(java_useragent) {
var tmp_json;
if (fs.existsSync(Java_JSON)) {
tmp_json = require(Java_JSON);
} else {
tmp_json = {};
}
if (tmp_json[java_useragent]) {
tmp_json[java_useragent] += 1;
} else {
tmp_json[java_useragent] = 1;
}
try {
fs.writeFileSync(Java_JSON, JSON.stringify(tmp_json));
} catch (exc) {
// oh poop.
}
}
// get_uuid
// Purpose: Gets a UUID of a player.
// Returns: UUID of player if they exist, false if error occured.
function get_uuid(username) {
let reqBody = '["' + username + '"]';
let req = request('POST', 'https://api.mojang.com/profiles/minecraft', {body: reqBody});
// Have response now.
let response = JSON.parse(req.getBody('utf8'));
if (response.error) {
// error obtaining user.
return false;
}
// we have the user, get their UUID.
return response[0]["id"];
}
// check_cache_override
// Purpose: Checks the cache and override directories for the user.
// Returns: 0 for not cached, 1 for cached and 2 for override.
function check_cache_override(type, name) {
// type can either be skin or cape.
if (type == "skin") {
if (fs.existsSync(path.join(Skin_OverrideLocation, name + '.png'))) {
// user has an entry in the override location.
return 2;
}
if (fs.existsSync(path.join(Skin_CacheLocation, name + '.png'))) {
// user has an entry in the cache location.
return 1;
}
// if execution of subroutine continues, no cache entry exists.
return 0;
} else if (type == "cape") {
if (fs.existsSync(path.join(Cape_OverrideLocation, name + '.png'))) {
// user has an entry in the override location.
return 2;
}
if (fs.existsSync(path.join(Cape_CacheLocation, name + '.png'))) {
// user has an entry in the cache location.
return 1;
}
// if execution of subroutine continues, no cache entry exists.
return 0;
}
}
// get_skin_from_mojang
// Purpose: Get a skin from Mojang.
// Returns: true if skin obtained, false if skin failed.
function get_skin_from_mojang(uuid, name) {
let req = request('GET', 'https://sessionserver.mojang.com/session/minecraft/profile/' + uuid);
// have response now.
let response = JSON.parse(req.getBody('utf8'));
if (response.error) {
// error obtaining profile.
return false;
}
// now parse for the base64 encoded json.
let b64json = response["properties"][0]["value"];
let b64buffer = new Buffer(b64json, 'base64');
b64json = b64buffer.toString('ascii');
b64json = JSON.parse(b64json);
// now we have a download link.
let download_url = b64json["textures"]["SKIN"]['url'];
let skin_file = downloadFileSync(download_url);
// write to file.
try {
let write_time = Date.now();
let writing = fs.writeFileSync(path.join(Skin_CacheLocation, name + '.png'), skin_file);
return true;
} catch (exc) {
console.error(exc);
return false;
}
}
// get_cape_from_mojang
// Purpose: Get a cape from Mojang.
// Returns: true if cape obtained, false if cape failed.
function get_cape_from_mojang(uuid, name) {
let req = request('GET', 'https://sessionserver.mojang.com/session/minecraft/profile/' + uuid);
// have response now.
let response = JSON.parse(req.getBody('utf8'));
if (response.error) {
// error obtaining profile.
return false;
}
// now parse for the base64 encoded json.
let b64json = response["properties"][0]["value"];
let b64buffer = new Buffer(b64json, 'base64');
b64json = b64buffer.toString('ascii');
b64json = JSON.parse(b64json);
// now we have a download link.
let download_url = b64json["textures"]["CAPE"]['url'];
let skin_file = downloadFileSync(download_url);
// write to file.
try {
let write_time = Date.now();
let writing = fs.writeFileSync(path.join(Cape_CacheLocation, name + ".png"), skin_file);
return true;
} catch (exc) {
console.error(exc);
return false;
}
}
// get_skin
// Purpose: Get the skin location of the user to serve.
// Returns: location to skin file if exists, location to default skin if not.
function get_skin(uname) {
let cache = check_cache_override('skin', uname);
if (cache == 1) {
// present in cache - serve user from there
return path.join(Skin_CacheLocation, uname + '.png')
} else if (cache == 2) {
// present in OVERRIDE - serve user from there.
return path.join(Skin_OverrideLocation, uname + '.png')
} else {
// presume user does not exist in cache. try to fetch beforehand though.
try {
// get uuid
let uuid = get_uuid(uname);
// get skin from mojang.
let skin_get = get_skin_from_mojang(uuid, uname);
if (!skin_get) {
// failed. return default skin path.
return Skin_DefaultFile;
} else {
// user is now in cache.
return path.join(Skin_CacheLocation, uname + '.png')
}
} catch {
// error occured somewhere.
return Skin_DefaultFile;
}
}
}
// get_cape
// Purpose: Get the cape location of the user to serve.
// Returns: location to cape file if exists, location to default cape if not.
function get_cape(uname) {
let cache = check_cache_override('cape', uname);
if (cache == 1) {
// present in cache - serve user from there
return path.join(Cape_CacheLocation, uname + '.png')
} else if (cache == 2) {
// present in OVERRIDE - serve user from there.
return path.join(Cape_OverrideLocation, uname + '.png')
} else {
// presume user does not exist in cache. try to fetch beforehand though.
try {
// get uuid
let uuid = get_uuid(uname);
// get skin from mojang.
let skin_get = get_cape_from_mojang(uuid, uname);
if (!skin_get) {
// failed. return default skin path.
return Cape_DefaultFile;
} else {
// user is now in cache.
return path.join(Cape_CacheLocation, uname + '.png')
}
} catch {
// error occured somewhere.
return Cape_DefaultFile;
}
}
}
// rm_dir
// Purpose: removes all files in a directory.
// Returns: nothing.
function rm_dir(dirPath) {
try { var files = fs.readdirSync(dirPath); }
catch(e) { return; }
if (files.length > 0)
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
if (fs.statSync(filePath).isFile())
fs.unlinkSync(filePath);
else
rmDir(filePath);
}
};
// Initialise cache-wiping functions.
// Skin.
setInterval(() => {
console.log('[CACHE] Wiping skins cache...');
rm_dir(Skin_CacheLocation);
Skin_PreviousCacheWipeTime = Skin_CacheWipeTime
Skin_CacheWipeTime = Date.now() + (3600000 * Skin_CacheTime);
console.log('[CACHE] Skin cache wipe completed.');
}, Skin_CacheWipeDis)
// Cape.
setInterval(() => {
console.log('[CACHE] Wiping capes cache...');
rm_dir(Cape_CacheLocation);
Cape_PreviousCacheWipeTime = Cape_CacheWipeTime
Cape_CacheWipeTime = Date.now() + (3600000 * Cape_CacheTime);
console.log('[CACHE] Cape cache wipe completed.');
}, Cape_CacheWipeDis)
// Do server initial cache wipe.
console.log('[CACHE] Server startup - wiping skin cache...');
rm_dir(Skin_CacheLocation);
console.log('[CACHE] Skin cache wipe completed.');
console.log('[CACHE] Server startup - wiping cape cache...');
rm_dir(Cape_CacheLocation);
console.log('[CACHE] Cape cache wipe completed.')
// Initialise express.
const express = require('express')
const app = express()
const port = 80
// Initialise static folders.
app.use(express.static('static'))
app.use('/MinecraftResources', express.static('resources'))
// Initialise routers.
// GET /MinecraftSkins/:username
// Paramters:
// username: username of character to get skin for
// Returns:
// skin.png
app.get('/MinecraftSkins/:username', async (req,res) => {
let parsed_username = req.params.username.slice(0,-4);
console.log('[GET] Cape: ' + parsed_username);
console.log(parsed_username)
let location = get_skin(parsed_username);
res.sendFile(location);
})
// GET /MinecraftCloaks/:username
// Paramters:
// username: username of character to get cape for
// Returns:
// cape.png
app.get('/MinecraftCloaks/:username', async (req,res) => {
let parsed_username = req.params.username.slice(0,-4);
console.log('[GET] Cloak: ' + parsed_username);
let location = get_cape(parsed_username);
res.sendFile(location);
})
// GET /statistics
// No parameters.
// Returns:
// - information from the stats API but neater and user-friendly.
app.get('/statistics', async (req,res) => {
let skin_cacheCount = fs.readdirSync(Skin_CacheLocation).length;
let skin_overrideCount = fs.readdirSync(Skin_OverrideLocation).length;
let cape_cacheCount = fs.readdirSync(Cape_CacheLocation).length;
let cape_overrideCount = fs.readdirSync(Cape_OverrideLocation).length;
let skin_wipeTime = new Date(Skin_CacheWipeTime);
let skin_prevWipeTime = new Date(Skin_PreviousCacheWipeTime);
let cape_wipeTime = new Date(Cape_CacheWipeTime);
let cape_prevWipeTime = new Date(Cape_PreviousCacheWipeTime);
let result = `<h1>Statistics</h1>
<h4>Skins</h4>
<p>Skins currently cached: ${skin_cacheCount}</p>
<p>Skins currently overriden: ${skin_overrideCount}</p>
<p>Time of next skin cache wipe: ${skin_wipeTime.toUTCString()}</p>
<p>Time of previous skin cache wipe: ${skin_prevWipeTime.toUTCString()}</p>
<h4>Capes</h4>
<p>Capes currently cached: ${cape_cacheCount}</p>
<p>Capes currently overridden: ${cape_overrideCount}</p>
<p>Time of next cape cache wipe: ${cape_wipeTime.toUTCString()}</p>
<p>Time of previous cape cache wipe: ${cape_prevWipeTime.toUTCString()}</p><hr />
<p><i>For Java statistics, see <a href="/statistics/java">the Java statistics page</a>.</i></p>`
res.send(result)
})
// GET /statistics/java
// No parameters
// Returns:
// - Java version info from the stats API but neater and user-friendly.
app.get('/statistics/java', async (req,res) => {
let result = '<h1>Versions of Java that have used this skin server:</h1>'
var javajson;
if (fs.existsSync(Java_JSON)) {
javajson = require(Java_JSON)
} else {
javajson = {}
}
for (const i in javajson) {
result += `<p><strong>${i}:</strong> ${javajson[i]}</p>`
}
result += '<hr /><p><i>To see other statistics, see <a href="/statistics">the main statistics page</a>.</i></p>'
res.send(result)
})
// GET /statistics/api/java
// No parameters.
// Returns:
// - Java versions that have "phoned home" on MC client startup.
app.get('/statisticss/api/java', async (req,res) => {
var javajson;
if (fs.existsSync(Java_JSON)) {
javajson = require(Java_JSON)
} else {
javajson = {}
}
res.json(javajson)
});
// GET /statistics/api/skins
// No parameteers
// Returns:
// - amount of skins cached
// - amount of skins with overrides
// - epoch timestamp of cache wipe time
// - epoch timestamp of previous cache wipe time
app.get('/statistics/api/skins', async (req,res) => {
let skin_CacheCount = fs.readdirSync(Skin_CacheLocation).length;
let skin_OverrideCount = fs.readdirSync(Skin_OverrideLocation).length;
res.contentType('application/json')
res.json({cached: skin_CacheCount, overridden: skin_OverrideCount, cache_wipe_time: Skin_CacheWipeTime, previous_cache_wipe_time: Skin_PreviousCacheWipeTime});
})
// GET /statistics/api/capes
// No parameteers
// Returns:
// - amount of capes cached
// - amount of capes with overrides
// - epoch timestamp of cache wipe time
// - epoch timestamp of previous cache wipe time
app.get('/statistics/api/capes', async (req,res) => {
let skin_CacheCount = fs.readdirSync(Cape_CacheLocation).length;
let skin_OverrideCount = fs.readdirSync(Cape_OverrideLocation).length;
res.contentType('application/json')
res.json({cached: skin_CacheCount, overridden: skin_OverrideCount, cache_wipe_time: Cape_CacheWipeTime, previous_cache_wipe_time: Cape_PreviousCacheWipeTime});
})
// GET /MinecraftResources
// No parameters
// Returns:
// - XML file containing location of resources.
// This can be also used to track amount of clients using skin server,
// but this shouldn't be relied upon.
// Client can be tracked by User Agent.
// If the first four characters of their user agent are "Java", then
// it is likely that they are a MC client.
app.get('/MinecraftResources', async (req,res) => {
if (req.headers['user-agent'].slice(0,4) == "Java") {
console.log('[INFO] New client made connection to server!');
add_to_java_poll(req.headers['user-agent']);
}
res.sendFile(Resources_XML_Location);
})
// Listen on defined port.
app.listen(port, () => {
console.log(`[HTTP] ActionMC skin server listening on port ${port}.`)
})
// EOF

586
package-lock.json generated Normal file
View File

@@ -0,0 +1,586 @@
{
"name": "amc-skins",
"version": "2.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/concat-stream": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz",
"integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=",
"requires": {
"@types/node": "*"
}
},
"@types/form-data": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
"integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=",
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "10.17.50",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.50.tgz",
"integrity": "sha512-vwX+/ija9xKc/z9VqMCdbf4WYcMTGsI0I/L/6shIF3qXURxZOhPQlPRHtjTpiNhAwn0paMJzlOQqw6mAGEQnTA=="
},
"@types/qs": {
"version": "6.9.5",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
"integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ=="
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"download-file-sync": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/download-file-sync/-/download-file-sync-1.0.4.tgz",
"integrity": "sha1-0+PFQ/g29BA5RVuQNMcuNVsDYBk="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"form-data": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"get-port": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
"integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw="
},
"http-basic": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
"integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==",
"requires": {
"caseless": "^0.12.0",
"concat-stream": "^1.6.2",
"http-response-object": "^3.0.1",
"parse-cache-control": "^1.0.1"
}
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"http-response-object": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
"integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
"requires": {
"@types/node": "^10.0.3"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w=="
},
"mime-types": {
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"requires": {
"mime-db": "1.45.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parse-cache-control": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
"integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104="
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"promise": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz",
"integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==",
"requires": {
"asap": "~2.0.6"
}
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"sync-request": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz",
"integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==",
"requires": {
"http-response-object": "^3.0.1",
"sync-rpc": "^1.2.1",
"then-request": "^6.0.0"
}
},
"sync-rpc": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz",
"integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==",
"requires": {
"get-port": "^3.1.0"
}
},
"then-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz",
"integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==",
"requires": {
"@types/concat-stream": "^1.6.0",
"@types/form-data": "0.0.33",
"@types/node": "^8.0.0",
"@types/qs": "^6.2.31",
"caseless": "~0.12.0",
"concat-stream": "^1.6.0",
"form-data": "^2.2.0",
"http-basic": "^8.1.1",
"http-response-object": "^3.0.1",
"promise": "^8.0.0",
"qs": "^6.4.0"
},
"dependencies": {
"@types/node": {
"version": "8.10.66",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
"integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="
}
}
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}
}
}

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "amc-skins",
"version": "2.0.1",
"description": "Legacy skins server for ActionMC.",
"main": "index.js",
"scripts": {
"test": "node ."
},
"author": "Virtue <virtue@actionmc.ml>",
"license": "ISC",
"dependencies": {
"download-file-sync": "^1.0.4",
"express": "^4.17.1",
"sync-request": "^6.1.0"
}
}

View File

@@ -0,0 +1,8 @@
Place contents of resources folder in .minecraft here.
This is required for some older versions of Minecraft.
Location of .minecraft:
Windows: %appdata%\.minecraft
Linux: ~/.minecraft
macOS: ~/Libary/Application Support/minecraft

19
static/README.md Normal file
View File

@@ -0,0 +1,19 @@
# Theta Skins Server
A replacement for the now-removed legacy Minecraft Skins API, as well as providing a few fixes for older versions.
# Run Instructions
- Clone the repository.
- Run ``npm install``
- Populate the "resources" folder with the contents of `.minecraft/resources` (follow the readme file in the folder for more information)
- Start the program with ``npm run test``
The program will automatically make directories and XML files.
# How to Access
Append the following string to your hosts file:
```
<your-server-ip> s3.amazonaws.com
```
Be sure to disable this after playing though, as a lot of the internet relies on Amazon S3 services and this server cannot provide all of them.

2
static/index.html Normal file
View File

@@ -0,0 +1,2 @@
<h1>Theta Skin Server</h1>
<p>Your website goes here!</p>