diff --git a/.gitignore b/.gitignore index 79ec252..533683f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ images/*/*.png -*.log node_modules/ -.DS_Store -*.rdb coverage/ -config.js +.DS_Store +*.log +*.rdb *.sublime-* +config.js diff --git a/README.md b/README.md index 69ef0c1..63ff7e4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Crafatar [![travis](https://img.shields.io/travis/crafatar/crafatar/master.svg?style=flat-square)](https://travis-ci.org/crafatar/crafatar/) [![Coverage Status](https://img.shields.io/coveralls/crafatar/crafatar.svg?style=flat-square)](https://coveralls.io/r/crafatar/crafatar) [![Code Climate](https://img.shields.io/codeclimate/github/crafatar/crafatar.svg?style=flat-square)](https://codeclimate.com/github/crafatar/crafatar) -[![IRC: #crafatar](https://img.shields.io/badge/IRC-%23crafatar-blue.svg?style=flat-square)](https://webchat.esper.net/?channels=crafatar) [![dependency status](https://img.shields.io/david/crafatar/crafatar.svg?style=flat-square)](https://david-dm.org/crafatar/crafatar) [![devDependency status](https://img.shields.io/david/dev/crafatar/crafatar.svg?style=flat-square)](https://david-dm.org/crafatar/crafatar#info=devDependencies) [![docs status](https://inch-ci.org/github/crafatar/crafatar.svg?branch=master&style=flat-square)](https://inch-ci.org/github/crafatar/crafatar) +[![IRC: esper.net](https://img.shields.io/badge/IRC-esper.net-blue.svg?style=flat-square)](https://webchat.esper.net/?channels=crafatar "#crafatar") [![dependency status](https://img.shields.io/david/crafatar/crafatar.svg?style=flat-square)](https://david-dm.org/crafatar/crafatar) [![devDependency status](https://img.shields.io/david/dev/crafatar/crafatar.svg?style=flat-square)](https://david-dm.org/crafatar/crafatar#info=devDependencies) [![docs status](https://inch-ci.org/github/crafatar/crafatar.svg?branch=master&style=flat-square)](https://inch-ci.org/github/crafatar/crafatar) logo @@ -33,41 +33,11 @@ Please [visit the website](https://crafatar.com) for details. * Open an [issue](https://github.com/crafatar/crafatar/issues/) on GitHub * You can [join IRC](https://webchat.esper.net/?channels=crafatar) in #crafatar on irc.esper.net. -## Installation on Heroku -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) +# Installation -## Installation on Dokku -##### [dokku server] -Install the [dokku-redis](https://github.com/ohardy/dokku-redis#redis-plugin-for-dokku) plugin. -```shell -dokku redis:start -dokku apps:create crafatar -dokku config:set crafatar BIND=0.0.0.0 PORT=5000 -``` -For persistent images and logs: -```shell -dokku docker-options:add crafatar deploy "-v /var/lib/crafatar/images:/app/images" -dokku docker-options:add crafatar deploy "-v /var/log/crafatar:/app/logs" -``` -If you want to listen on extra domains: -```shell -dokku domains crafatar:add example.com -``` -##### [your machine] -Add dokku remote and deploy! -```shell -git remote add dokku dokku@example.com:crafatar -git push dokku master -``` - -## Installation on your machine -* Use io.js -* [Install](https://github.com/Automattic/node-canvas/wiki) Cairo. -* `npm install` -* Start `redis-server` -* `npm start` -* Access [http://localhost:3000](http://localhost:3000) +Have a look at [crafatar/setup](https://github.com/crafatar/setup) to see how we set things up at Crafatar. +For more info about local setup, Heroku, or Dokku please see [Installation](https://github.com/crafatar/crafatar/wiki/Installation) on the wiki. ## Tests ```shell @@ -83,4 +53,4 @@ env VERBOSE_TEST=true npm test It can be helpful to monitor redis commands to debug caching errors: ```shell redis-cli monitor -``` +``` \ No newline at end of file diff --git a/app.json b/app.json index 40bd9ac..939ca44 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,6 @@ { "name": "Crafatar", - "description": "A Minecraft Avatar API written in NodeJS", + "description": "A blazing fast API for Minecraft faces!", "repository": "https://github.com/crafatar/crafatar", "keywords": [ "node", @@ -10,10 +10,21 @@ ], "website": "https://crafatar.com/", "env": { - "HEROKU": "true", - "BUILDPACK_URL": "https://github.com/mojodna/heroku-buildpack-multi.git#build-env" + "EPHEMERAL_STORAGE": { + "description": "Set to true if your storage is gone after deploying", + "required": false, + "value": true + } }, "addons": [ "rediscloud" + ], + "buildpacks": [ + { + "url": "https://github.com/mojodna/heroku-buildpack-cairo.git" + }, + { + "url": "https://github.com/heroku/heroku-buildpack-nodejs.git" + } ] -} +} \ No newline at end of file diff --git a/config.example.js b/config.example.js index be165d5..25b273e 100644 --- a/config.example.js +++ b/config.example.js @@ -1,36 +1,35 @@ var config = { avatars: { - min_size: 1, // for avatars - max_size: 512, // for avatars; too big values might lead to slow response time or DoS - default_size: 160 // for avatars; size to be used when no size given + min_size: 1, // for avatars + max_size: 512, // for avatars; too big values might lead to slow response time or DoS + default_size: 160 // for avatars; size to be used when no size given }, renders: { - min_scale: 1, // for 3D rendered skins - max_scale: 10, // for 3D rendered skins; too big values might lead to slow response time or DoS - default_scale: 6 // for 3D rendered skins; scale to be used when no scale given + min_scale: 1, // for 3D rendered skins + max_scale: 10, // for 3D rendered skins; too big values might lead to slow response time or DoS + default_scale: 6 // for 3D rendered skins; scale to be used when no scale given }, cleaner: { - interval: 1800, // interval seconds to check limits - disk_limit: 10240, // min allowed free KB on disk to trigger image deletion - redis_limit: 24576, // max allowed used KB on redis to trigger redis flush - amount: 50000 // amount of skins for which all iamge types are deleted + interval: 600, // interval seconds to check limits + disk_limit: 524288, // min allowed free KB on disk to trigger image deletion + redis_limit: 24576, // max allowed used KB on redis to trigger redis flush + amount: 50000 // amount of skins for which all image types are deleted }, directories: { - faces: "images/faces/", // directory where faces are kept. should have trailing "/" - helms: "images/helms/", // directory where helms are kept. should have trailing "/" - skins: "images/skins/", // directory where skins are kept. should have trailing "/" - renders: "images/renders/", // directory where rendered skins are kept. should have trailing "/" - capes: "images/capes/" // directory where capes are kept. should have trailing "/" + faces: "./images/faces/", // directory where faces are kept. must have trailing "/" + helms: "./images/helms/", // directory where helms are kept. must have trailing "/" + skins: "./images/skins/", // directory where skins are kept. must have trailing "/" + renders: "./images/renders/", // directory where rendered skins are kept. must have trailing "/" + capes: "./images/capes/" // directory where capes are kept. must have trailing "/" }, caching: { - local: 1200, // seconds until we will check if user's skin changed. should be > 60 to comply with Mojang's rate limit - browser: 3600 // seconds until browser will request image again + local: 1200, // seconds until we will check if user's skin changed. should be > 60 to comply with Mojang's rate limit + browser: 3600 // seconds until browser will request image again }, server: { - http_timeout: 1000, // ms until connection to Mojang is dropped - debug_enabled: false, // enables logging.debug - clusters: 1, // we recommend not using multiple clusters YET, see issue #80 - log_time: true // set to false if you use an external logger that provides timestamps + http_timeout: 2000, // ms until connection to Mojang is dropped + debug_enabled: false, // enables logging.debug & editing index page + log_time: true // set to false if you use an external logger that provides timestamps } }; diff --git a/lib/cache.js b/lib/cache.js index b996d05..219e6af 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -1,18 +1,16 @@ var logging = require("./logging"); var node_redis = require("redis"); var config = require("../config"); -var path = require("path"); var url = require("url"); -var fs = require("fs"); var redis = null; // sets up redis connection -// flushes redis when running on heroku (files aren't kept between pushes) +// flushes redis when using ephemeral storage (e.g. Heroku) function connect_redis() { logging.log("connecting to redis..."); // parse redis env - var redis_env = (process.env.REDISCLOUD_URL || process.env.REDIS_URL); + var redis_env = process.env.REDISCLOUD_URL || process.env.REDIS_URL; var redis_url = redis_env ? url.parse(redis_env) : {}; redis_url.port = redis_url.port || 6379; redis_url.hostname = redis_url.hostname || "localhost"; @@ -23,39 +21,19 @@ function connect_redis() { } redis.on("ready", function() { logging.log("Redis connection established."); - if (process.env.HEROKU) { - logging.log("Running on heroku, flushing redis"); + if (process.env.EPHEMERAL_STORAGE) { + logging.log("Storage is ephemeral, flushing redis"); redis.flushall(); } }); - redis.on("error", function (err) { + redis.on("error", function(err) { logging.error(err); }); - redis.on("end", function () { + redis.on("end", function() { logging.warn("Redis connection lost!"); }); } -// sets the date of the face file belonging to +skin_hash+ to now -// the helms file is ignored because we only need 1 file to read/write from -function update_file_date(rid, skin_hash) { - if (skin_hash) { - var face_path = path.join(__dirname, "..", config.directories.faces, skin_hash + ".png"); - fs.exists(face_path, function(exists) { - if (exists) { - var date = new Date(); - fs.utimes(face_path, date, date, function(err) { - if (err) { - logging.error(rid, "Error:", err.stack); - } - }); - } else { - logging.error(rid, "tried to update", face_path + " date, but it does not exist"); - } - }); - } -} - var exp = {}; // returns the redis instance @@ -92,20 +70,19 @@ exp.info = function(callback) { }); }; -// sets the timestamp for +userId+ and its face file's (+hash+) date to the current time +// sets the timestamp for +userId+ // if +temp+ is true, the timestamp is set so that the record will be outdated after 60 seconds // these 60 seconds match the duration of Mojang's rate limit ban // callback: error -exp.update_timestamp = function(rid, userId, hash, temp, callback) { - logging.debug(rid, "updating cache timestamp"); - var sub = temp ? (config.caching.local - 60) : 0; +exp.update_timestamp = function(rid, userId, temp, callback) { + logging.debug(rid, "updating cache timestamp (" + temp + ")"); + var sub = temp ? config.caching.local - 60 : 0; var time = Date.now() - sub; // store userId in lower case if not null userId = userId && userId.toLowerCase(); redis.hmset(userId, "t", time, function(err) { callback(err); }); - update_file_date(rid, hash); }; // create the key +userId+, store +skin_hash+, +cape_hash+, +slim+ and current time @@ -114,12 +91,10 @@ exp.update_timestamp = function(rid, userId, hash, temp, callback) { // +slim+ can be true (alex) or false (steve) // +callback+ contans error exp.save_hash = function(rid, userId, skin_hash, cape_hash, slim, callback) { - logging.debug(rid, "caching skin:" + skin_hash + " cape:" + cape_hash); - + logging.debug(rid, "caching skin:" + skin_hash + " cape:" + cape_hash + " slim:" + slim); // store shorter null value instead of "null" string skin_hash = skin_hash === null ? "" : skin_hash; cape_hash = cape_hash === null ? "" : cape_hash; - // store userId in lower case if not null userId = userId && userId.toLowerCase(); diff --git a/lib/cleaner.js b/lib/cleaner.js index 53924c5..21c032d 100644 --- a/lib/cleaner.js +++ b/lib/cleaner.js @@ -20,10 +20,11 @@ function should_clean_redis(callback) { } else { try { // logging.debug(info.toString()); - logging.debug("used mem:" + info.used_memory); var used = parseInt(info.used_memory) / 1024; - logging.log("RedisCleaner:", used + "KB used"); - callback(err, used >= config.cleaner.redis_limit); + var result = used >= config.cleaner.redis_limit; + var msg = "RedisCleaner: " + used + "KB used"; + (result ? logging.log : logging.debug)(msg); + callback(err, result); } catch(e) { callback(e, false); } @@ -35,17 +36,19 @@ function should_clean_redis(callback) { // callback: error, true|false function should_clean_disk(callback) { df({ - file: path.join(__dirname, "..", config.directories.faces), + file: config.directories.faces, prefixMultiplier: "KiB", isDisplayPrefixMultiplier: false, precision: 2 - }, function (err, response) { + }, function(err, response) { if (err) { callback(err, false); } else { var available = response[0].available; - logging.log("DiskCleaner:", available + "KB available"); - callback(err, available < config.cleaner.disk_limit); + var result = available < config.cleaner.disk_limit; + var msg = "DiskCleaner: " + available + "KB available"; + (result ? logging.log : logging.debug)(msg); + callback(err, result); } }); } @@ -71,28 +74,45 @@ exp.run = function() { logging.error(err); } else if (clean) { logging.warn("DiskCleaner: Disk limit reached! Cleaning images now"); - var facesdir = path.join(__dirname, "..", config.directories.faces); - var helmdir = path.join(__dirname, "..", config.directories.helms); - var renderdir = path.join(__dirname, "..", config.directories.renders); - var skindir = path.join(__dirname, "..", config.directories.skins); - fs.readdir(facesdir, function (readerr, files) { + + // hotfix for #139 | FIXME + logging.warn("DiskCleaner: Flushing Redis to prevent ENOENT"); + redis.flushall(); + // end hotfix + + var skinsdir = config.directories.skins; + var capesdir = config.directories.capes; + var facesdir = config.directories.faces; + var helmsdir = config.directories.helms; + var rendersdir = config.directories.renders; + fs.readdir(skinsdir, function(readerr, files) { if (!readerr) { for (var i = 0, l = Math.min(files.length, config.cleaner.amount); i < l; i++) { var filename = files[i]; if (filename[0] !== ".") { fs.unlink(path.join(facesdir, filename), nil); - fs.unlink(path.join(helmdir, filename), nil); - fs.unlink(path.join(skindir, filename), nil); + fs.unlink(path.join(helmsdir, filename), nil); + fs.unlink(path.join(skinsdir, filename), nil); } } } }); - fs.readdir(renderdir, function (readerr, files) { + fs.readdir(rendersdir, function(readerr, files) { if (!readerr) { for (var j = 0, l = Math.min(files.length, config.cleaner.amount); j < l; j++) { var filename = files[j]; if (filename[0] !== ".") { - fs.unlink(renderdir + filename, nil); + fs.unlink(rendersdir + filename, nil); + } + } + } + }); + fs.readdir(capesdir, function(readerr, files) { + if (!readerr) { + for (var j = 0, l = Math.min(files.length, config.cleaner.amount); j < l; j++) { + var filename = files[j]; + if (filename[0] !== ".") { + fs.unlink(capesdir + filename, nil); } } } diff --git a/lib/helpers.js b/lib/helpers.js index 4fcd367..5859b7a 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -25,14 +25,14 @@ function store_skin(rid, userId, profile, cache_details, callback) { if (!err && url) { var skin_hash = get_hash(url); if (cache_details && cache_details.skin === skin_hash) { - cache.update_timestamp(rid, userId, skin_hash, false, function(cache_err) { + cache.update_timestamp(rid, userId, false, function(cache_err) { callback(cache_err, skin_hash, slim); }); } else { logging.debug(rid, "new skin hash:", skin_hash); - var facepath = path.join(__dirname, "..", config.directories.faces, skin_hash + ".png"); - var helmpath = path.join(__dirname, "..", config.directories.helms, skin_hash + ".png"); - var skinpath = path.join(__dirname, "..", config.directories.skins, skin_hash + ".png"); + var facepath = path.join(config.directories.faces, skin_hash + ".png"); + var helmpath = path.join(config.directories.helms, skin_hash + ".png"); + var skinpath = path.join(config.directories.skins, skin_hash + ".png"); fs.exists(facepath, function(exists) { if (exists) { logging.debug(rid, "skin already exists, not downloading"); @@ -42,14 +42,12 @@ function store_skin(rid, userId, profile, cache_details, callback) { if (err1 || !img) { callback(err1, null, slim); } else { - skins.save_image(img, skinpath, function(skin_err) { + skins.save_image(img, skinpath, function(skin_err, skin_img) { if (skin_err) { - logging.error(rid, skin_err); callback(skin_err, null, slim); } else { skins.extract_face(img, facepath, function(err2) { if (err2) { - logging.error(rid, err2.stack); callback(err2, null, slim); } else { logging.debug(rid, "face extracted"); @@ -82,12 +80,12 @@ function store_cape(rid, userId, profile, cache_details, callback) { if (!err && url) { var cape_hash = get_hash(url); if (cache_details && cache_details.cape === cape_hash) { - cache.update_timestamp(rid, userId, cape_hash, false, function(cache_err) { + cache.update_timestamp(rid, userId, false, function(cache_err) { callback(cache_err, cape_hash); }); } else { logging.debug(rid, "new cape hash:", cape_hash); - var capepath = path.join(__dirname, "..", config.directories.capes, cape_hash + ".png"); + var capepath = path.join(config.directories.capes, cape_hash + ".png"); fs.exists(capepath, function(exists) { if (exists) { logging.debug(rid, "cape already exists, not downloading"); @@ -95,10 +93,9 @@ function store_cape(rid, userId, profile, cache_details, callback) { } else { networking.get_from(rid, url, function(img, response, net_err) { if (net_err || !img) { - logging.error(rid, net_err.stack); callback(net_err, null); } else { - skins.save_image(img, capepath, function(skin_err) { + skins.save_image(img, capepath, function(skin_err, skin_img) { logging.debug(rid, "cape saved"); callback(skin_err, cape_hash); }); @@ -122,15 +119,18 @@ var requests = { }; function push_request(userId, type, fun) { - if (!requests[type][userId]) { - requests[type][userId] = []; + // avoid special properties (e.g. 'constructor') + var userId_safe = "!" + userId; + if (!requests[type][userId_safe]) { + requests[type][userId_safe] = []; } - requests[type][userId].push(fun); + requests[type][userId_safe].push(fun); } // calls back all queued requests that match userId and type function resume(userId, type, err, hash, slim) { - var callbacks = requests[type][userId]; + var userId_safe = "!" + userId; + var callbacks = requests[type][userId_safe]; if (callbacks) { if (callbacks.length > 1) { logging.debug(callbacks.length, "simultaneous requests for", userId); @@ -145,17 +145,17 @@ function resume(userId, type, err, hash, slim) { } // it's still an empty array - delete requests[type][userId]; + delete requests[type][userId_safe]; } } // downloads the images for +userId+ while checking the cache // status based on +cache_details+. +type+ specifies which // image type should be called back on -// callback: error, image hash +// callback: error, image hash, slim function store_images(rid, userId, cache_details, type, callback) { var is_uuid = userId.length > 16; - if (requests[type][userId]) { + if (requests[type]["!" + userId]) { logging.debug(rid, "adding to request queue"); push_request(userId, type, callback); } else { @@ -229,8 +229,9 @@ exp.get_image_hash = function(rid, userId, type, callback) { callback(null, (cached_hash ? 1 : 0), cached_hash, cache_details.slim); } else { // download image - if (cache_details) { + if (cache_details && cache_details[type] !== undefined) { logging.debug(rid, "userId cached, but too old"); + logging.debug(rid, JSON.stringify(cache_details)); } else { logging.debug(rid, "userId not cached"); } @@ -238,7 +239,7 @@ exp.get_image_hash = function(rid, userId, type, callback) { if (store_err) { // we might have a cached hash although an error occured // (e.g. Mojang servers not reachable, using outdated hash) - cache.update_timestamp(rid, userId, cached_hash, true, function(err2) { + cache.update_timestamp(rid, userId, true, function(err2) { callback(err2 || store_err, -1, cache_details && cached_hash, slim); }); } else { @@ -256,16 +257,16 @@ exp.get_image_hash = function(rid, userId, type, callback) { // handles requests for +userId+ avatars with +size+ // callback: error, status, image buffer, skin hash -// image is the user's face+helm when helm is true, or the face otherwise +// image is the user's face+overlay when overlay is true, or the face otherwise // for status, see get_image_hash -exp.get_avatar = function(rid, userId, helm, size, callback) { +exp.get_avatar = function(rid, userId, overlay, size, callback) { exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash, slim) { if (skin_hash) { - var facepath = path.join(__dirname, "..", config.directories.faces, skin_hash + ".png"); - var helmpath = path.join(__dirname, "..", config.directories.helms, skin_hash + ".png"); + var facepath = path.join(config.directories.faces, skin_hash + ".png"); + var helmpath = path.join(config.directories.helms, skin_hash + ".png"); var filepath = facepath; fs.exists(helmpath, function(exists) { - if (helm && exists) { + if (overlay && exists) { filepath = helmpath; } skins.resize_img(filepath, size, function(img_err, image) { @@ -284,11 +285,11 @@ exp.get_avatar = function(rid, userId, helm, size, callback) { }; // handles requests for +userId+ skins -// callback: error, skin hash, status, image buffer +// callback: error, skin hash, status, image buffer, slim exp.get_skin = function(rid, userId, callback) { exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash, slim) { if (skin_hash) { - var skinpath = path.join(__dirname, "..", config.directories.skins, skin_hash + ".png"); + var skinpath = path.join(config.directories.skins, skin_hash + ".png"); fs.exists(skinpath, function(exists) { if (exists) { logging.debug(rid, "skin already exists, not downloading"); @@ -308,22 +309,22 @@ exp.get_skin = function(rid, userId, callback) { }; // helper method used for file names -// possible returned names based on +helm+ and +body+ are: +// possible returned names based on +overlay+ and +body+ are: // body, bodyhelm, head, headhelm -function get_type(helm, body) { +function get_type(overlay, body) { var text = body ? "body" : "head"; - return helm ? text + "helm" : text; + return overlay ? text + "helm" : text; } // handles creations of 3D renders // callback: error, skin hash, image buffer -exp.get_render = function(rid, userId, scale, helm, body, callback) { +exp.get_render = function(rid, userId, scale, overlay, body, callback) { exp.get_skin(rid, userId, function(err, skin_hash, status, img, slim) { if (!skin_hash) { callback(err, status, skin_hash, null); return; } - var renderpath = path.join(__dirname, "..", config.directories.renders, [skin_hash, scale, get_type(helm, body), slim ? "s" : "t"].join("-") + ".png"); + var renderpath = path.join(config.directories.renders, [skin_hash, scale, get_type(overlay, body), slim ? "s" : "t"].join("-") + ".png"); fs.exists(renderpath, function(exists) { if (exists) { renders.open_render(rid, renderpath, function(render_err, rendered_img) { @@ -335,17 +336,14 @@ exp.get_render = function(rid, userId, scale, helm, body, callback) { callback(err, 0, skin_hash, null); return; } - renders.draw_model(rid, img, scale, helm, body, slim, function(draw_err, drawn_img) { + renders.draw_model(rid, img, scale, overlay, body, slim, function(draw_err, drawn_img) { if (draw_err) { callback(draw_err, -1, skin_hash, null); } else if (!drawn_img) { callback(null, 0, skin_hash, null); } else { fs.writeFile(renderpath, drawn_img, "binary", function(fs_err) { - if (fs_err) { - logging.error(rid, fs_err.stack); - } - callback(null, 2, skin_hash, drawn_img); + callback(fs_err, 2, skin_hash, drawn_img); }); } }); @@ -362,7 +360,7 @@ exp.get_cape = function(rid, userId, callback) { callback(err, null, status, null); return; } - var capepath = path.join(__dirname, "..", config.directories.capes, cape_hash + ".png"); + var capepath = path.join(config.directories.capes, cape_hash + ".png"); fs.exists(capepath, function(exists) { if (exists) { logging.debug(rid, "cape already exists, not downloading"); diff --git a/lib/logging.js b/lib/logging.js index b39844f..668bd89 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -1,4 +1,3 @@ -var cluster = require("cluster"); var config = require("../config"); var exp = {}; @@ -18,10 +17,9 @@ function join_args(args) { function log(level, args, logger) { logger = logger || console.log; var time = config.server.log_time ? new Date().toISOString() + " " : ""; - var clid = (cluster.worker && cluster.worker.id || "M"); var lines = join_args(args).split("\n"); for (var i = 0, l = lines.length; i < l; i++) { - logger(time + clid, level + ":", lines[i]); + logger(time, level + ":", lines[i]); } } diff --git a/lib/networking.js b/lib/networking.js index 0aca57a..cbeba48 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -76,35 +76,56 @@ exp.get_from_options = function(rid, url, options, callback) { }, timeout: config.server.http_timeout, followRedirect: false, - encoding: (options.encoding || null), + encoding: options.encoding || null, }, function(error, response, body) { // log url + code + description var code = response && response.statusCode; - if (error) { - logging.error(rid, url, error); - } else { - var logfunc = code && code < 405 ? logging.debug : logging.warn; - logfunc(rid, url, code, http_code[code]); + + var logfunc = code && code < 405 ? logging.debug : logging.warn; + logfunc(rid, url, code || error && error.code, http_code[code]); + + // not necessarily used + var e = new Error(code); + e.name = "HTTP"; + e.code = "HTTPERROR"; + + switch (code) { + case 200: + case 301: + case 302: // never seen, but mojang might use it in future + case 307: // never seen, but mojang might use it in future + case 308: // never seen, but mojang might use it in future + // these are okay + break; + case 204: // no content, used like 404 by mojang. making sure it really has no content + case 404: + // can be cached as null + body = null; + break; + case 429: // this shouldn't usually happen, but occasionally does + case 500: + case 503: + case 504: + // we don't want to cache this + error = error || e; + body = null; + break; + default: + if (!error) { + // Probably 500 or the likes + logging.error(rid, "Unexpected response:", code, body); + } + error = error || e; + body = null; + break; } - // 200 or 301 depending on content type - if (!error && (code === 200 || code === 301)) { - // response received successfully - callback(body, response, null); - } else if (error) { - callback(body || null, response, error); - } else if (code === 404 || code === 204) { - // page does not exist - callback(null, response, null); - } else if (code === 429) { - // Too Many Requests exception - code 429 - // cause error so the image will not be cached - callback(body || null, response, (error || "TooManyRequests")); - } else { - logging.error(rid, " Unknown reply:"); - logging.error(rid, JSON.stringify(response)); - callback(body || null, response, error); + if (body && !body.length) { + // empty response + body = null; } + + callback(body, response, error); }); }; @@ -161,7 +182,18 @@ exp.get_profile = function(rid, uuid, callback) { callback(null, null); } else { exp.get_from_options(rid, session_url + uuid, { encoding: "utf8" }, function(body, response, err) { - callback(err || null, (body !== null ? JSON.parse(body) : null)); + try { + body = body ? JSON.parse(body) : null; + callback(err || null, body); + } catch(e) { + if (e instanceof SyntaxError) { + logging.warn(rid, "Failed to parse JSON", e); + logging.debug(rid, body); + callback(err || null, null); + } else { + throw e; + } + } }); } }; @@ -187,11 +219,10 @@ exp.save_texture = function(rid, tex_hash, outpath, callback) { var textureurl = textures_url + tex_hash; exp.get_from(rid, textureurl, function(img, response, err) { if (err) { - logging.error(rid, "error while downloading texture"); callback(err, response, null); } else { - skins.save_image(img, outpath, function(img_err) { - callback(img_err, response, img); + skins.save_image(img, outpath, function(img_err, saved_img) { + callback(img_err, response, saved_img); }); } }); diff --git a/lib/public/images/akliz.png b/lib/public/images/akliz.png index 3783439..ba4d2a6 100644 Binary files a/lib/public/images/akliz.png and b/lib/public/images/akliz.png differ diff --git a/lib/public/images/alex.png b/lib/public/images/mhf_alex.png similarity index 100% rename from lib/public/images/alex.png rename to lib/public/images/mhf_alex.png diff --git a/lib/public/images/alex_skin.png b/lib/public/images/mhf_alex_skin.png similarity index 100% rename from lib/public/images/alex_skin.png rename to lib/public/images/mhf_alex_skin.png diff --git a/lib/public/images/steve.png b/lib/public/images/mhf_steve.png similarity index 100% rename from lib/public/images/steve.png rename to lib/public/images/mhf_steve.png diff --git a/lib/public/images/steve_skin.png b/lib/public/images/mhf_steve_skin.png similarity index 100% rename from lib/public/images/steve_skin.png rename to lib/public/images/mhf_steve_skin.png diff --git a/lib/public/images/twitter.png b/lib/public/images/twitter.png deleted file mode 100644 index 39c805a..0000000 Binary files a/lib/public/images/twitter.png and /dev/null differ diff --git a/lib/public/javascript/crafatar.js b/lib/public/javascript/crafatar.js new file mode 100644 index 0000000..ee828b8 --- /dev/null +++ b/lib/public/javascript/crafatar.js @@ -0,0 +1,60 @@ +var valid_user_id = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username +var xhr = new XMLHttpRequest(); + +xhr.onload = function() { + var response = JSON.parse(xhr.responseText); + var status = {}; + response.map(function(elem) { + var key = Object.keys(elem)[0]; + status[key] = elem[key]; + }); + + var textures = status["textures.minecraft.net"] !== "green"; + var session = status["sessionserver.mojang.com"] !== "green"; + var skins = status["skins.minecraft.net"] !== "green"; + var error = null; + + if (textures || session && skins) { + error = "all"; + } else if (skins) { + error = "username"; + } else if (session) { + error = "UUID"; + } + + if (error) { + var warn = document.createElement("div"); + warn.setAttribute("class", "alert alert-warning"); + warn.setAttribute("role", "alert"); + warn.innerHTML = "
Mojang issues
Mojang's servers are having trouble right now, this may affect " + error + " requests at Crafatar. check status"; + document.querySelector("#alerts").appendChild(warn); + } +}; + +document.addEventListener("DOMContentLoaded", function(event) { + var avatars = document.querySelector("#avatar-wrapper"); + for (var i = 0; i < avatars.children.length; i++) { + // shake 'em on down! + // https://stackoverflow.com/a/11972692/2517068 + avatars.appendChild(avatars.children[Math.random() * i | 0]); + } + + var tryit = document.querySelector("#tryit"); + var tryname = document.querySelector("#tryname"); + var images = document.querySelectorAll(".tryit"); + tryit.onsubmit = function(e) { + e.preventDefault(); + tryname.value = tryname.value.trim(); + var value = tryname.value || "853c80ef3c3749fdaa49938b674adae6"; + if (!valid_user_id.test(value)) { + tryname.value = ""; + return; + } + for (var j = 0; j < images.length; j++) { + images[j].src = images[j].dataset.src.replace("$", value); + } + }; + + xhr.open("GET", "https://status.mojang.com/check", true); + xhr.send(); +}); \ No newline at end of file diff --git a/lib/public/stylesheets/bootstrap.min.css b/lib/public/stylesheets/bootstrap.min.css new file mode 100644 index 0000000..e22f474 --- /dev/null +++ b/lib/public/stylesheets/bootstrap.min.css @@ -0,0 +1,5 @@ +@charset "UTF-8";/*! + * Bootstrap v4.0.0-alpha (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */img,legend{border:0}dl,ol,p,pre,ul{margin-top:0}address,dl,ol,p,ul{margin-bottom:1rem}b,dt,optgroup,strong{font-weight:700}caption,th{text-align:left}fieldset,legend,td,th{padding:0}pre,textarea{overflow:auto}.btn-group>.btn-group,.btn-toolbar .btn-group,.btn-toolbar .input-group,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.dropdown-menu,.table-reflow thead,.table-reflow tr{float:left}.btn,.c-indicator{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal:after,.dropdown-item,.modal-footer:after,.modal-header:after,.nav-tabs:after,.navbar:after,.pager:after,.row:after{clear:both}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{vertical-align:middle}svg:not(:root){overflow:hidden}hr{height:0;-webkit-box-sizing:content-box;box-sizing:content-box}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}address,legend{line-height:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:none}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}textarea{resize:vertical}table{border-spacing:0;border-collapse:collapse}@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{text-shadow:none!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:16px;-webkit-tap-highlight-color:transparent}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}@-ms-viewport{width:device-width}@viewport{width:device-width}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:1rem;line-height:1.5;color:#373a3c;background-color:#fff}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #818a91}address{font-style:normal}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dd,label{margin-bottom:.5rem}dd{margin-left:0}blockquote,figure{margin:0 0 1rem}a{color:#0275d8;text-decoration:none}a:focus,a:hover{color:#014c8c;text-decoration:underline}a:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}[role=button]{cursor:pointer}table{background-color:transparent}caption{padding-top:.75rem;padding-bottom:.75rem;color:#818a91;caption-side:bottom}label{display:inline-block}button,input,select,textarea{margin:0;line-height:inherit}fieldset{min-width:0;margin:0;border:0}legend{display:block;width:100%;margin-bottom:.5rem;font-size:1.5rem}.list-inline>li,output{display:inline-block}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit;margin-bottom:.5rem}.display-1,.display-2,.display-3,.display-4,.lead{font-weight:300}.blockquote,hr{margin-bottom:1rem}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem}.display-1{font-size:3.5rem}.display-2{font-size:4.5rem}.display-3{font-size:5.5rem}.display-4{font-size:6rem}hr{margin-top:1rem;border:0;border-top:.0625rem solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-inline,.list-unstyled{padding-left:0;list-style:none}.list-inline{margin-left:-5px}.list-inline>li{padding-right:5px;padding-left:5px}.dl-horizontal{margin-right:-1.875rem;margin-left:-1.875rem}.container,.container-fluid{margin-right:auto;margin-left:auto}.dl-horizontal:after,.dl-horizontal:before{display:table;content:" "}.initialism{font-size:90%;text-transform:uppercase}.blockquote{padding:.5rem 1rem;font-size:1.25rem;border-left:.25rem solid #eceeef}.blockquote ol:last-child,.blockquote p:last-child,.blockquote ul:last-child{margin-bottom:0}.blockquote footer{display:block;font-size:80%;line-height:1.5;color:#818a91}.blockquote footer:before{content:"\2014 \00A0"}.blockquote-reverse{padding-right:1rem;padding-left:0;text-align:right;border-right:.25rem solid #eceeef;border-left:0}.blockquote-reverse footer:before{content:""}.blockquote-reverse footer:after{content:"\00A0 \2014"}.figure{display:inline-block}.figure>img{margin-bottom:.5rem;line-height:1}.table,pre{margin-bottom:1rem}.figure-caption{font-size:90%;color:#818a91}.carousel-inner>.carousel-item>a>img,.carousel-inner>.carousel-item>img,.figure>img,.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:.3rem}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:.25rem;line-height:1.5;background-color:#fff;border:1px solid #ddd;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}code,kbd{padding:.2rem .4rem;font-size:90%}.img-circle{border-radius:50%}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{color:#bd4147;background-color:#f7f7f9;border-radius:.25rem}kbd{color:#fff;background-color:#333;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:90%;line-height:1.5;color:#373a3c}.container-fluid:after,.container-fluid:before,.container:after,.container:before,.row:after,.row:before{display:table;content:" "}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.container,.container-fluid{padding-right:.9375rem;padding-left:.9375rem}.pre-scrollable{max-height:340px;overflow-y:scroll}.row{margin-right:-.9375rem;margin-left:-.9375rem}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:.9375rem;padding-left:.9375rem}.col-xs-1{width:8.333333%}.col-xs-2{width:16.666667%}.col-xs-3{width:25%}.col-xs-4{width:33.333333%}.col-xs-5{width:41.666667%}.col-xs-6{width:50%}.col-xs-7{width:58.333333%}.col-xs-8{width:66.666667%}.col-xs-9{width:75%}.col-xs-10{width:83.333333%}.col-xs-11{width:91.666667%}.col-xs-12{width:100%}.col-xs-pull-0{right:auto}.col-xs-pull-1{right:8.333333%}.col-xs-pull-2{right:16.666667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.333333%}.col-xs-pull-5{right:41.666667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.333333%}.col-xs-pull-8{right:66.666667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.333333%}.col-xs-pull-11{right:91.666667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:auto}.col-xs-push-1{left:8.333333%}.col-xs-push-2{left:16.666667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.333333%}.col-xs-push-5{left:41.666667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.333333%}.col-xs-push-8{left:66.666667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.333333%}.col-xs-push-11{left:91.666667%}.col-xs-push-12{left:100%}.col-xs-offset-0{margin-left:0}.col-xs-offset-1{margin-left:8.333333%}.col-xs-offset-2{margin-left:16.666667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.333333%}.col-xs-offset-5{margin-left:41.666667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.333333%}.col-xs-offset-8{margin-left:66.666667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.333333%}.col-xs-offset-11{margin-left:91.666667%}.col-xs-offset-12{margin-left:100%}@media (min-width:34em){.container{max-width:34rem}.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-1{width:8.333333%}.col-sm-2{width:16.666667%}.col-sm-3{width:25%}.col-sm-4{width:33.333333%}.col-sm-5{width:41.666667%}.col-sm-6{width:50%}.col-sm-7{width:58.333333%}.col-sm-8{width:66.666667%}.col-sm-9{width:75%}.col-sm-10{width:83.333333%}.col-sm-11{width:91.666667%}.col-sm-12{width:100%}.col-sm-pull-0{right:auto}.col-sm-pull-1{right:8.333333%}.col-sm-pull-2{right:16.666667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.333333%}.col-sm-pull-5{right:41.666667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.333333%}.col-sm-pull-8{right:66.666667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.333333%}.col-sm-pull-11{right:91.666667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:auto}.col-sm-push-1{left:8.333333%}.col-sm-push-2{left:16.666667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.333333%}.col-sm-push-5{left:41.666667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.333333%}.col-sm-push-8{left:66.666667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.333333%}.col-sm-push-11{left:91.666667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0}.col-sm-offset-1{margin-left:8.333333%}.col-sm-offset-2{margin-left:16.666667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.333333%}.col-sm-offset-5{margin-left:41.666667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.333333%}.col-sm-offset-8{margin-left:66.666667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.333333%}.col-sm-offset-11{margin-left:91.666667%}.col-sm-offset-12{margin-left:100%}}@media (min-width:48em){.container{max-width:45rem}.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-1{width:8.333333%}.col-md-2{width:16.666667%}.col-md-3{width:25%}.col-md-4{width:33.333333%}.col-md-5{width:41.666667%}.col-md-6{width:50%}.col-md-7{width:58.333333%}.col-md-8{width:66.666667%}.col-md-9{width:75%}.col-md-10{width:83.333333%}.col-md-11{width:91.666667%}.col-md-12{width:100%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.333333%}.col-md-pull-2{right:16.666667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.333333%}.col-md-pull-5{right:41.666667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.333333%}.col-md-pull-8{right:66.666667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.333333%}.col-md-pull-11{right:91.666667%}.col-md-pull-12{right:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.333333%}.col-md-push-2{left:16.666667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.333333%}.col-md-push-5{left:41.666667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.333333%}.col-md-push-8{left:66.666667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.333333%}.col-md-push-11{left:91.666667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0}.col-md-offset-1{margin-left:8.333333%}.col-md-offset-2{margin-left:16.666667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.333333%}.col-md-offset-5{margin-left:41.666667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.333333%}.col-md-offset-8{margin-left:66.666667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.333333%}.col-md-offset-11{margin-left:91.666667%}.col-md-offset-12{margin-left:100%}}@media (min-width:62em){.container{max-width:60rem}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-1{width:8.333333%}.col-lg-2{width:16.666667%}.col-lg-3{width:25%}.col-lg-4{width:33.333333%}.col-lg-5{width:41.666667%}.col-lg-6{width:50%}.col-lg-7{width:58.333333%}.col-lg-8{width:66.666667%}.col-lg-9{width:75%}.col-lg-10{width:83.333333%}.col-lg-11{width:91.666667%}.col-lg-12{width:100%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.333333%}.col-lg-pull-2{right:16.666667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.333333%}.col-lg-pull-5{right:41.666667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.333333%}.col-lg-pull-8{right:66.666667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.333333%}.col-lg-pull-11{right:91.666667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.333333%}.col-lg-push-2{left:16.666667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.333333%}.col-lg-push-5{left:41.666667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.333333%}.col-lg-push-8{left:66.666667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.333333%}.col-lg-push-11{left:91.666667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0}.col-lg-offset-1{margin-left:8.333333%}.col-lg-offset-2{margin-left:16.666667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.333333%}.col-lg-offset-5{margin-left:41.666667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.333333%}.col-lg-offset-8{margin-left:66.666667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.333333%}.col-lg-offset-11{margin-left:91.666667%}.col-lg-offset-12{margin-left:100%}}@media (min-width:75em){.container{max-width:72.25rem}.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{float:left}.col-xl-1{width:8.333333%}.col-xl-2{width:16.666667%}.col-xl-3{width:25%}.col-xl-4{width:33.333333%}.col-xl-5{width:41.666667%}.col-xl-6{width:50%}.col-xl-7{width:58.333333%}.col-xl-8{width:66.666667%}.col-xl-9{width:75%}.col-xl-10{width:83.333333%}.col-xl-11{width:91.666667%}.col-xl-12{width:100%}.col-xl-pull-0{right:auto}.col-xl-pull-1{right:8.333333%}.col-xl-pull-2{right:16.666667%}.col-xl-pull-3{right:25%}.col-xl-pull-4{right:33.333333%}.col-xl-pull-5{right:41.666667%}.col-xl-pull-6{right:50%}.col-xl-pull-7{right:58.333333%}.col-xl-pull-8{right:66.666667%}.col-xl-pull-9{right:75%}.col-xl-pull-10{right:83.333333%}.col-xl-pull-11{right:91.666667%}.col-xl-pull-12{right:100%}.col-xl-push-0{left:auto}.col-xl-push-1{left:8.333333%}.col-xl-push-2{left:16.666667%}.col-xl-push-3{left:25%}.col-xl-push-4{left:33.333333%}.col-xl-push-5{left:41.666667%}.col-xl-push-6{left:50%}.col-xl-push-7{left:58.333333%}.col-xl-push-8{left:66.666667%}.col-xl-push-9{left:75%}.col-xl-push-10{left:83.333333%}.col-xl-push-11{left:91.666667%}.col-xl-push-12{left:100%}.col-xl-offset-0{margin-left:0}.col-xl-offset-1{margin-left:8.333333%}.col-xl-offset-2{margin-left:16.666667%}.col-xl-offset-3{margin-left:25%}.col-xl-offset-4{margin-left:33.333333%}.col-xl-offset-5{margin-left:41.666667%}.col-xl-offset-6{margin-left:50%}.col-xl-offset-7{margin-left:58.333333%}.col-xl-offset-8{margin-left:66.666667%}.col-xl-offset-9{margin-left:75%}.col-xl-offset-10{margin-left:83.333333%}.col-xl-offset-11{margin-left:91.666667%}.col-xl-offset-12{margin-left:100%}}.table{width:100%;max-width:100%}.table td,.table th{padding:.75rem;line-height:1.5;vertical-align:top;border-top:1px solid #eceeef}.table thead th{vertical-align:bottom;border-bottom:2px solid #eceeef}.table tbody+tbody{border-top:2px solid #eceeef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered,.table-bordered td,.table-bordered th{border:1px solid #eceeef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:#f9f9f9}.table-active,.table-active>td,.table-active>th,.table-hover tbody tr:hover{background-color:#f5f5f5}.table-hover .table-active:hover,.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:#e8e8e8}.table-success,.table-success>td,.table-success>th{background-color:#dff0d8}.table-hover .table-success:hover,.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d0e9c6}.table-info,.table-info>td,.table-info>th{background-color:#d9edf7}.table-hover .table-info:hover,.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#c4e3f3}.table-warning,.table-warning>td,.table-warning>th{background-color:#fcf8e3}.table-hover .table-warning:hover,.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#faf2cc}.table-danger,.table-danger>td,.table-danger>th{background-color:#f2dede}.table-hover .table-danger:hover,.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebcccc}.table-responsive{display:block;width:100%;overflow-x:auto}.collapsing,.dropdown-divider,.embed-responsive,.modal,.modal-open,.navbar-divider{overflow:hidden}.thead-inverse th{color:#fff;background-color:#373a3c}.thead-default th{color:#55595c;background-color:#eceeef}.table-inverse{color:#eceeef;background-color:#373a3c}.table-inverse.table-bordered{border:0}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#55595c}.table-reflow tbody{display:block;white-space:nowrap}.table-reflow td,.table-reflow th{border-top:1px solid #eceeef;border-left:1px solid #eceeef}.table-reflow td:last-child,.table-reflow th:last-child{border-right:1px solid #eceeef}.table-reflow tbody:last-child tr:last-child td,.table-reflow tbody:last-child tr:last-child th,.table-reflow tfoot:last-child tr:last-child td,.table-reflow tfoot:last-child tr:last-child th,.table-reflow thead:last-child tr:last-child td,.table-reflow thead:last-child tr:last-child th{border-bottom:1px solid #eceeef}.table-reflow tr td,.table-reflow tr th{display:block!important;border:1px solid #eceeef}.form-control,.form-control-file,.form-control-range{display:block}.form-control{width:100%;padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#55595c;background-color:#fff;background-image:none;border:.0625rem solid #ccc;border-radius:.25rem}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{border-color:#66afe9;outline:0}.form-control::-webkit-input-placeholder{color:#999;opacity:1}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999;opacity:1}.form-control::placeholder{color:#999;opacity:1}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#5cb85c}.form-control:disabled,.form-control[readonly],fieldset[disabled] .form-control{background-color:#eceeef;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}.form-control-label{padding:.4375rem .75rem;margin-bottom:0}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:2.375rem}.input-group-sm input[type=date].form-control,.input-group-sm input[type=time].form-control,.input-group-sm input[type=datetime-local].form-control,.input-group-sm input[type=month].form-control,input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:1.95rem}.input-group-lg input[type=date].form-control,.input-group-lg input[type=time].form-control,.input-group-lg input[type=datetime-local].form-control,.input-group-lg input[type=month].form-control,input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:3.291667rem}}.form-control-static{min-height:2.375rem;padding-top:.4375rem;padding-bottom:.4375rem;margin-bottom:0}.form-control-static.form-control-lg,.form-control-static.form-control-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.275rem .75rem;font-size:.85rem;line-height:1.5;border-radius:.2rem}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.75rem 1.25rem;font-size:1.25rem;line-height:1.333333;border-radius:.3rem}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-bottom:.75rem}.checkbox label,.checkbox-inline,.radio label,.radio-inline{padding-left:1.25rem;margin-bottom:0;cursor:pointer;font-weight:400}.checkbox label input:only-child,.radio label input:only-child{position:static}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.collapsing,.dropdown,.dropup{position:relative}.checkbox+.checkbox,.radio+.radio{margin-top:-.25rem}.checkbox-inline,.radio-inline{position:relative;display:inline-block;vertical-align:middle}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:.75rem}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox]:disabled,input[type=radio].disabled,input[type=radio]:disabled{cursor:not-allowed}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio label,fieldset[disabled] .radio-inline{cursor:not-allowed}.form-control-error,.form-control-success,.form-control-warning{padding-right:2.25rem;background-repeat:no-repeat;background-position:center right .59375rem;-webkit-background-size:1.54375rem 1.54375rem;background-size:1.54375rem 1.54375rem}.has-success .form-control{border-color:#5cb85c}.has-success .input-group-addon{color:#5cb85c;background-color:#eaf6ea;border-color:#5cb85c}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#f0ad4e}.has-success .form-control-success{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNoZWNrIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDYxMiA3OTIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDYxMiA3OTIiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGZpbGw9IiM1Q0I4NUMiIGQ9Ik0yMzMuOCw2MTAuMWMtMTMuMywwLTI1LjktNi4yLTM0LTE2LjlMOTAuNSw0NDguOEM3Ni4zLDQzMCw4MCw0MDMuMyw5OC44LDM4OS4xYzE4LjgtMTQuMyw0NS41LTEwLjUsNTkuOCw4LjNsNzEuOSw5NWwyMjAuOS0yNTAuNWMxMi41LTIwLDM4LjgtMjYuMSw1OC44LTEzLjZjMjAsMTIuNCwyNi4xLDM4LjcsMTMuNiw1OC44TDI3MCw1OTBjLTcuNCwxMi0yMC4yLDE5LjQtMzQuMywyMC4xQzIzNS4xLDYxMC4xLDIzNC41LDYxMC4xLDIzMy44LDYxMC4xeiIvPjwvc3ZnPg==)}.has-warning .form-control{border-color:#f0ad4e}.has-warning .input-group-addon{color:#f0ad4e;background-color:#fff;border-color:#f0ad4e}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#d9534f}.has-warning .form-control-warning{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ildhcm5pbmciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNjEyIDc5MiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgNjEyIDc5MiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZmlsbD0iI0YwQUQ0RSIgZD0iTTYwMyw2NDAuMmwtMjc4LjUtNTA5Yy0zLjgtNi42LTEwLjgtMTAuNi0xOC41LTEwLjZzLTE0LjcsNC4xLTE4LjUsMTAuNkw5LDY0MC4yYy0zLjcsNi41LTMuNiwxNC40LDAuMiwyMC44YzMuOCw2LjUsMTAuOCwxMC40LDE4LjMsMTAuNGg1NTcuMWM3LjUsMCwxNC41LTMuOSwxOC4zLTEwLjRDNjA2LjYsNjU0LjYsNjA2LjcsNjQ2LjYsNjAzLDY0MC4yeiBNMzM2LjYsNjEwLjJoLTYxLjJWNTQ5aDYxLjJWNjEwLjJ6IE0zMzYuNiw1MDMuMWgtNjEuMlYzMDQuMmg2MS4yVjUwMy4xeiIvPjwvc3ZnPg==)}.has-error .form-control{border-color:#d9534f}.has-error .input-group-addon{color:#d9534f;background-color:#fdf7f7;border-color:#d9534f}.has-error .form-control-error{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNyb3NzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDYxMiA3OTIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDYxMiA3OTIiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGZpbGw9IiNEOTUzNEYiIGQ9Ik00NDcsNTQ0LjRjLTE0LjQsMTQuNC0zNy42LDE0LjQtNTEuOSwwTDMwNiw0NTEuN2wtODkuMSw5Mi43Yy0xNC40LDE0LjQtMzcuNiwxNC40LTUxLjksMGMtMTQuNC0xNC40LTE0LjQtMzcuNiwwLTUxLjlsOTIuNC05Ni40TDE2NSwyOTkuNmMtMTQuNC0xNC40LTE0LjQtMzcuNiwwLTUxLjlzMzcuNi0xNC40LDUxLjksMGw4OS4yLDkyLjdsODkuMS05Mi43YzE0LjQtMTQuNCwzNy42LTE0LjQsNTEuOSwwYzE0LjQsMTQuNCwxNC40LDM3LjYsMCw1MS45TDM1NC43LDM5Nmw5Mi40LDk2LjRDNDYxLjQsNTA2LjgsNDYxLjQsNTMwLDQ0Nyw1NDQuNHoiLz48L3N2Zz4=)}.btn-danger-outline,.btn-info-outline,.btn-info.active,.btn-info:active,.btn-primary-outline,.btn-primary.active,.btn-primary:active,.btn-secondary-outline,.btn-secondary.active,.btn-secondary:active,.btn-success-outline,.btn-success.active,.btn-success:active,.btn-warning-outline,.btn-warning.active,.btn-warning:active,.btn.active,.btn:active,.open>.btn-info.dropdown-toggle,.open>.btn-primary.dropdown-toggle,.open>.btn-secondary.dropdown-toggle,.open>.btn-success.dropdown-toggle,.open>.btn-warning.dropdown-toggle{background-image:none}@media (min-width:34em){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.btn-block,input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.btn{display:inline-block;padding:.375rem 1rem;font-size:1rem;font-weight:400;line-height:1.5;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;user-select:none;border:.0625rem solid transparent;border-radius:.25rem}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{text-decoration:none}.btn.active,.btn:active{outline:0}.btn.disabled,.btn:disabled,fieldset[disabled] .btn{cursor:not-allowed;opacity:.65}a.btn.disaabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary.active,.btn-primary.focus,.btn-primary:active,.btn-primary:focus,.btn-primary:hover,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#025aa5;border-color:#01549b}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary:disabled.focus,.btn-primary:disabled:focus,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus{background-color:#0275d8;border-color:#0275d8}.btn-primary.disabled:hover,.btn-primary:disabled:hover,fieldset[disabled] .btn-primary:hover{background-color:#0275d8;border-color:#0275d8}.btn-secondary{color:#373a3c;background-color:#fff;border-color:#ccc}.btn-secondary.active,.btn-secondary.focus,.btn-secondary:active,.btn-secondary:focus,.btn-secondary:hover,.open>.btn-secondary.dropdown-toggle{color:#373a3c;background-color:#e6e6e6;border-color:#adadad}.btn-secondary.disabled.focus,.btn-secondary.disabled:focus,.btn-secondary:disabled.focus,.btn-secondary:disabled:focus,fieldset[disabled] .btn-secondary.focus,fieldset[disabled] .btn-secondary:focus{background-color:#fff;border-color:#ccc}.btn-secondary.disabled:hover,.btn-secondary:disabled:hover,fieldset[disabled] .btn-secondary:hover{background-color:#fff;border-color:#ccc}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info.active,.btn-info.focus,.btn-info:active,.btn-info:focus,.btn-info:hover,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info:disabled.focus,.btn-info:disabled:focus,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus{background-color:#5bc0de;border-color:#5bc0de}.btn-info.disabled:hover,.btn-info:disabled:hover,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#5bc0de}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success.active,.btn-success.focus,.btn-success:active,.btn-success:focus,.btn-success:hover,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success:disabled.focus,.btn-success:disabled:focus,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus{background-color:#5cb85c;border-color:#5cb85c}.btn-success.disabled:hover,.btn-success:disabled:hover,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#5cb85c}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.active,.btn-warning.focus,.btn-warning:active,.btn-warning:focus,.btn-warning:hover,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning:disabled.focus,.btn-warning:disabled:focus,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.disabled:hover,.btn-warning:disabled:hover,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#f0ad4e}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger.active,.btn-danger.focus,.btn-danger:active,.btn-danger:focus,.btn-danger:hover,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.active,.btn-danger:active,.open>.btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger:disabled.focus,.btn-danger:disabled:focus,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus{background-color:#d9534f;border-color:#d9534f}.btn-danger.disabled:hover,.btn-danger:disabled:hover,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d9534f}.btn-primary-outline{color:#0275d8;background-color:transparent;border-color:#0275d8}.btn-primary-outline.active,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline:focus,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary-outline.disabled.focus,.btn-primary-outline.disabled:focus,.btn-primary-outline:disabled.focus,.btn-primary-outline:disabled:focus,fieldset[disabled] .btn-primary-outline.focus,fieldset[disabled] .btn-primary-outline:focus{border-color:#43a7fd}.btn-primary-outline.disabled:hover,.btn-primary-outline:disabled:hover,fieldset[disabled] .btn-primary-outline:hover{border-color:#43a7fd}.btn-secondary-outline{color:#ccc;background-color:transparent;border-color:#ccc}.btn-secondary-outline.active,.btn-secondary-outline.focus,.btn-secondary-outline:active,.btn-secondary-outline:focus,.btn-secondary-outline:hover,.open>.btn-secondary-outline.dropdown-toggle{color:#fff;background-color:#ccc;border-color:#ccc}.btn-secondary-outline.disabled.focus,.btn-secondary-outline.disabled:focus,.btn-secondary-outline:disabled.focus,.btn-secondary-outline:disabled:focus,fieldset[disabled] .btn-secondary-outline.focus,fieldset[disabled] .btn-secondary-outline:focus{border-color:#fff}.btn-secondary-outline.disabled:hover,.btn-secondary-outline:disabled:hover,fieldset[disabled] .btn-secondary-outline:hover{border-color:#fff}.btn-info-outline{color:#5bc0de;background-color:transparent;border-color:#5bc0de}.btn-info-outline.active,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline:focus,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info-outline.disabled.focus,.btn-info-outline.disabled:focus,.btn-info-outline:disabled.focus,.btn-info-outline:disabled:focus,fieldset[disabled] .btn-info-outline.focus,fieldset[disabled] .btn-info-outline:focus{border-color:#b0e1ef}.btn-info-outline.disabled:hover,.btn-info-outline:disabled:hover,fieldset[disabled] .btn-info-outline:hover{border-color:#b0e1ef}.btn-success-outline{color:#5cb85c;background-color:transparent;border-color:#5cb85c}.btn-success-outline.active,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline:focus,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success-outline.disabled.focus,.btn-success-outline.disabled:focus,.btn-success-outline:disabled.focus,.btn-success-outline:disabled:focus,fieldset[disabled] .btn-success-outline.focus,fieldset[disabled] .btn-success-outline:focus{border-color:#a3d7a3}.btn-success-outline.disabled:hover,.btn-success-outline:disabled:hover,fieldset[disabled] .btn-success-outline:hover{border-color:#a3d7a3}.btn-warning-outline{color:#f0ad4e;background-color:transparent;border-color:#f0ad4e}.btn-warning-outline.active,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline:focus,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning-outline.disabled.focus,.btn-warning-outline.disabled:focus,.btn-warning-outline:disabled.focus,.btn-warning-outline:disabled:focus,fieldset[disabled] .btn-warning-outline.focus,fieldset[disabled] .btn-warning-outline:focus{border-color:#f8d9ac}.btn-warning-outline.disabled:hover,.btn-warning-outline:disabled:hover,fieldset[disabled] .btn-warning-outline:hover{border-color:#f8d9ac}.btn-danger-outline{color:#d9534f;background-color:transparent;border-color:#d9534f}.btn-danger-outline.active,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline:focus,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger-outline.disabled.focus,.btn-danger-outline.disabled:focus,.btn-danger-outline:disabled.focus,.btn-danger-outline:disabled:focus,fieldset[disabled] .btn-danger-outline.focus,fieldset[disabled] .btn-danger-outline:focus{border-color:#eba5a3}.btn-danger-outline.disabled:hover,.btn-danger-outline:disabled:hover,fieldset[disabled] .btn-danger-outline:hover{border-color:#eba5a3}.btn-link{font-weight:400;color:#0275d8;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled,fieldset[disabled] .btn-link{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#014c8c;text-decoration:underline;background-color:transparent}.btn-link:disabled:focus,.btn-link:disabled:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#818a91;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.25rem;font-size:1.25rem;line-height:1.333333;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .75rem;font-size:.85rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block}.btn-block+.btn-block{margin-top:5px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{height:0;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height;-o-transition-property:height;transition-property:height}.dropdown-toggle:after{display:inline-block;width:0;height:0;margin-left:.25rem;vertical-align:middle;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:1rem;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-header,.dropdown-item{display:block;padding:3px 20px;line-height:1.5;white-space:nowrap}.dropdown-divider{height:1px;margin:.5rem 0;background-color:#e5e5e5}.dropdown-item{width:100%;font-weight:400;color:#373a3c;text-align:inherit;background:0 0;border:0}.c-indicator,.label,.pager{text-align:center}.dropdown-item:focus,.dropdown-item:hover{color:#2b2d2f;text-decoration:none;background-color:#f5f5f5}.dropdown-item.active,.dropdown-item.active:focus,.dropdown-item.active:hover{color:#fff;text-decoration:none;background-color:#0275d8;outline:0}.dropdown-item.disabled,.dropdown-item.disabled:focus,.dropdown-item.disabled:hover{color:#818a91}.dropdown-item.disabled:focus,.dropdown-item.disabled:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:"progid:DXImageTransform.Microsoft.gradient(enabled = false)"}.c-input,.file{cursor:pointer}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{font-size:.85rem;color:#818a91}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover,.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:.3em solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar:after,.btn-toolbar:before{display:table;content:" "}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group-lg.btn-group>.btn+.dropdown-toggle,.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group-lg>.btn .caret,.btn-lg .caret{border-width:.3em .3em 0}.dropup .btn-group-lg>.btn .caret,.dropup .btn-lg .caret{border-width:0 .3em .3em}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:.25rem;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:.25rem}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.c-input,.input-group,.input-group-btn,.input-group-btn>.btn{position:relative}.input-group{display:table;border-collapse:separate}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1;color:#55595c;text-align:center;background-color:#eceeef;border:1px solid #ccc;border-radius:.25rem}.alert-link,.close,.label{font-weight:700}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.275rem .75rem;font-size:.85rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:1.25rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{font-size:0;white-space:nowrap}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.c-input{display:inline;padding-left:1.5rem;color:#555}.c-input>input{position:absolute;z-index:-1;opacity:0}.c-input>input:checked~.c-indicator{color:#fff;background-color:#0074d9}.c-input>input:active~.c-indicator{color:#fff;background-color:#84c6ff}.c-input+.c-input{margin-left:1rem}.c-indicator{position:absolute;top:0;left:0;display:block;width:1rem;height:1rem;font-size:65%;line-height:1rem;color:#eee;user-select:none;background-color:#eee;background-repeat:no-repeat;background-position:center center;-webkit-background-size:50% 50%;background-size:50% 50%}.c-checkbox .c-indicator{border-radius:.25rem}.c-checkbox input:checked~.c-indicator{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgOCA4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA4IDgiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTYuNCwxTDUuNywxLjdMMi45LDQuNUwyLjEsMy43TDEuNCwzTDAsNC40bDAuNywwLjdsMS41LDEuNWwwLjcsMC43bDAuNy0wLjdsMy41LTMuNWwwLjctMC43TDYuNCwxTDYuNCwxeiINCgkvPg0KPC9zdmc+DQo=)}.c-checkbox input:indeterminate~.c-indicator{background-color:#0074d9;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iOHB4IiBoZWlnaHQ9IjhweCIgdmlld0JveD0iMCAwIDggOCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOCA4IiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0wLDN2Mmg4VjNIMHoiLz4NCjwvc3ZnPg0K)}.c-radio .c-indicator{border-radius:50%}.c-radio input:checked~.c-indicator{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgOCA4IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA4IDgiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTQsMUMyLjMsMSwxLDIuMywxLDRzMS4zLDMsMywzczMtMS4zLDMtM1M1LjcsMSw0LDF6Ii8+DQo8L3N2Zz4NCg==)}.c-inputs-stacked .c-input{display:inline}.c-inputs-stacked .c-input:after{display:block;margin-bottom:.25rem;content:""}.c-select,.file{display:inline-block}.c-inputs-stacked .c-input+.c-input{margin-left:0}.c-select{max-width:100%;-webkit-appearance:none;padding:.375rem 1.75rem .375rem .75rem;padding-right:.75rem\9;vertical-align:middle;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAUCAMAAACzvE1FAAAADFBMVEUzMzMzMzMzMzMzMzMKAG/3AAAAA3RSTlMAf4C/aSLHAAAAPElEQVR42q3NMQ4AIAgEQTn//2cLdRKppSGzBYwzVXvznNWs8C58CiussPJj8h6NwgorrKRdTvuV9v16Afn0AYFOB7aYAAAAAElFTkSuQmCC) right .75rem center no-repeat #fff;background-image:none\9;-webkit-background-size:8px 10px;background-size:8px 10px;border:1px solid #ccc;-moz-appearance:none;appearance:none}.c-select:focus{border-color:#51a7e8;outline:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.075),0 0 5px rgba(81,167,232,.5);box-shadow:inset 0 1px 2px rgba(0,0,0,.075),0 0 5px rgba(81,167,232,.5)}.c-select::-ms-expand{opacity:0}.c-select-sm{padding-top:3px;padding-bottom:3px;font-size:12px}.c-select-sm:not([multiple]){height:26px;min-height:26px}.file{position:relative;height:2.5rem}.file-custom,.file-custom:before{position:absolute;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#555}.file input{min-width:14rem;margin:0;filter:alpha(opacity=0);opacity:0}.file-custom{top:0;right:0;left:0;z-index:5;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;border:.075rem solid #ddd;border-radius:.25rem;-webkit-box-shadow:inset 0 .2rem .4rem rgba(0,0,0,.05);box-shadow:inset 0 .2rem .4rem rgba(0,0,0,.05)}.file-custom:after{content:"Choose file..."}.file-custom:before{top:-.075rem;right:-.075rem;bottom:-.075rem;z-index:6;display:block;content:"Browse";background-color:#eee;border:.075rem solid #ddd;border-radius:0 .25rem .25rem 0}.file input:focus~.file-custom{-webkit-box-shadow:0 0 0 .075rem #fff,0 0 0 .2rem #0074d9;box-shadow:0 0 0 .075rem #fff,0 0 0 .2rem #0074d9}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:inline-block}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#818a91}.nav-link.disabled,.nav-link.disabled:focus,.nav-link.disabled:hover{color:#818a91;cursor:not-allowed;background-color:transparent}.nav-inline .nav-link+.nav-link{margin-left:1rem}.nav-pills .nav-item+.nav-item,.nav-tabs .nav-item+.nav-item{margin-left:.2rem}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs:after,.nav-tabs:before{display:table;content:" "}.nav-tabs .nav-item{float:left;margin-bottom:-1px}.nav-tabs .nav-link{display:block;padding:.5em 1em;border:1px solid transparent;border-radius:.25rem .25rem 0 0}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eceeef #eceeef #ddd}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link.disabled:focus,.nav-tabs .nav-link.disabled:hover{color:#818a91;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.open .nav-link,.nav-tabs .nav-item.open .nav-link:focus,.nav-tabs .nav-item.open .nav-link:hover,.nav-tabs .nav-link.active,.nav-tabs .nav-link.active:focus,.nav-tabs .nav-link.active:hover{color:#55595c;background-color:#fff;border-color:#ddd #ddd transparent}.nav-pills .nav-item{float:left}.nav-pills .nav-link{display:block;padding:.5em 1em;border-radius:.25rem}.nav-pills .nav-item.open .nav-link,.nav-pills .nav-item.open .nav-link:focus,.nav-pills .nav-item.open .nav-link:hover,.nav-pills .nav-link.active,.nav-pills .nav-link.active:focus,.nav-pills .nav-link.active:hover{color:#fff;cursor:default;background-color:#0275d8}.nav-stacked .nav-item{display:block;float:none}.nav-stacked .nav-item+.nav-item{margin-top:.2rem;margin-left:0}.navbar-divider,.navbar-nav .nav-item+.nav-item,.navbar-nav .nav-link+.nav-link{margin-left:1rem}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;padding:.5rem 1rem}.navbar:after,.navbar:before{display:table;content:" "}.navbar-static-top{z-index:1000}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.card,.card-title{margin-bottom:.75rem}.navbar-fixed-top{top:0}.navbar-fixed-bottom{bottom:0}.navbar-sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1030;width:100%}@media (min-width:34em){.navbar{border-radius:.25rem}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top,.navbar-sticky-top{border-radius:0}}.navbar-brand{float:left;padding-top:.25rem;padding-bottom:.25rem;margin-right:1rem;font-size:1.25rem}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}.navbar-divider{float:left;width:1px;padding-top:.425rem;padding-bottom:.425rem;margin-right:1rem}.navbar-divider:before{content:'\00a0'}.navbar-toggler{padding:.5rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:.0625rem solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}@media (min-width:34em){.navbar-toggleable-xs{display:block!important}}@media (min-width:48em){.navbar-toggleable-sm{display:block!important}}.navbar-nav .nav-item{float:left}.navbar-nav .nav-link{display:block;padding-top:.425rem;padding-bottom:.425rem}.navbar-light .navbar-brand,.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.8)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.6)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .active>.nav-link:focus,.navbar-light .navbar-nav .active>.nav-link:hover,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.active:focus,.navbar-light .navbar-nav .nav-link.active:hover,.navbar-light .navbar-nav .nav-link.open,.navbar-light .navbar-nav .nav-link.open:focus,.navbar-light .navbar-nav .nav-link.open:hover,.navbar-light .navbar-nav .open>.nav-link,.navbar-light .navbar-nav .open>.nav-link:focus,.navbar-light .navbar-nav .open>.nav-link:hover{color:rgba(0,0,0,.8)}.navbar-light .navbar-divider{background-color:rgba(0,0,0,.075)}.navbar-dark .navbar-brand,.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.card-inverse .card-blockquote,.card-inverse .card-footer,.card-inverse .card-header,.card-inverse .card-title,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .active>.nav-link:focus,.navbar-dark .navbar-nav .active>.nav-link:hover,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.active:focus,.navbar-dark .navbar-nav .nav-link.active:hover,.navbar-dark .navbar-nav .nav-link.open,.navbar-dark .navbar-nav .nav-link.open:focus,.navbar-dark .navbar-nav .nav-link.open:hover,.navbar-dark .navbar-nav .open>.nav-link,.navbar-dark .navbar-nav .open>.nav-link:focus,.navbar-dark .navbar-nav .open>.nav-link:hover{color:#fff}.navbar-dark .navbar-divider{background-color:rgba(255,255,255,.075)}.card{position:relative;border:.0625rem solid #e5e5e5;border-radius:.25rem}.card-block{padding:1.25rem}.card-footer,.card-header{padding:.75rem 1.25rem;background-color:#f5f5f5}.card-title{margin-top:0}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-radius:.25rem .25rem 0 0}.card>.list-group:last-child .list-group-item:last-child{border-radius:0 0 .25rem .25rem}.card-header{border-bottom:.0625rem solid #e5e5e5}.card-header:first-child{border-radius:.1875rem .1875rem 0 0}.card-footer{border-top:.0625rem solid #e5e5e5}.card-footer:last-child{border-radius:0 0 .1875rem .1875rem}.card-primary{background-color:#0275d8;border-color:#0275d8}.card-success{background-color:#5cb85c;border-color:#5cb85c}.card-info{background-color:#5bc0de;border-color:#5bc0de}.card-warning{background-color:#f0ad4e;border-color:#f0ad4e}.card-danger{background-color:#d9534f;border-color:#d9534f}.card-inverse .card-footer,.card-inverse .card-header{border-bottom:.075rem solid rgba(255,255,255,.2)}.card-inverse .card-blockquote>footer,.card-inverse .card-link,.card-inverse .card-text{color:rgba(255,255,255,.65)}.card-inverse .card-link:focus,.card-inverse .card-link:hover{color:#fff}.card-blockquote{padding:0;margin-bottom:0;border-left:0}.card-img{border-radius:.25rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img-top{border-radius:.25rem .25rem 0 0}.card-img-bottom{border-radius:0 0 .25rem .25rem}.card-deck{display:table;table-layout:fixed;border-spacing:1.25rem 0}.card-deck .card{display:table-cell;width:1%;vertical-align:top}.card-columns .card,.progress{width:100%}.card-deck-wrapper{margin-right:-1.25rem;margin-left:-1.25rem}.card-group{display:table;width:100%;table-layout:fixed}.card-group .card{display:table-cell;vertical-align:top}.breadcrumb>li,.card-columns .card,.pagination{display:inline-block}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child){border-radius:0}.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}.breadcrumb,.pagination{border-radius:.25rem;margin-bottom:1rem}.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.breadcrumb{padding:.75rem 1rem;list-style:none;background-color:#eceeef}.breadcrumb>li+li:before{padding-right:.5rem;padding-left:.5rem;color:#818a91;content:"/ "}.breadcrumb>.active{color:#818a91}.pagination{padding-left:0;margin-top:1rem}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:.5rem .75rem;margin-left:-1px;line-height:1.5;color:#0275d8;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{color:#014c8c;background-color:#eceeef;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:2;color:#fff;cursor:default;background-color:#0275d8;border-color:#0275d8}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#818a91;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm>li>a,.pagination-sm>li>span{padding:.275rem .75rem;font-size:.85rem;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.pager{padding-left:0;margin-top:1rem;margin-bottom:1rem;list-style:none}.pager:after,.pager:before{display:table;content:" "}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eceeef}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#818a91;cursor:not-allowed;background-color:#fff}.pager-next>a,.pager-next>span{float:right}.pager-prev>a,.pager-prev>span{float:left}.label{display:inline-block;padding:.25em .4em;font-size:75%;line-height:1;color:#fff;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.label:empty{display:none}.btn .label{position:relative;top:-1px}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label-pill{padding-right:.6em;padding-left:.6em;border-radius:1rem}.label-default{background-color:#818a91}.label-default[href]:focus,.label-default[href]:hover{background-color:#687077}.label-primary{background-color:#0275d8}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#025aa5}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}.jumbotron-hr{border-top-color:#d0d5d8}@media (min-width:34em){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:15px;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-heading{margin-top:0;color:inherit}.alert-dismissible{padding-right:35px}.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d0e9c6}.alert-success hr{border-top-color:#c1e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bcdff1}.alert-info hr{border-top-color:#a6d5ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faf2cc}.alert-warning hr{border-top-color:#f7ecb5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebcccc}.alert-danger hr{border-top-color:#e4b9b9}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:block;height:1rem;margin-bottom:1rem}.progress[value]{-webkit-appearance:none;color:#0074d9;border:0;-moz-appearance:none;appearance:none}.progress[value]::-webkit-progress-bar{background-color:#eee;border-radius:.25rem}.progress[value]::-webkit-progress-value::before{content:attr(value)}.progress[value]::-webkit-progress-value{background-color:#0074d9;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.progress[value="100"]::-webkit-progress-value{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}@media screen and (min-width:0 \0){.progress{background-color:#eee;border-radius:.25rem}.progress-bar{display:inline-block;height:1rem;text-indent:-999rem;background-color:#0074d9;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.progress[width^="0"]{min-width:2rem;color:#818a91;background-color:transparent;background-image:none}.progress[width="100%"]{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}}.progress-striped[value]::-webkit-progress-value{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-striped[value]::-moz-progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-animated[value]::-webkit-progress-value{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-animated[value]::-moz-progress-bar{animation:progress-bar-stripes 2s linear infinite}.progress-success[value]::-webkit-progress-value{background-color:#5cb85c}.progress-success[value]::-moz-progress-bar{background-color:#5cb85c}@media screen and (min-width:0 \0){.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-animated .progress-bar-striped{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-success .progress-bar{background-color:#5cb85c}}.progress-info[value]::-webkit-progress-value{background-color:#5bc0de}.progress-info[value]::-moz-progress-bar{background-color:#5bc0de}@media screen and (min-width:0 \0){.progress-info .progress-bar{background-color:#5bc0de}}.progress-warning[value]::-webkit-progress-value{background-color:#f0ad4e}.progress-warning[value]::-moz-progress-bar{background-color:#f0ad4e}@media screen and (min-width:0 \0){.progress-warning .progress-bar{background-color:#f0ad4e}}.progress-danger[value]::-webkit-progress-value{background-color:#d9534f}.progress-danger[value]::-moz-progress-bar{background-color:#d9534f}@media screen and (min-width:0 \0){.progress-danger .progress-bar{background-color:#d9534f}}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right{padding-left:10px}.media-left{padding-right:10px}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:0}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-.0625rem;background-color:#fff;border:.0625rem solid #ddd}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-flush .list-group-item{border-width:.0625rem 0;border-radius:0}a.list-group-item,button.list-group-item{width:100%;color:#555;text-align:inherit}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#818a91;cursor:not-allowed;background-color:#eceeef}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#818a91}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#a8d6fe}.list-group-item-state{color:#a94442;background-color:#f2dede}a.list-group-item-state,button.list-group-item-state{color:#a94442}a.list-group-item-state .list-group-item-heading,button.list-group-item-state .list-group-item-heading{color:inherit}a.list-group-item-state:focus,a.list-group-item-state:hover,button.list-group-item-state:focus,button.list-group-item-state:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-state.active,a.list-group-item-state.active:focus,a.list-group-item-state.active:hover,button.list-group-item-state.active,button.list-group-item-state.active:focus,button.list-group-item-state.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9{padding-bottom:42.857143%}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.close{float:right;font-size:1.5rem;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-content,.popover{-webkit-background-clip:padding-box}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;-webkit-overflow-scrolling:touch;outline:0}.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before{display:table;content:" "}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.in{opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.5}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.popover,.tooltip{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:.85rem;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;line-break:auto;text-decoration:none}.popover,.tooltip{position:absolute;display:block}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:34em){.modal-dialog{width:600px;margin:30px auto}.modal-sm{width:300px}}@media (min-width:48em){.modal-lg{width:900px}}.tooltip{z-index:1070;text-align:start;opacity:0}.tooltip.in{opacity:.9}.tooltip.bs-tether-element-attached-bottom,.tooltip.tooltip-top{padding:5px 0;margin-top:-3px}.tooltip.bs-tether-element-attached-bottom .tooltip-arrow,.tooltip.tooltip-top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tether-element-attached-left,.tooltip.tooltip-right{padding:0 5px;margin-left:3px}.tooltip.bs-tether-element-attached-left .tooltip-arrow,.tooltip.tooltip-right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tether-element-attached-top,.tooltip.tooltip-bottom{padding:5px 0;margin-top:3px}.tooltip.bs-tether-element-attached-top .tooltip-arrow,.tooltip.tooltip-bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tether-element-attached-right,.tooltip.tooltip-left{padding:0 5px;margin-left:-3px}.tooltip.bs-tether-element-attached-right .tooltip-arrow,.tooltip.tooltip-left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{top:0;left:0;z-index:1060;max-width:276px;padding:1px;text-align:start;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.carousel-caption,.carousel-control{color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.text-nowrap,.text-truncate{white-space:nowrap}.popover.bs-tether-element-attached-bottom,.popover.popover-top{margin-top:-10px}.popover.bs-tether-element-attached-bottom .popover-arrow,.popover.popover-top .popover-arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.bs-tether-element-attached-bottom .popover-arrow:after,.popover.popover-top .popover-arrow:after{bottom:1px;margin-left:-10px;content:"";border-top-color:#fff;border-bottom-width:0}.popover.bs-tether-element-attached-left,.popover.popover-right{margin-left:10px}.popover.bs-tether-element-attached-left .popover-arrow,.popover.popover-right .popover-arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.bs-tether-element-attached-left .popover-arrow:after,.popover.popover-right .popover-arrow:after{bottom:-10px;left:1px;content:"";border-right-color:#fff;border-left-width:0}.popover.bs-tether-element-attached-top,.popover.popover-bottom{margin-top:10px}.popover.bs-tether-element-attached-top .popover-arrow,.popover.popover-bottom .popover-arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-top .popover-arrow:after,.popover.popover-bottom .popover-arrow:after{top:1px;margin-left:-10px;content:"";border-top-width:0;border-bottom-color:#fff}.popover.bs-tether-element-attached-right,.popover.popover-left{margin-left:-10px}.popover.bs-tether-element-attached-right .popover-arrow,.popover.popover-left .popover-arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-right .popover-arrow:after,.popover.popover-left .popover-arrow:after{right:1px;bottom:-10px;content:"";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:-.7rem -.7rem 0 0}.popover-content{padding:9px 14px}.popover-arrow,.popover-arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.carousel,.carousel-inner{position:relative}.popover-arrow{border-width:11px}.popover-arrow:after{content:"";border-width:10px}.carousel-inner{width:100%;overflow:hidden}.carousel-inner>.carousel-item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.carousel-item>a>img,.carousel-inner>.carousel-item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.carousel-item.active.right,.carousel-inner>.carousel-item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.carousel-item.active.left,.carousel-inner>.carousel-item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.carousel-item.active,.carousel-inner>.carousel-item.next.left,.carousel-inner>.carousel-item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;opacity:.5}.carousel-control.left{background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;width:20px;height:20px;margin-top:-10px;font-family:serif;line-height:1}.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:transparent;border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px}.carousel-caption .btn,.text-hide{text-shadow:none}@media (min-width:34em){.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .icon-prev{margin-left:-15px}.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:after,.clearfix:before{display:table;content:" "}.center-block{display:block;margin-right:auto;margin-left:auto}.hidden-xl-down,.hidden-xs-up,.visible-print-block,[hidden]{display:none!important}.pull-right{float:right!important}.pull-left{float:left!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.p-r-0,.p-x-0{padding-right:0!important}.p-l-0,.p-x-0{padding-left:0!important}.p-t-0,.p-y-0{padding-top:0!important}.p-b-0,.p-y-0{padding-bottom:0!important}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.m-r-0,.m-x-0{margin-right:0!important}.m-l-0,.m-x-0{margin-left:0!important}.m-t-0,.m-y-0{margin-top:0!important}.m-b-0,.m-y-0{margin-bottom:0!important}.invisible{visibility:hidden}.text-hide{font:"0/0" a;color:transparent;background-color:transparent;border:0}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-truncate{overflow:hidden;text-overflow:ellipsis}.text-xs-left{text-align:left}.text-xs-right{text-align:right}.text-xs-center{text-align:center}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#818a91}.text-primary{color:#0275d8}a.text-primary:focus,a.text-primary:hover{color:#025aa5}.text-success{color:#5cb85c}a.text-success:focus,a.text-success:hover{color:#449d44}.text-info{color:#5bc0de}a.text-info:focus,a.text-info:hover{color:#31b0d5}.text-warning{color:#f0ad4e}a.text-warning:focus,a.text-warning:hover{color:#ec971f}.text-danger{color:#d9534f}a.text-danger:focus,a.text-danger:hover{color:#c9302c}.bg-inverse{color:#eceeef;background-color:#373a3c}.bg-faded{background-color:#f7f7f9}.bg-primary{color:#fff;background-color:#0275d8}a.bg-primary:focus,a.bg-primary:hover{background-color:#025aa5}.bg-success{color:#fff;background-color:#5cb85c}a.bg-success:focus,a.bg-success:hover{background-color:#449d44}.bg-info{color:#fff;background-color:#5bc0de}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5}.bg-warning{color:#fff;background-color:#f0ad4e}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f}.bg-danger{color:#fff;background-color:#d9534f}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c}.m-a-0{margin:0!important}.m-r,.m-x{margin-right:1rem!important}.m-l,.m-x{margin-left:1rem!important}.m-t,.m-y{margin-top:1rem!important}.m-b,.m-y{margin-bottom:1rem!important}.m-a{margin:1rem!important}.m-t-md,.m-y-md{margin-top:1.5rem!important}.m-b-md,.m-y-md{margin-bottom:1.5rem!important}.m-x-auto{margin-right:auto!important;margin-left:auto!important}.m-r-md,.m-x-md{margin-right:1.5rem!important}.m-l-md,.m-x-md{margin-left:1.5rem!important}.m-a-md{margin:1.5rem!important}.m-r-lg,.m-x-lg{margin-right:3rem!important}.m-l-lg,.m-x-lg{margin-left:3rem!important}.m-t-lg,.m-y-lg{margin-top:3rem!important}.m-b-lg,.m-y-lg{margin-bottom:3rem!important}.m-a-lg{margin:3rem!important}.p-a-0{padding:0!important}.p-r,.p-x{padding-right:1rem!important}.p-l,.p-x{padding-left:1rem!important}.p-t,.p-y{padding-top:1rem!important}.p-b,.p-y{padding-bottom:1rem!important}.p-a{padding:1rem!important}.p-r-md,.p-x-md{padding-right:1.5rem!important}.p-l-md,.p-x-md{padding-left:1.5rem!important}.p-t-md,.p-y-md{padding-top:1.5rem!important}.p-b-md,.p-y-md{padding-bottom:1.5rem!important}.p-a-md{padding:1.5rem!important}.p-r-lg,.p-x-lg{padding-right:3rem!important}.p-l-lg,.p-x-lg{padding-left:3rem!important}.p-t-lg,.p-y-lg{padding-top:3rem!important}.p-b-lg,.p-y-lg{padding-bottom:3rem!important}.p-a-lg{padding:3rem!important}.pos-f-t{position:fixed;top:0;right:0;left:0;z-index:1030}@media (max-width:33.9em){.hidden-xs-down{display:none!important}}@media (min-width:34em){.text-sm-left{text-align:left}.text-sm-right{text-align:right}.text-sm-center{text-align:center}.hidden-sm-up{display:none!important}}@media (max-width:47.9em){.hidden-sm-down{display:none!important}}@media (min-width:48em){.text-md-left{text-align:left}.text-md-right{text-align:right}.text-md-center{text-align:center}.hidden-md-up{display:none!important}}@media (max-width:61.9em){.hidden-md-down{display:none!important}}@media (min-width:62em){.text-lg-left{text-align:left}.text-lg-right{text-align:right}.text-lg-center{text-align:center}.hidden-lg-up{display:none!important}}@media (max-width:74.9em){.hidden-lg-down{display:none!important}}@media (min-width:75em){.text-xl-left{text-align:left}.text-xl-right{text-align:right}.text-xl-center{text-align:center}.hidden-xl-up{display:none!important}}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print .hidden-print{display:none!important}} \ No newline at end of file diff --git a/lib/public/stylesheets/style.css b/lib/public/stylesheets/style.css index 7d4de14..5dbd90f 100644 --- a/lib/public/stylesheets/style.css +++ b/lib/public/stylesheets/style.css @@ -13,11 +13,6 @@ a { color: #00B7FF; } -a.anchor { - position: relative; - top: -50px; -} - a.forkme { top: 0; right: 0; @@ -25,14 +20,14 @@ a.forkme { position: fixed; display: inline-block; background: #008000; - box-shadow: 0 0 5px #000; color: #fff; font-weight: bold; - padding: 3px 40px; + padding: 3px 100px; border: 2px solid #006400; - -webkit-transform: rotate(45deg) translate(65px); - transform: rotate(45deg) translate(65px); + -webkit-transform: rotate(45deg) translate(108px, -46px); + transform: rotate(45deg) translate(108px, -46px); } + a.forkme:hover { color: #ddd; text-decoration: none; @@ -40,60 +35,89 @@ a.forkme:hover { a.sponsor { position: fixed; + z-index: 1041; + width: 48px; + height: 48px; right: 0px; top: 0px; - height: 40px; - width: 40px; - z-index: 1041; - margin: 5px 10px; + margin: 5px; } -.container > .navbar-header { - display: inline-block; - margin: inherit; +.alert { + font-size: 1rem; } -a.navbar-brand.twitter { - color: #55acee; - font-size: 16px; +#documentation .row { + background: #eee; + border-radius: 0.25rem; } -a.navbar-brand.twitter:before { - content: ""; - background: url("/images/twitter.png"); - display: inline-block; - height: 16px; - width: 16px; - vertical-align: middle; +#documentation .row .col-md-2 { + text-align: center; } -mark.green { +#documentation .row > div { + padding: 15px; +} + +#try input { + width: 100%; + background: #fff; + border: 1px solid #ddd; + padding: 0.3em; + line-height: 1.5em; + margin: 0px; +} + +img.tryit { + -webkit-filter: drop-shadow(0px 0px 6px); + filter: drop-shadow(0px 0px 6px); +} + +mark { background: inherit; - color: #008000; font-weight: bold; padding: 0; } -thead { - font-weight: bold; +mark.green { + color: #080; } +mark.blue { + color: #08f; +} + +span[title] { + cursor: help; + text-decoration: underline dotted; +} + + .row { margin-right: auto; margin-left: auto; } -h1, h2, h3, h4, h5, h6 { - color: #333; - font-weight: normal; +h1, h2, h3, h4, h6 { + font-weight: 200; } -h3 { +h1 { + font-size: 4rem; +} + +h2 { margin-top: 2em; } -h4 { - margin-top: 1em; +h3 { + font-size: 1.3rem; + margin-top: 2em; +} + +code { + word-wrap: break-word; } .code { @@ -110,186 +134,88 @@ h4 { position: relative; } -.code .example { - cursor: text; -} - -.code .example:hover { - color: #000; - text-decoration: underline; -} - -.preview-background { - background: #eee; - height: 220px; -} - -.code .example-wrapper .preview, .code .preview-placeholder { - display: none; - left: 0; - right: 0; - position: absolute; - bottom: -260px; - padding-left: 10px; - height: 220px; - background-position: 10px center; - background-repeat: no-repeat; - font-size: 14px; - font-family: "Helvetica Neue", Arial, sans-serif; - font-weight: 300; - color: #666; -} - -.code .preview-placeholder { - display: block; - font-weight: bold; - line-height: 200px; -} - -.code .preview-placeholder:hover { - /* fixes glitchy blinking */ - display: block !important; -} - -.code:hover .preview-placeholder { - display: none; -} - -.code .example-wrapper .preview i { - color: #aaa; -} - -.code .example-wrapper:hover .preview { - display: block; -} - -#avatar-example-1:hover .preview { - background-image: url("/avatars/jeb_"); -} -#avatar-example-2:hover .preview { - background-image: url("/avatars/jeb_?helm"); -} -#avatar-example-3:hover .preview { - background-image: url("/avatars/jeb_?size=128"); -} -#avatar-example-4:hover .preview { - background-image: url("/avatars/853c80ef3c3749fdaa49938b674adae6"); -} -#avatar-example-5:hover .preview { - background-image: url("/avatars/0?default=alex"); -} -#avatar-example-6:hover .preview { - background-image: url("/avatars/0?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png"); -} - -#render-example-1:hover .preview { - background-image: url("/renders/body/jeb_?helm&scale=4"); -} -#render-example-2:hover .preview { - background-image: url("/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=8"); -} - -#skin-example-1:hover .preview { - background-image: url("/skins/jeb_"); -} -#skin-example-2:hover .preview { - background-image: url("/skins/0?default=alex"); -} - -#cape-example-1:hover .preview { - background-image: url("/capes/Dinnerbone"); -} -#cape-example-2:hover .preview { - background-image: url("/capes/md_5"); -} - -img.preload { - /* - preload hover images - browsers don't load 0x0 images - */ - position: fixed; - top: -9999px; - left: -9999px; +.jumbotron { + padding: 1em 0 3em; } .jumbotron img { margin: 5px; } -.avatar-wrapper { +#avatar-wrapper { height: 64px; overflow: hidden; + font-size: 0; } .avatar { width: 64px; height: 64px; display: inline-block; - margin-right: 0.5em; + margin-right: 6px; } .avatar.jomo {background-image: url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64")} -.avatar.jomo:hover {background-image: url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64&helm")} +.avatar.jomo:hover {background-image: url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64&overlay")} .avatar.jake_0 {background-image: url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64")} -.avatar.jake_0:hover {background-image: url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64&helm")} +.avatar.jake_0:hover {background-image: url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64&overlay")} .avatar.sk89q {background-image: url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64")} -.avatar.sk89q:hover {background-image: url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64&helm")} +.avatar.sk89q:hover {background-image: url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64&overlay")} .avatar.md_5 {background-image: url("/avatars/af74a02d19cb445bb07f6866a861f783?size=64")} -.avatar.md_5:hover {background-image: url("/avatars/af74a02d19cb445bb07f6866a861f783?size=64&helm")} +.avatar.md_5:hover {background-image: url("/avatars/af74a02d19cb445bb07f6866a861f783?size=64&overlay")} .avatar.jeb {background-image: url("/avatars/853c80ef3c3749fdaa49938b674adae6?size=64")} -.avatar.jeb:hover {background-image: url("/avatars/853c80ef3c3749fdaa49938b674adae6?size=64&helm")} +.avatar.jeb:hover {background-image: url("/avatars/853c80ef3c3749fdaa49938b674adae6?size=64&overlay")} .avatar.notch {background-image: url("/avatars/069a79f444e94726a5befca90e38aaf5?size=64")} -.avatar.notch:hover {background-image: url("/avatars/069a79f444e94726a5befca90e38aaf5?size=64&helm")} +.avatar.notch:hover {background-image: url("/avatars/069a79f444e94726a5befca90e38aaf5?size=64&overlay")} .avatar.dinnerbone {background-image: url("/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64")} -.avatar.dinnerbone:hover {background-image: url("/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64&helm")} +.avatar.dinnerbone:hover {background-image: url("/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64&overlay")} .avatar.ez {background-image: url("/avatars/7d043c7389524696bfba571c05b6aec0?size=64")} -.avatar.ez:hover {background-image: url("/avatars/7d043c7389524696bfba571c05b6aec0?size=64&helm")} +.avatar.ez:hover {background-image: url("/avatars/7d043c7389524696bfba571c05b6aec0?size=64&overlay")} .avatar.grumm {background-image: url("/avatars/e6b5c088068044df9e1b9bf11792291b?size=64")} -.avatar.grumm:hover {background-image: url("/avatars/e6b5c088068044df9e1b9bf11792291b?size=64&helm")} +.avatar.grumm:hover {background-image: url("/avatars/e6b5c088068044df9e1b9bf11792291b?size=64&overlay")} .avatar.themogmimer {background-image: url("/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64")} -.avatar.themogmimer:hover {background-image: url("/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64&helm")} +.avatar.themogmimer:hover {background-image: url("/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64&overlay")} .avatar.marc {background-image: url("/avatars/b05881186e75410db2db4d3066b223f7?size=64")} -.avatar.marc:hover {background-image: url("/avatars/b05881186e75410db2db4d3066b223f7?size=64&helm")} +.avatar.marc:hover {background-image: url("/avatars/b05881186e75410db2db4d3066b223f7?size=64&overlay")} .avatar.searge {background-image: url("/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64")} -.avatar.searge:hover {background-image: url("/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64&helm")} +.avatar.searge:hover {background-image: url("/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64&overlay")} .avatar.xlson {background-image: url("/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64")} -.avatar.xlson:hover {background-image: url("/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64&helm")} +.avatar.xlson:hover {background-image: url("/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64&overlay")} .avatar.minecraftchick {background-image: url("/avatars/c9b54008fd8047428b238787b5f2401c?size=64")} -.avatar.minecraftchick:hover {background-image: url("/avatars/c9b54008fd8047428b238787b5f2401c?size=64&helm")} +.avatar.minecraftchick:hover {background-image: url("/avatars/c9b54008fd8047428b238787b5f2401c?size=64&overlay")} .avatar.kappe {background-image: url("/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64")} -.avatar.kappe:hover {background-image: url("/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64&helm")} +.avatar.kappe:hover {background-image: url("/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64&overlay")} .avatar.krisjelbring {background-image: url("/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64")} -.avatar.krisjelbring:hover {background-image: url("/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64&helm")} +.avatar.krisjelbring:hover {background-image: url("/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64&overlay")} .avatar.thinkofdeath {background-image: url("/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64")} -.avatar.thinkofdeath:hover {background-image: url("/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64&helm")} +.avatar.thinkofdeath:hover {background-image: url("/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64&overlay")} .avatar.evilseph {background-image: url("/avatars/020242a17b9441799eff511eea1221da?size=64")} -.avatar.evilseph:hover {background-image: url("/avatars/020242a17b9441799eff511eea1221da?size=64&helm")} +.avatar.evilseph:hover {background-image: url("/avatars/020242a17b9441799eff511eea1221da?size=64&overlay")} .avatar.mollstam {background-image: url("/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64")} -.avatar.mollstam:hover {background-image: url("/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64&helm")} +.avatar.mollstam:hover {background-image: url("/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64&overlay")} .avatar.mollstam {background-image: url("/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64")} -.avatar.mollstam:hover {background-image: url("/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64&helm")} +.avatar.mollstam:hover {background-image: url("/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64&overlay")} .avatar.flipped { -webkit-transform: rotate(180deg); transform: rotate(180deg); -} +} \ No newline at end of file diff --git a/lib/renders.js b/lib/renders.js index f30f321..9fe1b9c 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -247,12 +247,7 @@ exp.draw_model = function(rid, img, scale, overlay, is_body, slim, callback) { // helper method to open a render from +renderpath+ // callback: error, image buffer exp.open_render = function(rid, renderpath, callback) { - fs.readFile(renderpath, function(err, buf) { - if (err) { - logging.error(rid, "error while opening skin file:", err); - } - callback(err, buf); - }); + fs.readFile(renderpath, callback); }; module.exports = exp; \ No newline at end of file diff --git a/lib/response.js b/lib/response.js index 09a703a..561d58f 100644 --- a/lib/response.js +++ b/lib/response.js @@ -12,6 +12,9 @@ var human_status = { }; +// print these, but without stacktrace +var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED", "HTTPERROR"]; + // handles HTTP responses // +request+ a http.IncomingMessage // +response+ a http.ServerResponse @@ -23,31 +26,36 @@ var human_status = { // * hash: image hash, required when body is an image // * err: a possible Error module.exports = function(request, response, result) { - - response.on("close", function() { - logging.warn(request.id, "Connection closed"); - }); - - response.on("finish", function() { - logging.log(request.method, request.url.href, request.id, response.statusCode, headers["Response-Time"] + "ms", "(" + (human_status[result.status] || "-") + ")"); - }); - - response.on("error", function(err) { - logging.error(request.id, err); - }); - // These headers are the same for every response var headers = { - "Content-Type": (result.body && result.type) || "text/plain", + "Content-Type": result.body && result.type || "text/plain", "Cache-Control": "max-age=" + config.caching.browser + ", public", "Response-Time": Date.now() - request.start, "X-Request-ID": request.id, "Access-Control-Allow-Origin": "*" }; + response.on("close", function() { + logging.warn(request.id, "Connection closed"); + }); + + response.on("finish", function() { + logging.log(request.id, request.method, request.url.href, response.statusCode, headers["Response-Time"] + "ms", "(" + (human_status[result.status] || "-") + ")"); + }); + + response.on("error", function(err) { + logging.error(request.id, err); + }); + if (result.err) { - logging.error(request.id, result.err); - logging.error(request.id, result.err.stack); + var silent = silent_errors.indexOf(result.err.code) !== -1; + if (result.err.stack && !silent) { + logging.error(request.id, result.err.stack); + } else if (silent) { + logging.warn(request.id, result.err); + } else { + logging.error(request.id, result.err); + } result.status = -1; } diff --git a/lib/routes/avatars.js b/lib/routes/avatars.js index 0d03a22..5146638 100644 --- a/lib/routes/avatars.js +++ b/lib/routes/avatars.js @@ -1,4 +1,3 @@ -var logging = require("../logging"); var helpers = require("../helpers"); var config = require("../../config"); var skins = require("../skins"); @@ -8,7 +7,7 @@ var url = require("url"); function handle_default(img_status, userId, size, def, req, err, callback) { def = def || skins.default_skin(userId); - if (def !== "steve" && def !== "alex") { + if (def !== "steve" && def !== "mhf_steve" && def !== "alex" && def !== "mhf_alex") { if (helpers.id_valid(def)) { // clean up the old URL to match new image var parsed = req.url; @@ -30,6 +29,10 @@ function handle_default(img_status, userId, size, def, req, err, callback) { } } else { // handle steve and alex + def = def.toLowerCase(); + if (def.substr(0, 4) !== "mhf_") { + def = "mhf_" + def; + } skins.resize_img(path.join(__dirname, "..", "public", "images", def + ".png"), size, function(resize_err, image) { callback({ status: img_status, @@ -47,7 +50,7 @@ module.exports = function(req, callback) { var userId = (req.url.path_list[1] || "").split(".")[0]; var size = parseInt(req.url.query.size) || config.avatars.default_size; var def = req.url.query.default; - var helm = req.url.query.hasOwnProperty("helm"); + var overlay = req.url.query.hasOwnProperty("overlay") || req.url.query.hasOwnProperty("helm"); // check for extra paths if (req.url.path_list.length > 2) { @@ -80,9 +83,8 @@ module.exports = function(req, callback) { userId = userId.replace(/-/g, ""); try { - helpers.get_avatar(req.id, userId, helm, size, function(err, status, image, hash) { + helpers.get_avatar(req.id, userId, overlay, size, function(err, status, image, hash) { if (err) { - logging.error(req.id, err); if (err.code === "ENOENT") { // no such file cache.remove_hash(req.id, userId); @@ -101,7 +103,6 @@ module.exports = function(req, callback) { } }); } catch (e) { - logging.error(req.id, "error:", e.stack); handle_default(-1, userId, size, def, req, e, callback); } }; \ No newline at end of file diff --git a/lib/routes/capes.js b/lib/routes/capes.js index c3f303d..77f1953 100644 --- a/lib/routes/capes.js +++ b/lib/routes/capes.js @@ -1,4 +1,3 @@ -var logging = require("../logging"); var helpers = require("../helpers"); var cache = require("../cache"); @@ -32,7 +31,6 @@ module.exports = function(req, callback) { try { helpers.get_cape(rid, userId, function(err, hash, status, image) { if (err) { - logging.error(rid, err); if (err.code === "ENOENT") { // no such file cache.remove_hash(rid, userId); diff --git a/lib/routes/index.js b/lib/routes/index.js index c3d76cf..fc13275 100644 --- a/lib/routes/index.js +++ b/lib/routes/index.js @@ -1,11 +1,25 @@ +var logging = require("../logging"); var config = require("../../config"); var path = require("path"); -var jade = require("jade"); +var read = require("fs").readFileSync; +var ejs = require("ejs"); -// compile jade -var index = jade.compileFile(path.join(__dirname, "..", "views", "index.jade")); +var str; +var index; + +function compile() { + logging.log("Compiling index page"); + str = read(path.join(__dirname, "..", "views", "index.html.ejs"), "utf-8"); + index = ejs.compile(str); +} + +compile(); module.exports = function(req, callback) { + if (config.server.debug_enabled) { + // allow changes without reloading + compile(); + } var html = index({ title: "Crafatar", domain: "https://" + req.headers.host, diff --git a/lib/routes/renders.js b/lib/routes/renders.js index 0df771f..6950a11 100644 --- a/lib/routes/renders.js +++ b/lib/routes/renders.js @@ -9,10 +9,10 @@ var url = require("url"); var fs = require("fs"); // valid types: head, body -// helmet is query param -function handle_default(rid, scale, helm, body, img_status, userId, size, def, req, err, callback) { +// overlay is query param +function handle_default(rid, scale, overlay, body, img_status, userId, size, def, req, err, callback) { def = def || skins.default_skin(userId); - if (def !== "steve" && def !== "alex") { + if (def !== "steve" && def !== "mhf_steve" && def !== "alex" && def !== "mhf_alex") { if (helpers.id_valid(def)) { // clean up the old URL to match new image var parsed = req.url; @@ -34,9 +34,13 @@ function handle_default(rid, scale, helm, body, img_status, userId, size, def, r } } else { // handle steve and alex + def = def.toLowerCase(); + if (def.substr(0, 4) !== "mhf_") { + def = "mhf_" + def; + } fs.readFile(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function(fs_err, buf) { // we render the default skins, but not custom images - renders.draw_model(rid, buf, scale, helm, body, def === "alex", function(render_err, def_img) { + renders.draw_model(rid, buf, scale, overlay, body, def === "mhf_alex", function(render_err, def_img) { callback({ status: img_status, body: def_img, @@ -57,7 +61,7 @@ module.exports = function(req, callback) { var userId = (req.url.path_list[2] || "").split(".")[0]; var def = req.url.query.default; var scale = parseInt(req.url.query.scale) || config.renders.default_scale; - var helm = req.url.query.hasOwnProperty("helm"); + var overlay = req.url.query.hasOwnProperty("overlay") || req.url.query.hasOwnProperty("helm"); // check for extra paths if (req.url.path_list.length > 3) { @@ -96,9 +100,8 @@ module.exports = function(req, callback) { userId = userId.replace(/-/g, ""); try { - helpers.get_render(rid, userId, scale, helm, body, function(err, status, hash, image) { + helpers.get_render(rid, userId, scale, overlay, body, function(err, status, hash, image) { if (err) { - logging.error(rid, err); if (err.code === "ENOENT") { // no such file cache.remove_hash(rid, userId); @@ -114,11 +117,10 @@ module.exports = function(req, callback) { }); } else { logging.debug(rid, "image not found, using default."); - handle_default(rid, scale, helm, body, status, userId, scale, def, req, err, callback); + handle_default(rid, scale, overlay, body, status, userId, scale, def, req, err, callback); } }); } catch(e) { - logging.error(rid, "error:", e.stack); - handle_default(rid, scale, helm, body, -1, userId, scale, def, req, e, callback); + handle_default(rid, scale, overlay, body, -1, userId, scale, def, req, e, callback); } }; \ No newline at end of file diff --git a/lib/routes/skins.js b/lib/routes/skins.js index 3a245fa..19280e4 100644 --- a/lib/routes/skins.js +++ b/lib/routes/skins.js @@ -1,13 +1,14 @@ var logging = require("../logging"); var helpers = require("../helpers"); var skins = require("../skins"); +var cache = require("../cache"); var path = require("path"); var lwip = require("lwip"); var url = require("url"); function handle_default(img_status, userId, def, req, err, callback) { def = def || skins.default_skin(userId); - if (def !== "steve" && def !== "alex") { + if (def !== "steve" && def !== "mhf_steve" && def !== "alex" && def !== "mhf_alex") { if (helpers.id_valid(def)) { // clean up the old URL to match new image var parsed = req.url; @@ -29,6 +30,10 @@ function handle_default(img_status, userId, def, req, err, callback) { } } else { // handle steve and alex + def = def.toLowerCase(); + if (def.substr(0, 4) !== "mhf_") { + def = "mhf_" + def; + } lwip.open(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function(lwip_err, image) { if (image) { image.toBuffer("png", function(buf_err, buffer) { @@ -78,9 +83,8 @@ module.exports = function(req, callback) { userId = userId.replace(/-/g, ""); try { - helpers.get_skin(rid, userId, function(err, hash, status, image) { + helpers.get_skin(rid, userId, function(err, hash, status, image, slim) { if (err) { - logging.error(req.id, err); if (err.code === "ENOENT") { // no such file cache.remove_hash(req.id, userId); @@ -99,7 +103,6 @@ module.exports = function(req, callback) { } }); } catch(e) { - logging.error(rid, "error:", e.stack); handle_default(-1, userId, def, req, e, callback); } }; \ No newline at end of file diff --git a/lib/server.js b/lib/server.js index b480a3e..3d2d6be 100644 --- a/lib/server.js +++ b/lib/server.js @@ -131,18 +131,33 @@ var exp = {}; exp.boot = function(callback) { var port = process.env.PORT || 3000; var bind_ip = process.env.BIND || "0.0.0.0"; - logging.log("Server running on http://" + bind_ip + ":" + port + "/"); server = http.createServer(requestHandler).listen(port, bind_ip, function() { + logging.log("Server running on http://" + bind_ip + ":" + port + "/"); if (callback) { callback(); } }); + + // stop accepting new connections, + // wait for established connections to finish (30s max), + // then exit + process.on("SIGTERM", function() { + logging.warn("Got SIGTERM, no longer accepting connections!"); + + setTimeout(function() { + logging.error("Dropping connections after 30s. Force quit."); + process.exit(1); + }, 30000); + + server.close(function() { + logging.log("All connections closed, shutting down."); + process.exit(); + }); + }); }; exp.close = function(callback) { - server.close(function() { - callback(); - }); + server.close(callback); }; module.exports = exp; diff --git a/lib/skins.js b/lib/skins.js index a302443..a6e1637 100644 --- a/lib/skins.js +++ b/lib/skins.js @@ -56,7 +56,7 @@ exp.extract_helm = function(rid, facefile, buffer, outname, callback) { } else { face_helm_img.toBuffer("png", {compression: "none"}, function(buf_err2, face_helm_buffer) { if (buf_err2) { - callback(buf_err2) + callback(buf_err2); } else { if (face_helm_buffer.toString() !== face_buffer.toString()) { face_helm_img.writeFile(outname, function(write_err) { @@ -101,11 +101,11 @@ exp.resize_img = function(inname, size, callback) { }); }; -// returns "alex" or "steve" calculated by the +uuid+ +// returns "mhf_alex" or "mhf_steve" calculated by the +uuid+ exp.default_skin = function(uuid) { if (uuid.length <= 16) { // we can't get the skin type by username - return "steve"; + return "mhf_steve"; } else { // great thanks to Minecrell for research into Minecraft and Java's UUID hashing! // https://git.io/xJpV @@ -117,7 +117,7 @@ exp.default_skin = function(uuid) { parseInt(uuid[15], 16) ^ parseInt(uuid[23], 16) ^ parseInt(uuid[31], 16); - return lsbs_even ? "alex" : "steve"; + return lsbs_even ? "mhf_alex" : "mhf_steve"; } }; @@ -126,7 +126,6 @@ exp.default_skin = function(uuid) { exp.open_skin = function(rid, skinpath, callback) { fs.readFile(skinpath, function(err, buf) { if (err) { - logging.error(rid, "error while opening skin file:", err); callback(err, null); } else { callback(null, buf); @@ -135,18 +134,18 @@ exp.open_skin = function(rid, skinpath, callback) { }; // write the image +buffer+ to the +outpath+ file -// callback: error +// the image is stripped down by lwip. +// callback: error, image exp.save_image = function(buffer, outpath, callback) { lwip.open(buffer, "png", function(err, image) { if (err) { - callback(err); + callback(err, image); } else { - image.batch() - .writeFile(outpath, function(write_err) { + image.writeFile(outpath, function(write_err) { if (write_err) { - callback(write_err); + callback(write_err, image); } else { - callback(null); + callback(null, image); } }); } diff --git a/lib/views/index.html.ejs b/lib/views/index.html.ejs new file mode 100644 index 0000000..bd4630f --- /dev/null +++ b/lib/views/index.html.ejs @@ -0,0 +1,306 @@ + + + + Crafatar – A blazing fast API for Minecraft faces! + + + <%# FIXME: Use CDN %> + + + + + + + + + + + + + + + + + + + + Fork me on GitHub + +
+
+

Crafatar

+

A blazing fast API for Minecraft faces!

+ +
+ <%# These are shuffled by JS %> +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+

Try it

+
+
+
+ +
+
+ +
+
+
+
+ +
+

Avatars

+
+
+ avatar +
+
+
+ <%= domain %>/avatars/uuid +
+

Accepted modifiers: size, overlay, default.

+
+
+
+ +
+

Head Renders

+
+
+ head +
+
+
+ <%= domain %>/renders/head/uuid +
+

+ Accepted modifiers: scale, overlay, default.
+ Please note that renders are still beta and have some issues. New renders are in progress! +

+
+
+
+ +
+

Body Renders

+
+
+ body +
+
+
+ <%= domain %>/renders/body/uuid +
+

+ Accepted modifiers: scale, overlay, default.
+ Please note that renders are still beta and have some issues. New renders are in progress! +

+
+
+
+ +
+

Skins

+
+
+ skin +
+
+
+ <%= domain %>/skins/uuid +
+

Accepted modifiers: default.

+
+
+
+ +
+

Capes

+
+
+ cape +
+
+
+ <%= domain %>/capes/uuid +
+

Accepted modifiers: default.

+
+
+
+ +
+ +
+

Meta

+

+ In the examples above, you can generally use usernames instead of uuid. However, apart from the special cases MHF_Steve and MHF_Alex this is discouraged as explained below.
+ You can append .png or any other file extension to the URL path if you like to, but all images are PNG. +

+ +
+

Attribution

+

+ Attribution is not required, but it is encouraged.
+ If you want to show some support for this (free!) service, place a notice like this somewhere: + + Thank you to <a href="https://crafatar.com">Crafatar</a> for providing avatars. + +

+
+ +
+

URL Parameters

+

+ You can tweak images using query string parameters.
+ Example: <%= domain %>/avatars/853c80ef3c3749fdaa49938b674adae6?size=4&default=MHF_Steve&overlay +

+
    +
  • size: The size for avatars in pixels. <%= config.avatars.min_size %> - <%= config.avatars.max_size %> +
  • scale: The scale factor for renders. <%= config.renders.min_scale %> - <%= config.renders.max_scale %> +
  • overlay: Apply the overlay to the avatar. Presence of this parameter implies true. This option was previously known as helm. +
  • + default: The fallback to be used when the requested image cannot be served. You can use a custom URL or any uuid.
    + The option defaults to either MHF_Steve or MHF_Alex, depending on the requested UUID. All usernames default to MHF_Steve. +
+
+ +
+

About UUIDs

+

UUIDs may be any valid Mojang UUID in the blank or dashed format.

+

Malformed UUIDs are rejected.

+
+ +
+

About Usernames

+

+ We strongly advise you to use UUIDs instead of usernames! UUIDs never change while usernames do.
+ Looking up players by username has officially been deprecated by Mojang ever since UUIDs were introduced.
+ Crafatar uses a legacy API which updates very slowly to retrieve skins for usernames.
+ Skins come without any details, including whether a player uses the Alex or Steve skin model.
+ Additionally, Mojang has stated that this legacy interface may be disabled anytime, causing all requests to fail. +

+

Malformed usernames are rejected.

+
+ +
+

About Caching

+

+ Crafatar checks for skin updates every <%= config.caching.local / 60 %> minutes.
+ Images are cached in your browser for <%= config.caching.browser / 60 %> minutes until a new request to Crafatar is made.
+ In addition, CloudFlare caches up to 2 hours on a per-url basis. +

+

When you changed your skin you can try clearing your browser cache to see the change faster.

+
+ +
+

CORS

+

Crafatar supports Cross-Origin Resource Sharing, so you can make AJAX request from other sites!

+
+ +
+

HTTP Headers

+

+ Responses come with some custom HTTP headers, useful for debugging.
+ Please note that these headers may be cached by CloudFlare. +

+ +
    +
  • + X-Storage-Type: Details about how the requested image was stored on the server +
      +
    • none: No external requests. Player has no skin (cached)
    • +
    • cached: No external requests. (skin cached)
    • +
    • checked: Requested skin details, skin cached. (1 external request)
      + This happens either when the user removed their skin or when it didn't change.
    • +
    • downloaded: Requested skin details, skin downloaded. (2 external requests)
    • +
    • server error: This can happen, for example, when Mojang's servers are down.
      + If possible, a cached image is served instead.
    • +
    • user error: You have done something wrong, such as requesting a malformed uuid.
      + Check the response body for details.
    • +
    +
  • + X-Request-ID: The internal ID assigned to this request.
    + If you think something is wrong with your request, please contact us and provide this ID. +
+
+
+ +
+

Contact

+ +
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/lib/views/index.jade b/lib/views/index.jade deleted file mode 100644 index 251a5c0..0000000 --- a/lib/views/index.jade +++ /dev/null @@ -1,397 +0,0 @@ -extends layout - -block content - .jumbotron - .container - h1 Crafatar - p A blazing fast API for Minecraft faces! - .avatar-wrapper - .avatar.jomo(title="jomo's avatar") - .avatar.jake_0(title="jake_0's avatar") - .avatar.sk89q(title="sk89q's avatar") - .avatar.md_5(title="md_5's avatar") - .avatar.notch(title="notch's avatar") - .avatar.jeb(title="jeb's avatar") - .avatar.dinnerbone.flipped(title="dinnerbone's avatar") - .avatar.ez(title="ez' avatar") - .avatar.grumm.flipped(title="grumm's avatar") - .avatar.themogmimer(title="themogmimer's avatar") - .avatar.searge(title="searge's avatar") - .avatar.xlson(title="xlson's avatar") - .avatar.krisjelbring(title="krisjelbring's avatar") - .avatar.minecraftchick(title="minecraftchick's avatar") - .avatar.kappe(title="kappe's avatar") - .avatar.marc(title="marc's avatar") - .avatar.mollstam(title="mollstam's avatar") - .avatar.evilseph(title="evilseph's avatar") - .avatar.thinkofdeath(title="thinkofdeath's avatar") - - .container - section(id="documentation") - h2 Documentation - .row - section - a(id="avatars", class="anchor") - a(href="#avatars") - h3 Avatars - | Replace - mark.green userid - | with a Mojang UUID or username to get the related head. All images are PNGs. - .code - | #{domain}/avatars/ - mark.green userid - - section - a(id="avatar-parameters" class="anchor") - a(href="#avatar-parameters") - h4 Avatar Parameters - table(class="table table-striped") - thead - tr - td parameter - td type - td default - td description - tbody - tr - td size - td integer - td #{config.avatars.default_size} - td The size of the image in pixels, #{config.avatars.min_size} - #{config.avatars.max_size}. - tr - td default - td string - td - | The standard value is calculated based on the UUID (even = alex, odd = steve).
- | Usernames always default to steve. - td - | The image to be served when the userid has no skin.
- | Valid options are - a(href="/avatars/0?default=steve") steve - | , - a(href="/avatars/0?default=alex") alex - | , or a custom URL. - tr - td helm - td null - td - td Apply the "second" layer (hat) to the avatar. - - section - a(id="avatar-examples", class="anchor") - a(href="#avatar-examples") - h4 Avatar Examples - .code - #avatar-example-1.example-wrapper - .example #{domain}/avatars/jeb_ - p.preview Jeb's avatar - #avatar-example-2.example-wrapper - .example #{domain}/avatars/jeb_?helm - p.preview Jeb's avatar with helm - #avatar-example-3.example-wrapper - .example #{domain}/avatars/jeb_?size=128 - p.preview Jeb's avatar, 128 × 128 - #avatar-example-4.example-wrapper - .example #{domain}/avatars/853c80ef3c3749fdaa49938b674adae6 - p.preview Jeb's avatar by UUID - #avatar-example-5.example-wrapper - .example #{domain}/avatars/jeb_?default=alex - p.preview Jeb's avatar, or fall back to alex (this example assumes jeb_ does not exist) - #avatar-example-6.example-wrapper - .example #{domain}/avatars/jeb_?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png - p.preview - | Jeb's avatar, or fall back to a custom image (this example assumes jeb_ does not exist) - p.preview-placeholder - | Hover over the example URLs above for a preview! - .preview-background - - - section - a(id="renders" class="anchor") - a(href="#renders") - h3 3D Renders - p - | Crafatar also provides support for 3D renders of Minecraft skins.
- | Please note that this feature is currently beta!
- | Replace - mark.green userid - | with a Mojang UUID or username to get a render of the skin. - | The head render type returns a render of the skin's head. - span.code - | #{domain}/renders/head/ - mark.green userid - | The body render returns a render of the entire skin. - span.code - | #{domain}/renders/body/ - mark.green userid - - section - a(id="render-parameters" class="anchor") - a(href="#render-parameters") - h4 Render Parameters - table(class="table table-striped") - thead - tr - td parameter - td type - td default - td description - tbody - tr - td scale - td integer - td #{config.renders.default_scale}. The actual size differs between the type of render. - td The scale factor of the image #{config.renders.min_scale} - #{config.renders.max_scale}. - tr - td helm - td null - td - td Apply the "second" layer (hat) to the avatar. - - section - a(id="render-examples", class="anchor") - a(href="#render-examples") - h4 Render Examples - .code - #render-example-1.example-wrapper - .example #{domain}/renders/body/jeb_?helm&scale=4 - p.preview Jeb's body, with helmet, scale 4 - #render-example-2.example-wrapper - .example #{domain}/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=8 - p.preview Jeb's head, by UUID, scale 8 - p.preview-placeholder - | Hover over the example URLs above for a preview! - .preview-background - - - section - a(id="skins" class="anchor") - a(href="#skins") - h3 Skins - p - | You can also get the full skin file of a player.
- | Replace - mark.green userid - | with a Mojang UUID or username to get the related skin.
- | The user's skin is returned, or the default image is served.
- | You can use the default parameter here as well. - span.code - | #{domain}/skins/ - mark.green userid - - section - a(id="skin-parameters" class="anchor") - a(href="#skin-parameters") - h4 Skin Parameters - table(class="table table-striped") - thead - tr - td parameter - td type - td default - td description - tbody - tr - td default - td string - td - | The standard value is calculated based on the UUID (even = alex, odd = steve).
- | Usernames always default to steve. - td - | The image to be served when the userid has no skin.
- | Valid options are - a(href="/skins/0?default=steve") steve - | , - a(href="/skins/0?default=alex") alex - | , or a custom URL. - - section - a(id="skin-examples", class="anchor") - a(href="#skin-examples") - h4 Skin Examples - .code - #skin-example-1.example-wrapper - .example #{domain}/skins/jeb_ - p.preview Jeb's skin - #skin-example-2.example-wrapper - .example #{domain}/skins/jeb_?default=alex - p.preview Jeb's skin, or fall back to alex (this example assumes jeb_ does not exist) - p.preview-placeholder - | Hover over the example URLs above for a preview! - .preview-background - - section - a(id="capes" class="anchor") - a(href="#capes") - h3 Capes - p - | A cape endpoint is also available to get the active cape of a user.
- | Replace - mark.green userid - | with a Mojang UUID or username to get the related cape.
- | The user's cape is returned, otherwise a 404 is returned.
- .code - | #{domain}/capes/ - mark.green userid - - section - a(id="cape-examples", class="anchor") - a(href="#cape-examples") - h4 Cape Examples - .code - #cape-example-1.example-wrapper - .example #{domain}/capes/Dinnerbone - p.preview Dinnerbone's Cape Mojang capes are not transparent... - #cape-example-2.example-wrapper - .example #{domain}/capes/md_5 - p.preview md_5's Cape - p.preview-placeholder - | Hover over the example URLs above for a preview! - .preview-background - - section - a(id="meta" class="anchor") - a(href="#meta") - h2 Meta - - section - a(id="meta-cors" class="anchor") - a(href="#meta-cors") - h3 CORS - p - | Crafatar supports CORS so you can make AJAX request from within the browser! - - section - a(id="meta-http-headers" class="anchor") - a(href="#meta-http-headers") - h3 HTTP Headers - p - | Responses come with these HTTP headers, useful for debugging.
- | Please note that these headers are cached by CloudFlare (CF-Cache-Status: HIT). - - section - a(id="meta-response-time" class="anchor") - a(href="#meta-response-time") - h4 Response-Time - p The time, in milliseconds, it took Crafatar to process the request. - - section - a(id="meta-x-storage-type" class="anchor") - a(href="#meta-x-storage-type") - h4 X-Storage-Type - p Details about how the requested image was stored on the server - ul - li none: No external requests. Cached: User has no skin. - li cached: No external requests. Skin cached and stored locally. - li - | checked: 1 external request. Skin cached, checked for updates, no skin downloaded.
- | This happens either when the user removed their skin or when it didn't change. - li downloaded: 2 external requests. First request or skin changed, skin downloaded. - li - | server error: This can happen, for example, when Mojang's servers are down.
- | If possible, a cached image is served instead. - li - | user error: You have done something wrong, such as requesting a malformed userid.
- | Check the response body for details. - section - a(id="meta-x-request-id" class="anchor") - a(href="#meta-x-request-id") - h4 X-Request-ID - p - | The internal ID assigned to this request.
- | If you think something is wrong with your request, please contact us and provide this ID. - - section - a(id="meta-about-usernames" class="anchor") - a(href="#meta-about-usernames") - h3 About Usernames - p - | We strongly advise you to use UUIDs instead of usernames in production.
- | Usernames are deprecated by Mojang and you should only use usernames for testing.
- | You don't have to change anything when using UUIDs and someone changes their Username.
- | Malformed usernames are rejected. - - section - a(id="meta-about-uuids" class="anchor") - a(href="#meta-about-uuids") - h3 About UUIDs - p - | UUIDs may use the blank or dashed format.
- | Malformed UUIDs are rejected. - - section - a(id="meta-about-caching" class="anchor") - a(href="#meta-about-caching") - h3 About Caching - p - | Crafatar caches skins for #{config.caching.local/60} minutes before checking for skin changes.
- | Images are cached in your browser for #{config.caching.browser/60} minutes until a new request to Crafatar is made.
- | When you changed your skin you can try clearing your browser cache to see the change faster. - - - section - a(id="contact" class="anchor") - a(href="#contact") - h2 Contact - ul - li Follow us on twitter @crafatar - li Open an issue on GitHub - li Join us in #crafatar on irc.esper.net - - footer - hr - p(class="pull-right") Copyright Crafatar #{new Date().getFullYear()} - - - // preload hover images - img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64", alt="preloaded image") - img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/069a79f444e94726a5befca90e38aaf5?size=64", alt="preloaded image") - img.preload(src="/avatars/0?default=alex", alt="preloaded image") - img.preload(src="/avatars/0?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png", alt="preloaded image") - img.preload(src="/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64", alt="preloaded image") - img.preload(src="/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64", alt="preloaded image") - img.preload(src="/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64", alt="preloaded image") - img.preload(src="/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64", alt="preloaded image") - img.preload(src="/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64", alt="preloaded image") - img.preload(src="/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64", alt="preloaded image") - img.preload(src="/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64", alt="preloaded image") - img.preload(src="/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/7d043c7389524696bfba571c05b6aec0?size=64", alt="preloaded image") - img.preload(src="/avatars/7d043c7389524696bfba571c05b6aec0?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6", alt="preloaded image") - img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6?size=64", alt="preloaded image") - img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64", alt="preloaded image") - img.preload(src="/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64", alt="preloaded image") - img.preload(src="/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/af74a02d19cb445bb07f6866a861f783?size=64", alt="preloaded image") - img.preload(src="/avatars/af74a02d19cb445bb07f6866a861f783?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/b05881186e75410db2db4d3066b223f7?size=64", alt="preloaded image") - img.preload(src="/avatars/b05881186e75410db2db4d3066b223f7?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64", alt="preloaded image") - img.preload(src="/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/c9b54008fd8047428b238787b5f2401c?size=64", alt="preloaded image") - img.preload(src="/avatars/c9b54008fd8047428b238787b5f2401c?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64", alt="preloaded image") - img.preload(src="/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/e6b5c088068044df9e1b9bf11792291b?size=64", alt="preloaded image") - img.preload(src="/avatars/e6b5c088068044df9e1b9bf11792291b?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64", alt="preloaded image") - img.preload(src="/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64&helm", alt="preloaded image") - img.preload(src="/avatars/jeb_", alt="preloaded image") - img.preload(src="/avatars/jeb_?helm", alt="preloaded image") - img.preload(src="/avatars/jeb_?size=128", alt="preloaded image") - img.preload(src="/capes/Dinnerbone", alt="preloaded image") - img.preload(src="/capes/md_5", alt="preloaded image") - img.preload(src="/renders/body/jeb_?helm&scale=4", alt="preloaded image") - img.preload(src="/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=8", alt="preloaded image") - img.preload(src="/skins/0?default=alex", alt="preloaded image") - img.preload(src="/skins/jeb_", alt="preloaded image") \ No newline at end of file diff --git a/lib/views/layout.jade b/lib/views/layout.jade deleted file mode 100644 index 0eef6f8..0000000 --- a/lib/views/layout.jade +++ /dev/null @@ -1,31 +0,0 @@ -doctype html -html(lang="en") - head - title= title - link(rel="icon", sizes="16x16", type="image/png", href="/favicon.png") - link(href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.1/css/bootstrap.min.css", rel="stylesheet") - link(rel="stylesheet", href="/stylesheets/style.css") - - meta(name="description", content="Crafatar is a blazing fast Minecraft avatar API with support for avatars, skins, and even 3D renders!") - meta(name="keywords", content="minecraft, avatar, renders, skins, uuid, username") - meta(name="viewport", content="initial-scale=1,maximum-scale=1") - - meta(charset='utf-8') - meta(property='og:title', content='Crafatar') - meta(property='og:type', content='website') - meta(property='og:url', content='https://crafatar.com') - meta(property='og:image', content='https://crafatar.com/logo.png') - meta(property='og:description', content='A blazing fast Minecraft avatar API with support for avatars, skins, and 3D renders.') - - meta(name='twitter:card', content='summary') - meta(name='twitter:creator', content='@Crafatar') - body - a.forkme(href="https://github.com/crafatar/crafatar", target="_blank") Fork me on GitHub - a.sponsor(href="https://akliz.net/crafatar", target="_blank", title="Crafatar is sponsored by Akliz") - img(src="/images/akliz.png", alt="Akliz") - .navbar.navbar-default.navbar-fixed-top - .container - .navbar-header - a.navbar-brand(href="/") Crafatar - a.navbar-brand.twitter(href="https://twitter.com/Crafatar", target="_blank") crafatar - block content \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/package.json b/package.json index 6ab201c..fad407f 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,15 @@ "name": "crafatar", "version": "1.0.0", "private": true, - "author": "Jake0oo0", - "description": "A Minecraft avatar service with support for avatars, 1.8 skins, and even 3D renders!", + "description": "A blazing fast API for Minecraft faces!", "contributors": [ { - "name": "jomo" + "name": "jomo", + "url": "https://github.com/jomo" + }, + { + "name": "Jake", + "url": "https://github.com/Jake0oo0" } ], "repository": { @@ -22,7 +26,7 @@ ], "scripts": { "postinstall": "cp 'config.example.js' 'config.js'", - "start": "forever -l logs/log.log -o logs/out.log -e logs/error.log -p ./ -a --minUptime 8000 --spinSleepTime 1500 www.js", + "start": "node www.js", "test": "mocha", "test-travis": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, @@ -32,19 +36,18 @@ "dependencies": { "canvas": "^1.3.4", "crc": "~3.3.0", - "forever": "~0.14.2", - "jade": "~1.11.0", + "ejs": "^2.3.4", "lwip": "~0.0.7", "mime": "~1.3.4", - "node-df": "~0.1.1", - "redis": "~0.12.1", - "request": "~2.58.0", + "node-df": "crafatar/node-df", + "redis": "~2.0.0", + "request": "~2.64.0", "toobusy-js": "~0.4.2" }, "devDependencies": { "coveralls": "~2.11.2", - "istanbul": "~0.3.17", - "mocha": "~2.2.5", - "mocha-lcov-reporter": "~0.0.2" + "istanbul": "~0.3.20", + "mocha": "~2.3.3", + "mocha-lcov-reporter": "~1.0.0" } } diff --git a/test/bulk.sh b/test/bulk.sh index 239b32c..a975c83 100755 --- a/test/bulk.sh +++ b/test/bulk.sh @@ -25,9 +25,9 @@ bulk() { trap return INT echo "$ids" | while read id; do if [ -z "$async" ]; then - curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?helm" + curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?overlay" else - curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?helm" & + curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?overlay" & sleep "$interval" fi done diff --git a/test/test.js b/test/test.js index 424ba46..40d0772 100644 --- a/test/test.js +++ b/test/test.js @@ -52,7 +52,10 @@ var alex_ids = [ "fffffff1" + "fffffff1" + "fffffff1" + "fffffff0", ]; -var rid = "TestReqID: "; +// generates a 12 character random string +function rid() { + return Math.random().toString(36).substring(2, 14); +} function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; @@ -120,14 +123,14 @@ describe("Crafatar", function() { }); it("should not exist (uuid)", function(done) { var number = getRandomInt(0, 9).toString(); - networking.get_profile(rid, Array(33).join(number), function(err, profile) { + networking.get_profile(rid(), Array(33).join(number), function(err, profile) { assert.ifError(err); assert.strictEqual(profile, null); done(); }); }); it("should not exist (username)", function(done) { - networking.get_username_url(rid, "Steve", 0, function(err, profile) { + networking.get_username_url(rid(), "Steve", 0, function(err, profile) { assert.ifError(err); done(); }); @@ -136,7 +139,7 @@ describe("Crafatar", function() { describe("Avatar", function() { it("uuid's account should exist, but skin should not", function(done) { // profile "Alex" - hoping it'll never have a skin - networking.get_profile(rid, "ec561538f3fd461daff5086b22154bce", function(err, profile) { + networking.get_profile(rid(), "ec561538f3fd461daff5086b22154bce", function(err, profile) { assert.ifError(err); assert.notStrictEqual(profile, null); networking.get_uuid_info(profile, "CAPE", function(url) { @@ -145,15 +148,15 @@ describe("Crafatar", function() { }); }); }); - it("Username should default to Steve", function(done) { - assert.strictEqual(skins.default_skin("TestUser"), "steve"); + it("Username should default to MHF_Steve", function(done) { + assert.strictEqual(skins.default_skin("TestUser"), "mhf_steve"); done(); }); for (var a in alex_ids) { var alexid = alex_ids[a]; (function(alex_id) { - it("UUID " + alex_id + " should default to Alex", function(done) { - assert.strictEqual(skins.default_skin(alex_id), "alex"); + it("UUID " + alex_id + " should default to MHF_Alex", function(done) { + assert.strictEqual(skins.default_skin(alex_id), "mhf_alex"); done(); }); }(alexid)); @@ -161,8 +164,8 @@ describe("Crafatar", function() { for (var s in steve_ids) { var steveid = steve_ids[s]; (function(steve_id) { - it("UUID " + steve_id + " should default to Steve", function(done) { - assert.strictEqual(skins.default_skin(steve_id), "steve"); + it("UUID " + steve_id + " should default to MHF_Steve", function(done) { + assert.strictEqual(skins.default_skin(steve_id), "mhf_steve"); done(); }); }(steveid)); @@ -172,7 +175,7 @@ describe("Crafatar", function() { it("should time out on uuid info download", function(done) { var original_timeout = config.server.http_timeout; config.server.http_timeout = 1; - networking.get_profile(rid, "069a79f444e94726a5befca90e38aaf5", function(err, profile) { + networking.get_profile(rid(), "069a79f444e94726a5befca90e38aaf5", function(err, profile) { assert.strictEqual(err.code, "ETIMEDOUT"); config.server.http_timeout = original_timeout; done(); @@ -181,7 +184,7 @@ describe("Crafatar", function() { it("should time out on username info download", function(done) { var original_timeout = config.server.http_timeout; config.server.http_timeout = 1; - networking.get_username_url(rid, "jomo", 0, function(err, url) { + networking.get_username_url(rid(), "jomo", 0, function(err, url) { assert.strictEqual(err.code, "ETIMEDOUT"); config.server.http_timeout = original_timeout; done(); @@ -190,7 +193,7 @@ describe("Crafatar", function() { it("should time out on skin download", function(done) { var original_timeout = config.http_timeout; config.server.http_timeout = 1; - networking.get_from(rid, "http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", function(body, res, error) { + networking.get_from(rid(), "http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", function(body, res, error) { assert.strictEqual(error.code, "ETIMEDOUT"); config.server.http_timeout = original_timeout; done(); @@ -198,22 +201,14 @@ describe("Crafatar", function() { }); it("should not find the skin", function(done) { assert.doesNotThrow(function() { - networking.get_from(rid, "http://textures.minecraft.net/texture/this-does-not-exist", function(img, response, err) { + networking.get_from(rid(), "http://textures.minecraft.net/texture/this-does-not-exist", function(img, response, err) { assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions done(); }); }); }); - it("should ignore file updates on invalid files", function(done) { - assert.doesNotThrow(function() { - cache.update_timestamp(rid, "0123456789abcdef0123456789abcdef", "invalid-file.png", false, function(err) { - assert.ifError(err); - done(); - }); - }); - }); it("should not find the file", function(done) { - skins.open_skin(rid, "non/existent/path", function(err, img) { + skins.open_skin(rid(), "non/existent/path", function(err, img) { assert(err); done(); }); @@ -309,30 +304,37 @@ describe("Crafatar", function() { }); it("should not fail on simultaneous requests", function(done) { - var url = "http://localhost:3000/avatars/696a82ce41f44b51aa31b8709b8686f0"; - // 10 requests at once - var requests = 10; - var finished = 0; - function partDone() { - finished++; - if (requests === finished) { - done(); + // do not change "constructor" ! + // it's a reserved property name, we're testing for that + var sids = ["696a82ce41f44b51aa31b8709b8686f0", "constructor"]; + + for (var j in sids) { + var id = sids[j]; + var url = "http://localhost:3000/avatars/" + id; + // 10 requests at once + var requests = 10; + var finished = 0; + function partDone() { + finished++; + if (requests === finished) { + done(); + } + } + function req() { + request.get(url, function(error, res, body) { + assert.ifError(error); + assert.strictEqual(res.statusCode, 200); + assert_headers(res); + assert(res.headers.etag); + assert.strictEqual(res.headers["content-type"], "image/png"); + assert(body); + partDone(); + }); + } + // make simultanous requests + for (var k = 0; k < requests; k++) { + req(k); } - } - function req() { - request.get(url, function(error, res, body) { - assert.ifError(error); - assert.strictEqual(res.statusCode, 200); - assert_headers(res); - assert(res.headers.etag); - assert.strictEqual(res.headers["content-type"], "image/png"); - assert(body); - partDone(); - }); - } - // make simultanous requests - for (var j = 0; j < requests; j++) { - req(j); } }); @@ -344,12 +346,12 @@ describe("Crafatar", function() { }, "avatar with non-existent username": { url: "http://localhost:3000/avatars/0?size=16", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [2416827277, 1243826040] }, - "avatar with non-existent username defaulting to alex": { - url: "http://localhost:3000/avatars/0?size=16&default=alex", - etag: '"alex"', + "avatar with non-existent username defaulting to mhf_alex": { + url: "http://localhost:3000/avatars/0?size=16&default=mhf_alex", + etag: '"mhf_alex"', crc32: [862751081, 809395677] }, "avatar with non-existent username defaulting to username": { @@ -363,39 +365,39 @@ describe("Crafatar", function() { redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16" }, "avatar with non-existent username defaulting to url": { - url: "http://localhost:3000/avatars/0?size=16&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/avatars/0?size=16&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, - "helm avatar with existing username": { - url: "http://localhost:3000/avatars/jeb_?size=16&helm", + "overlay avatar with existing username": { + url: "http://localhost:3000/avatars/jeb_?size=16&overlay", etag: '"a846b82963"', crc32: 646871998 }, - "helm avatar with non-existent username": { - url: "http://localhost:3000/avatars/0?size=16&helm", - etag: '"steve"', + "overlay avatar with non-existent username": { + url: "http://localhost:3000/avatars/0?size=16&overlay", + etag: '"mhf_steve"', crc32: [2416827277, 1243826040] }, - "helm avatar with non-existent username defaulting to alex": { - url: "http://localhost:3000/avatars/0?size=16&helm&default=alex", - etag: '"alex"', + "overlay avatar with non-existent username defaulting to mhf_alex": { + url: "http://localhost:3000/avatars/0?size=16&overlay&default=mhf_alex", + etag: '"mhf_alex"', crc32: [862751081, 809395677] }, - "helm avatar with non-existent username defaulting to username": { - url: "http://localhost:3000/avatars/0?size=16&helm&default=jeb_", + "overlay avatar with non-existent username defaulting to username": { + url: "http://localhost:3000/avatars/0?size=16&overlay&default=jeb_", crc32: 0, - redirect: "/avatars/jeb_?size=16&helm=" + redirect: "/avatars/jeb_?size=16&overlay=" }, - "helm avatar with non-existent username defaulting to uuid": { - url: "http://localhost:3000/avatars/0?size=16&helm&default=853c80ef3c3749fdaa49938b674adae6", + "overlay avatar with non-existent username defaulting to uuid": { + url: "http://localhost:3000/avatars/0?size=16&overlay&default=853c80ef3c3749fdaa49938b674adae6", crc32: 0, - redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&helm=" + redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&overlay=" }, - "helm avatar with non-existent username defaulting to url": { - url: "http://localhost:3000/avatars/0?size=16&helm&default=http%3A%2F%2Fexample.com", + "overlay avatar with non-existent username defaulting to url": { + url: "http://localhost:3000/avatars/0?size=16&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "avatar with existing uuid": { url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16", @@ -404,12 +406,12 @@ describe("Crafatar", function() { }, "avatar with non-existent uuid": { url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [2416827277, 1243826040] }, - "avatar with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=alex", - etag: '"alex"', + "avatar with non-existent uuid defaulting to mhf_alex": { + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=mhf_alex", + etag: '"mhf_alex"', crc32: [862751081, 809395677] }, "avatar with non-existent uuid defaulting to username": { @@ -423,39 +425,39 @@ describe("Crafatar", function() { redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16" }, "avatar with non-existent uuid defaulting to url": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, - "helm avatar with existing uuid": { - url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&helm", + "overlay avatar with existing uuid": { + url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&overlay", etag: '"a846b82963"', crc32: 646871998 }, - "helm avatar with non-existent uuid": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm", - etag: '"steve"', + "overlay avatar with non-existent uuid": { + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay", + etag: '"mhf_steve"', crc32: [2416827277, 1243826040] }, - "helm avatar with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm&default=alex", - etag: '"alex"', + "overlay avatar with non-existent uuid defaulting to mhf_alex": { + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay&default=mhf_alex", + etag: '"mhf_alex"', crc32: [862751081, 809395677] }, - "helm avatar with non-existent uuid defaulting to username": { + "overlay avatar with non-existent uuid defaulting to username": { url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=jeb_", crc32: 0, redirect: "/avatars/jeb_?size=16" }, - "helm avatar with non-existent uuid defaulting to uuid": { + "overlay avatar with non-existent uuid defaulting to uuid": { url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6", crc32: 0, redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16" }, - "helm avatar with non-existent uuid defaulting to url": { - url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm&default=http%3A%2F%2Fexample.com", + "overlay avatar with non-existent uuid defaulting to url": { + url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "cape with existing username": { url: "http://localhost:3000/capes/jeb_", @@ -467,9 +469,9 @@ describe("Crafatar", function() { crc32: 0 }, "cape with non-existent username defaulting to url": { - url: "http://localhost:3000/capes/0?default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/capes/0?default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "cape with existing uuid": { url: "http://localhost:3000/capes/853c80ef3c3749fdaa49938b674adae6", @@ -481,9 +483,9 @@ describe("Crafatar", function() { crc32: 0 }, "cape with non-existent uuid defaulting to url": { - url: "http://localhost:3000/capes/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/capes/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "skin with existing username": { url: "http://localhost:3000/skins/jeb_", @@ -492,12 +494,12 @@ describe("Crafatar", function() { }, "skin with non-existent username": { url: "http://localhost:3000/skins/0", - etag: '"steve"', + etag: '"mhf_steve"', crc32: 981937087 }, - "skin with non-existent username defaulting to alex": { - url: "http://localhost:3000/skins/0?default=alex", - etag: '"alex"', + "skin with non-existent username defaulting to mhf_alex": { + url: "http://localhost:3000/skins/0?default=mhf_alex", + etag: '"mhf_alex"', crc32: 2298915739 }, "skin with non-existent username defaulting to username": { @@ -511,9 +513,9 @@ describe("Crafatar", function() { redirect: "/skins/853c80ef3c3749fdaa49938b674adae6?size=16" }, "skin with non-existent username defaulting to url": { - url: "http://localhost:3000/skins/0?default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/skins/0?default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "skin with existing uuid": { url: "http://localhost:3000/skins/853c80ef3c3749fdaa49938b674adae6", @@ -522,12 +524,12 @@ describe("Crafatar", function() { }, "skin with non-existent uuid": { url: "http://localhost:3000/skins/00000000000000000000000000000000", - etag: '"steve"', + etag: '"mhf_steve"', crc32: 981937087 }, - "skin with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/skins/00000000000000000000000000000000?default=alex", - etag: '"alex"', + "skin with non-existent uuid defaulting to mhf_alex": { + url: "http://localhost:3000/skins/00000000000000000000000000000000?default=mhf_alex", + etag: '"mhf_alex"', crc32: 2298915739 }, "skin with non-existent uuid defaulting to username": { @@ -541,9 +543,9 @@ describe("Crafatar", function() { redirect: "/skins/853c80ef3c3749fdaa49938b674adae6?size=16" }, "skin with non-existent uuid defaulting to url": { - url: "http://localhost:3000/skins/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/skins/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "head render with existing username": { url: "http://localhost:3000/renders/head/jeb_?scale=2", @@ -552,12 +554,12 @@ describe("Crafatar", function() { }, "head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [3257141069, 214248305] }, - "head render with non-existent username defaulting to alex": { - url: "http://localhost:3000/renders/head/0?scale=2&default=alex", - etag: '"alex"', + "head render with non-existent username defaulting to mhf_alex": { + url: "http://localhost:3000/renders/head/0?scale=2&default=mhf_alex", + etag: '"mhf_alex"', crc32: [263450586, 3116770561] }, "head render with non-existent username defaulting to username": { @@ -571,39 +573,39 @@ describe("Crafatar", function() { redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?scale=2" }, "head render with non-existent username defaulting to url": { - url: "http://localhost:3000/renders/head/0?scale=2&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/head/0?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, - "helm head render with existing username": { - url: "http://localhost:3000/renders/head/jeb_?scale=2&helm", + "overlay head render with existing username": { + url: "http://localhost:3000/renders/head/jeb_?scale=2&overlay", etag: '"a846b82963"', crc32: [762377383, 1726474987] }, - "helm head render with non-existent username": { - url: "http://localhost:3000/renders/head/0?scale=2&helm", - etag: '"steve"', + "overlay head render with non-existent username": { + url: "http://localhost:3000/renders/head/0?scale=2&overlay", + etag: '"mhf_steve"', crc32: [3257141069, 214248305] }, - "helm head render with non-existent username defaulting to alex": { - url: "http://localhost:3000/renders/head/0?scale=2&helm&default=alex", - etag: '"alex"', + "overlay head render with non-existent username defaulting to mhf_alex": { + url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=mhf_alex", + etag: '"mhf_alex"', crc32: [263450586, 3116770561] }, - "helm head render with non-existent username defaulting to username": { - url: "http://localhost:3000/renders/head/0?scale=2&helm&default=jeb_", + "overlay head render with non-existent username defaulting to username": { + url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=jeb_", crc32: 0, - redirect: "/renders/head/jeb_?scale=2&helm=" + redirect: "/renders/head/jeb_?scale=2&overlay=" }, - "helm head render with non-existent username defaulting to uuid": { - url: "http://localhost:3000/renders/head/0?scale=2&helm&default=853c80ef3c3749fdaa49938b674adae6", + "overlay head render with non-existent username defaulting to uuid": { + url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6", crc32: 0, - redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm=" + redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay=" }, - "helm head render with non-existent username defaulting to url": { - url: "http://localhost:3000/renders/head/0?scale=2&helm&default=http%3A%2F%2Fexample.com", + "overlay head render with non-existent username defaulting to url": { + url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2", @@ -612,12 +614,12 @@ describe("Crafatar", function() { }, "head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [3257141069, 214248305] }, - "head render with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=alex", - etag: '"alex"', + "head render with non-existent uuid defaulting to mhf_alex": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=mhf_alex", + etag: '"mhf_alex"', crc32: [263450586, 3116770561] }, "head render with non-existent uuid defaulting to username": { @@ -631,39 +633,39 @@ describe("Crafatar", function() { redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2" }, "head render with non-existent uuid defaulting to url": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, - "helm head render with existing uuid": { - url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", + "overlay head render with existing uuid": { + url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay", etag: '"a846b82963"', crc32: [762377383] }, - "helm head render with non-existent uuid": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm", - etag: '"steve"', + "overlay head render with non-existent uuid": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay", + etag: '"mhf_steve"', crc32: [3257141069, 214248305] }, - "helm head render with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=alex", - etag: '"alex"', + "overlay head render with non-existent uuid defaulting to mhf_alex": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=mhf_alex", + etag: '"mhf_alex"', crc32: [263450586, 3116770561] }, - "helm head with non-existent uuid defaulting to username": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=jeb_", + "overlay head with non-existent uuid defaulting to username": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=jeb_", crc32: 0, - redirect: "/renders/head/jeb_?scale=2&helm=" + redirect: "/renders/head/jeb_?scale=2&overlay=" }, - "helm head with non-existent uuid defaulting to uuid": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=853c80ef3c3749fdaa49938b674adae6", + "overlay head with non-existent uuid defaulting to uuid": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6", crc32: 0, - redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm=" + redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay=" }, - "helm head render with non-existent uuid defaulting to url": { - url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com", + "overlay head render with non-existent uuid defaulting to url": { + url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "body render with existing username": { url: "http://localhost:3000/renders/body/jeb_?scale=2", @@ -672,12 +674,12 @@ describe("Crafatar", function() { }, "body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [1046655221, 1620063267] }, - "body render with non-existent username defaulting to alex": { - url: "http://localhost:3000/renders/body/0?scale=2&default=alex", - etag: '"alex"', + "body render with non-existent username defaulting to mhf_alex": { + url: "http://localhost:3000/renders/body/0?scale=2&default=mhf_alex", + etag: '"mhf_alex"', crc32: [549240598, 3952648540] }, "body render with non-existent username defaulting to username": { @@ -691,39 +693,39 @@ describe("Crafatar", function() { redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2" }, "body render with non-existent username defaulting to url": { - url: "http://localhost:3000/renders/body/0?scale=2&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/body/0?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, - "helm body render with existing username": { - url: "http://localhost:3000/renders/body/jeb_?scale=2&helm", + "overlay body render with existing username": { + url: "http://localhost:3000/renders/body/jeb_?scale=2&overlay", etag: '"a846b82963"', crc32: [699892097, 2732138694] }, - "helm body render with non-existent username": { - url: "http://localhost:3000/renders/body/0?scale=2&helm", - etag: '"steve"', + "overlay body render with non-existent username": { + url: "http://localhost:3000/renders/body/0?scale=2&overlay", + etag: '"mhf_steve"', crc32: [1046655221, 1620063267] }, - "helm body render with non-existent username defaulting to alex": { - url: "http://localhost:3000/renders/body/0?scale=2&helm&default=alex", - etag: '"alex"', + "overlay body render with non-existent username defaulting to mhf_alex": { + url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=mhf_alex", + etag: '"mhf_alex"', crc32: [549240598, 3952648540] }, - "helm body render with non-existent username defaulting to username": { - url: "http://localhost:3000/renders/body/0?scale=2&helm&default=jeb_", + "overlay body render with non-existent username defaulting to username": { + url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=jeb_", crc32: 0, - redirect: "/renders/body/jeb_?scale=2&helm=" + redirect: "/renders/body/jeb_?scale=2&overlay=" }, - "helm body render with non-existent username defaulting to uuid": { - url: "http://localhost:3000/renders/body/0?scale=2&helm&default=853c80ef3c3749fdaa49938b674adae6", + "overlay body render with non-existent username defaulting to uuid": { + url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6", crc32: 0, - redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&helm=" + redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay=" }, - "helm body render with non-existent username defaulting to url": { - url: "http://localhost:3000/renders/body/0?scale=2&helm&default=http%3A%2F%2Fexample.com", + "overlay body render with non-existent username defaulting to url": { + url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, "body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2", @@ -732,12 +734,12 @@ describe("Crafatar", function() { }, "body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2", - etag: '"steve"', + etag: '"mhf_steve"', crc32: [1046655221, 1620063267] }, - "body render with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=alex", - etag: '"alex"', + "body render with non-existent uuid defaulting to mhf_alex": { + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=mhf_alex", + etag: '"mhf_alex"', crc32: [549240598, 3952648540] }, "body render with non-existent uuid defaulting to username": { @@ -751,29 +753,29 @@ describe("Crafatar", function() { redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2" }, "body render with non-existent uuid defaulting to url": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com", + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, - "helm body render with existing uuid": { - url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", + "overlay body render with existing uuid": { + url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay", etag: '"a846b82963"', crc32: [699892097] }, - "helm body render with non-existent uuid": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm", - etag: '"steve"', + "overlay body render with non-existent uuid": { + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay", + etag: '"mhf_steve"', crc32: [1046655221, 1620063267] }, - "helm body render with non-existent uuid defaulting to alex": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=alex", - etag: '"alex"', + "overlay body render with non-existent uuid defaulting to mhf_alex": { + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay&default=mhf_alex", + etag: '"mhf_alex"', crc32: [549240598, 3952648540] }, - "helm body render with non-existent uuid defaulting to url": { - url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com", + "overlay body render with non-existent uuid defaulting to url": { + url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive", crc32: 0, - redirect: "http://example.com" + redirect: "http://example.com/CaseSensitive" }, }; @@ -800,7 +802,7 @@ describe("Crafatar", function() { try { assert.ok(matches); } catch(e) { - throw new Error(crc(body) + " != " + location.crc32); + throw new Error(crc(body) + " != " + location.crc32 + " | " + body.toString("base64")); } assert.strictEqual(res.headers.location, location.redirect); if (location.etag === undefined) { @@ -879,13 +881,13 @@ describe("Crafatar", function() { // we have to make sure that we test both a 32x64 and 64x64 skin describe("Networking: Render", function() { it("should not fail (username, 32x64 skin)", function(done) { - helpers.get_render(rid, "md_5", 6, true, true, function(err, hash, img) { + helpers.get_render(rid(), "md_5", 6, true, true, function(err, hash, img) { assert.strictEqual(err, null); done(); }); }); it("should not fail (username, 64x64 skin)", function(done) { - helpers.get_render(rid, "Jake_0", 6, true, true, function(err, hash, img) { + helpers.get_render(rid(), "Jake_0", 6, true, true, function(err, hash, img) { assert.strictEqual(err, null); done(); }); @@ -894,7 +896,7 @@ describe("Crafatar", function() { describe("Networking: Cape", function() { it("should not fail (guaranteed cape)", function(done) { - helpers.get_cape(rid, "Dinnerbone", function(err, hash, status, img) { + helpers.get_cape(rid(), "Dinnerbone", function(err, hash, status, img) { assert.strictEqual(err, null); done(); }); @@ -903,13 +905,13 @@ describe("Crafatar", function() { before(function() { cache.get_redis().flushall(); }); - helpers.get_cape(rid, "Dinnerbone", function(err, hash, status, img) { + helpers.get_cape(rid(), "Dinnerbone", function(err, hash, status, img) { assert.strictEqual(err, null); done(); }); }); it("should not be found", function(done) { - helpers.get_cape(rid, "Jake_0", function(err, hash, status, img) { + helpers.get_cape(rid(), "Jake_0", function(err, hash, status, img) { assert.ifError(err); assert.strictEqual(img, null); done(); @@ -919,7 +921,7 @@ describe("Crafatar", function() { describe("Networking: Skin", function() { it("should not fail", function(done) { - helpers.get_cape(rid, "Jake_0", function(err, hash, status, img) { + helpers.get_cape(rid(), "Jake_0", function(err, hash, status, img) { assert.strictEqual(err, null); done(); }); @@ -928,7 +930,7 @@ describe("Crafatar", function() { before(function() { cache.get_redis().flushall(); }); - helpers.get_cape(rid, "Jake_0", function(err, hash, status, img) { + helpers.get_cape(rid(), "Jake_0", function(err, hash, status, img) { assert.strictEqual(err, null); done(); }); @@ -949,14 +951,14 @@ describe("Crafatar", function() { }); it("should be downloaded", function(done) { - helpers.get_avatar(rid, id, false, 160, function(err, status, image) { + helpers.get_avatar(rid(), id, false, 160, function(err, status, image) { assert.ifError(err); assert.strictEqual(status, 2); done(); }); }); it("should be cached", function(done) { - helpers.get_avatar(rid, id, false, 160, function(err, status, image) { + helpers.get_avatar(rid(), id, false, 160, function(err, status, image) { assert.ifError(err); assert.strictEqual(status === 0 || status === 1, true); done(); @@ -968,7 +970,7 @@ describe("Crafatar", function() { it("should be checked", function(done) { var original_cache_time = config.caching.local; config.caching.local = 0; - helpers.get_avatar(rid, id, false, 160, function(err, status, image) { + helpers.get_avatar(rid(), id, false, 160, function(err, status, image) { assert.ifError(err); assert.strictEqual(status, 3); config.caching.local = original_cache_time; @@ -980,7 +982,7 @@ describe("Crafatar", function() { describe("Networking: Skin", function() { it("should not fail (uuid)", function(done) { - helpers.get_skin(rid, id, function(err, hash, status, img) { + helpers.get_skin(rid(), id, function(err, hash, status, img) { assert.strictEqual(err, null); done(); }); @@ -989,13 +991,13 @@ describe("Crafatar", function() { describe("Networking: Render", function() { it("should not fail (full body)", function(done) { - helpers.get_render(rid, id, 6, true, true, function(err, hash, img) { + helpers.get_render(rid(), id, 6, true, true, function(err, hash, img) { assert.ifError(err); done(); }); }); it("should not fail (only head)", function(done) { - helpers.get_render(rid, id, 6, true, false, function(err, hash, img) { + helpers.get_render(rid(), id, 6, true, false, function(err, hash, img) { assert.ifError(err); done(); }); @@ -1004,7 +1006,7 @@ describe("Crafatar", function() { describe("Networking: Cape", function() { it("should not fail (possible cape)", function(done) { - helpers.get_cape(rid, id, function(err, hash, status, img) { + helpers.get_cape(rid(), id, function(err, hash, status, img) { assert.ifError(err); done(); }); @@ -1019,18 +1021,18 @@ describe("Crafatar", function() { if (id_type === "uuid") { it("uuid should be rate limited", function(done) { - networking.get_profile(rid, id, function() { - networking.get_profile(rid, id, function(err, profile) { - assert.strictEqual(err, "TooManyRequests"); - assert.strictEqual(profile.error, "TooManyRequestsException"); + networking.get_profile(rid(), id, function() { + networking.get_profile(rid(), id, function(err, profile) { + assert.strictEqual(err.toString(), "HTTP: 429"); + assert.strictEqual(profile, null); done(); }); }); }); } else { it("username should NOT be rate limited (username)", function(done) { - helpers.get_avatar(rid, id, false, 160, function() { - helpers.get_avatar(rid, id, false, 160, function(err, status, image) { + helpers.get_avatar(rid(), id, false, 160, function() { + helpers.get_avatar(rid(), id, false, 160, function(err, status, image) { assert.strictEqual(err, null); done(); }); diff --git a/www.js b/www.js index 981c14b..2a52daf 100644 --- a/www.js +++ b/www.js @@ -1,25 +1,12 @@ var logging = require("./lib/logging"); var cleaner = require("./lib/cleaner"); var config = require("./config"); -var cluster = require("cluster"); -process.on("uncaughtException", function (err) { +process.on("uncaughtException", function(err) { logging.error("uncaughtException", err.stack || err.toString()); + process.exit(1); }); -if (cluster.isMaster) { - var cores = config.server.clusters || require("os").cpus().length; - logging.log("Starting", cores + " worker" + (cores > 1 ? "s" : "")); - for (var i = 0; i < cores; i++) { - cluster.fork(); - } +setInterval(cleaner.run, config.cleaner.interval * 1000); - cluster.on("exit", function (worker) { - logging.error("Worker #" + worker.id + " died. Rebooting a new one."); - setTimeout(cluster.fork, 100); - }); - - setInterval(cleaner.run, config.cleaner.interval * 1000); -} else { - require("./lib/server.js").boot(); -} \ No newline at end of file +require("./lib/server.js").boot(); \ No newline at end of file