Compare commits

...

40 Commits

Author SHA1 Message Date
Azures
0e2a23ccbb
Use environment variables for endpoint URLs 2025-12-08 21:05:09 +01:00
41690f84c7 Custom endpoints
- minor change to customise textures & session server host for custom yggdrasil server
2025-12-08 21:02:48 +01:00
jomo
d6293cc73d bump version to 2.1.5 2024-02-01 22:29:09 +01:00
jomo
c155c8d098 update dependencies 2024-02-01 22:25:43 +01:00
jomo
bba004acc7 improve URL parsing
uses `new URL()` and `decodeURI()` instead of `url.parse()`
also checks that the requested file is in a subdirectory of `public/` before serving the file

fixes path traversal vulnerability GHSA-5cxq-25mp-q5f2
2024-02-01 22:24:29 +01:00
jomo
9cb32a843f strip dashes from uuids before handling them 2024-02-01 22:19:02 +01:00
jomo
e44ebda56f periodically log number of current skin and cape requests 2024-02-01 22:00:44 +01:00
jomo
fb4d24de6b bump version to 2.1.4 2020-12-12 23:46:41 +01:00
Jonathan Madeley
59f27f0769 mcuuid.net -> minecraftuuid.com 2020-12-12 23:39:13 +01:00
jomo
019ca37037 improve Dockerfile 2020-12-12 23:38:04 +01:00
jomo
56765488e0 improve test script 2020-12-12 23:37:13 +01:00
jomo
1328f98746 change old tests from usernames to uuids 2020-12-12 22:50:29 +01:00
jomo
ef4b2f8005 fix an issue with rate limiting 2020-12-12 22:49:31 +01:00
jomo
fe5ce6b688 update dependencies, remove some devDependencies 2020-12-12 22:48:57 +01:00
jomo
a6e8e6b0f9 delete travis stuff 2020-12-12 22:45:58 +01:00
jomo
29955a1765 improve mojang status message
as Mojang has removed their status page and their status API is no longer updating,
status information is now fetched from https://mc-heads.net/json/mc_status
and the warning message links to https://mc-heads.net/mcstatus

see #271, closes #272
2020-09-10 22:32:23 +02:00
jomo
265a98d404 pass on caching status information foor 3D renders
this was falsely always set to 2, indicating the skin was downloaded, even when it was cached
2020-07-13 00:41:21 +02:00
jomo
624bf0e338 don't count session_requests when SESSIONS_RATE_LIMIT is not set 2020-07-13 00:14:27 +02:00
jomo
db565f86c8 delete unnecessary files 2020-07-13 00:01:50 +02:00
jomo
0d2fe02cbc bump version to 2.1.3 2020-04-05 05:40:25 +02:00
jomo
e69b3f38fb new logo \o/ 2020-04-05 05:15:59 +02:00
jomo
22309efba9 show quotes on frontpage 2020-04-05 05:15:27 +02:00
jomo
3bd76ad918 update popular users and tools 2020-04-05 05:14:02 +02:00
jomo
22448c098b use 500 instead of 502 when using Cloudflare
Otherwise Cloudflare will replace images served with 502
with their own error page. This can only be turned off
in paid plans of Cloudflare.
2020-04-05 02:42:14 +02:00
jomo
7ad6f85aec improve regex 2020-03-30 01:23:16 +02:00
jomo
b87be6f9f3 simplify package.json 2020-03-29 20:59:33 +02:00
jomo
e0233f2899 document undocumented functions 2020-03-29 20:13:24 +02:00
jomo
14cbcae60c bump version to 2.1.2 2020-03-29 07:44:21 +02:00
jomo
eae7745758 add Content-Length, fixes #238 2020-03-29 07:43:23 +02:00
jomo
7f95a34e29 simplify http status codes, update website info 2020-03-29 07:43:23 +02:00
jomo
15a4f17560 add rate limit option for sessionserver
any outgoing requests to the sessionserver
that would exceed the configured rate limit are skipped
to prevent being blocked by CloudFront

if a texture hash is cached but outdated, the cache ttl will be bumped
as if the request succeeded, in order to lower requests in the near future
2020-03-29 07:43:23 +02:00
jomo
d967db3ad4 use environment variables for configuration 2020-03-29 07:32:39 +02:00
jomo
d81e2777d2 delete unused function 2020-03-28 23:38:20 +01:00
jomo
ea1ae64283 add 403 to expected response codes 2020-03-28 23:37:08 +01:00
jomo
5eb5b6fa5e bump version to 2.1.1 2020-03-24 19:26:58 +01:00
jomo
e6373002a2 update readme 2020-03-24 19:26:34 +01:00
jomo
424a4ab93b remove notice from website 2020-03-24 18:51:20 +01:00
jomo
16948de18d don't warn about closed connections 2020-03-24 18:50:58 +01:00
jomo
8e08e02272 update .travis.yml, package.json and LICENSE 2020-03-24 18:50:38 +01:00
jomo
c975cc793b remove cleaner.js 2020-03-24 18:49:42 +01:00
36 changed files with 2362 additions and 2279 deletions

View File

@ -1,2 +0,0 @@
https://github.com/mojodna/heroku-buildpack-cairo.git
https://github.com/heroku/heroku-buildpack-nodejs.git

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
.*
*.md
Dockerfile
LICENSE
images/
node_modules/

View File

@ -1,21 +0,0 @@
# We use EditorConfig to standardize settings between contributors
# See http://editorconfig.org for more info and plugin downloads
root = true
[*]
end_of_line = lf
insert_final_newline = false
trim_trailing_whitespace = true
[*.{js, json, yml}]
indent_style = space
indent_size = 2
charset = utf-8
[*.md]
trim_trailing_whitespace = false
[.gitignore]
# echo "filename" >> .gitignorre
insert_final_newline = true

6
.gitignore vendored
View File

@ -1,9 +1,3 @@
images/*/*.png
node_modules/
coverage/
.DS_Store
*.log
*.rdb
*.sublime-*
config.js
lib/public/images/sponsor.png

View File

@ -1,29 +0,0 @@
language: node_js
node_js:
- 8.9.4
sudo: false
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- libcairo2-dev
- libjpeg8-dev
- libpango1.0-dev
- libgif-dev
- build-essential
- g++-4.8
script:
- npm run-script test-travis
notifications:
irc:
channels:
- irc.esper.net#crafatar
skip_join: true
env:
- TRAVIS=true CXX=g++-4.8
services:
- redis-server
cache:
directories:
- node_modules

View File

@ -1,27 +1,35 @@
FROM node:12-alpine
FROM node:12-alpine AS builder
ARG REDIS_URL
ARG DEBUG
ARG EPHEMERAL_STORAGE
RUN apk --no-cache add git python3 build-base redis cairo-dev pango-dev jpeg-dev giflib-dev
RUN apk --no-cache --virtual .build-deps add git python build-base
RUN apk --no-cache --virtual .canvas-deps add cairo-dev pango-dev jpeg-dev giflib-dev
RUN mkdir -p /crafatar/images/faces
RUN mkdir -p /crafatar/images/helms
RUN mkdir -p /crafatar/images/skins
RUN mkdir -p /crafatar/images/renders
RUN mkdir -p /crafatar/images/capes
VOLUME /crafatar/images
COPY package.json www.js crafatar/
COPY config.example.js crafatar/config.js
COPY lib/ crafatar/lib/
WORKDIR /crafatar
RUN adduser -D app
USER app
COPY --chown=app package.json package-lock.json /home/app/crafatar/
WORKDIR /home/app/crafatar
RUN npm install
EXPOSE 3000
ENTRYPOINT npm start
COPY --chown=app . .
RUN mkdir -p images/faces images/helms images/skins images/renders images/capes
ARG VERBOSE_TEST
ARG DEBUG
RUN nohup redis-server & npm test
FROM node:12-alpine
RUN apk --no-cache add cairo pango jpeg giflib
RUN adduser -D app
USER app
RUN mkdir /home/app/crafatar
WORKDIR /home/app/crafatar
RUN mkdir -p images/faces images/helms images/skins images/renders images/capes
COPY --chown=app --from=builder /home/app/crafatar/node_modules/ node_modules/
COPY --chown=app package.json www.js config.js ./
COPY --chown=app lib/ lib/
VOLUME /home/app/crafatar/images
ENV NODE_ENV production
ENTRYPOINT ["npm", "start"]
EXPOSE 3000

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 Crafatar Team
Copyright (c) 2020 Crafatar Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View File

@ -1 +0,0 @@
web: npm start

View File

@ -1,8 +1,8 @@
# 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)
[![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)
# Crafatar
<img alt="logo" src="lib/public/logo.png" align="right" width="128px" height="128px">
[![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) [![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)
<img alt="logo" src="lib/public/logo.png" align="right">
<a href="https://crafatar.com">Crafatar</a> serves Minecraft avatars based on the skin for use in external applications.
Inspired by <a href="https://gravatar.com">Gravatar</a> (hence the name) and <a href="https://minotar.net">Minotar</a>.
@ -34,6 +34,14 @@ Please [visit the website](https://crafatar.com) for details.
# Installation
## Docker
```sh
docker network create crafatar
docker run --net crafatar -d --name redis redis
docker run --net crafatar -v crafatar-images:/home/app/crafatar/images -e REDIS_URL=redis://redis -p 3000:3000 crafatar/crafatar
```
## Manual
- Install [nodejs](https://nodejs.org/) 12 (LTS)
@ -44,32 +52,21 @@ Please [visit the website](https://crafatar.com) for details.
Crafatar is now available at http://0.0.0.0:3000.
## Docker
## Configration / Environment variables
Download the docker image from [releases](https://github.com/crafatar/crafatar/releases) (docker hub coming soon™).
See the `config.js` file.
```sh
docker load -i crafatar-docker.tar
mkdir /path/to/crafatar-images
```
# Operational notes
```sh
docker network create crafatar
docker run --net crafatar -d --name redis redis
docker run --net crafatar -v /path/to/crafatar-images:/crafatar/images -e REDIS_URL=redis://redis -p 3000:3000 crafatar:2.1.0
```
## inodes
## Environment variables
Crafatar stores a lot of images on disk. For avatars, these are 8×8 px PNG images with an average file size of \~90 bytes. This can lead to issues on file systems such as ext4, which (by default) has a bytes-per-inode ratio of 16Kb. With thousands of files with an average file size below this ratio, you will run out of available inodes before running out of disk space. (Note that this will still be reported as `ENOSPC: no space left on device`).
| Variable | Default | Description |
| :- | :- | :- |
| `BIND` | `0.0.0.0` | Hostname to listen on |
| `PORT` | `3000` | Port to listen on |
| `DEBUG` | `false` | Enable verbose debug logging |
| `REDIS_URL` | `redis://127.0.0.1:6379` | URI of the redis server |
| `EPHEMERAL_STORAGE` | | If set, redis is flushed on start* |
Consider using a different file system, changing the inode ratio, or deleting files before the inode limit is reached.
\* Use this to avoid issues when you have a persistent redis database but an ephemeral storage
## disk space and memory usage
Eventually you will run out of disk space and/or redis will be out of memory. Make sure to delete image files and/or flush redis before this happens.
# Tests
```sh

View File

@ -1,30 +0,0 @@
{
"name": "Crafatar",
"description": "A blazing fast API for Minecraft faces!",
"repository": "https://github.com/crafatar/crafatar",
"keywords": [
"node",
"minecraft",
"avatar",
"redis"
],
"website": "https://crafatar.com/",
"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"
}
]
}

View File

@ -1,40 +0,0 @@
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
},
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
},
cleaner: {
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. 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
},
server: {
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
},
sponsor: {
sidebar: '<hr><div class="list-group"><a class="list-group-item sponsor-item" href="https://akliz.net/crafatar" target="_blank" title="Applies to all modpacks and plans for the first billing cycle only.">Save 20% on a Minecraft server with Akliz.</a></div>',
top_right: '<a href="https://akliz.net/crafatar" target="_blank" title="Crafatar is sponsored by Akliz" class="sponsor"><img src="/images/sponsor.png" alt="Akliz"></a>'
},
};
module.exports = config;

69
config.js Normal file
View File

@ -0,0 +1,69 @@
var config = {
avatars: {
// for avatars
min_size: parseInt(process.env.AVATAR_MIN) || 1,
// for avatars; large values might lead to slow response time or DoS
max_size: parseInt(process.env.AVATAR_MAX) || 512,
// for avatars; size to be used when no size given
default_size: parseInt(process.env.AVATAR_DEFAULT) || 160
},
renders: {
// for 3D rendered skins
min_scale: parseInt(process.env.RENDER_MIN) || 1,
// for 3D rendered skins; large values might lead to slow response time or DoS
max_scale: parseInt(process.env.RENDER_MAX) || 10,
// for 3D rendered skins; scale to be used when no scale given
default_scale: parseInt(process.env.RENDER_DEFAULT) || 6
},
directories: {
// directory where faces are kept. must have trailing "/"
faces: process.env.FACE_DIR || "./images/faces/",
// directory where helms are kept. must have trailing "/"
helms: process.env.HELM_DIR || "./images/helms/",
// directory where skins are kept. must have trailing "/"
skins: process.env.SKIN_DIR || "./images/skins/",
// directory where rendered skins are kept. must have trailing "/"
renders: process.env.RENDER_DIR || "./images/renders/",
// directory where capes are kept. must have trailing "/"
capes: process.env.CAPE_DIR || "./images/capes/"
},
caching: {
// seconds until we will check if user's skin changed.
// Should be > 60 to comply with Mojang's rate limit
local: parseInt(process.env.CACHE_LOCAL) || 1200,
// seconds until browser will request image again
browser: parseInt(process.env.CACHE_BROWSER) || 3600,
// If true, redis is flushed on start.
// Use this to avoid issues when you have a persistent redis database but an ephemeral storage
ephemeral: process.env.EPHEMERAL_STORAGE === "true",
// Used for information on the front page
cloudflare: process.env.CLOUDFLARE === "true"
},
// URL of your redis server
redis: process.env.REDIS_URL || 'redis://localhost:6379',
server: {
// port to listen on
port: parseInt(process.env.PORT) || 3000,
// IP address to listen on
bind: process.env.BIND || "0.0.0.0",
// ms until connection to Mojang is dropped
http_timeout: parseInt(process.env.EXTERNAL_HTTP_TIMEOUT) || 2000,
// enables logging.debug & editing index page
debug_enabled: process.env.DEBUG === "true",
// set to false if you use an external logger that provides timestamps,
log_time: process.env.LOG_TIME === "true",
// rate limit per second for outgoing requests to the Mojang session server
// requests exceeding this limit are skipped and considered failed
sessions_rate_limit: parseInt(process.env.SESSIONS_RATE_LIMIT)
},
sponsor: {
sidebar: process.env.SPONSOR_SIDE,
top_right: process.env.SPONSOR_TOP_RIGHT
},
endpoints: {
textures_url: process.env.TEXTURES_ENDPOINT || "https://textures.minecraft.net/texture/",
session_url: process.env.SESSION_ENDPOINT || "https://sessionserver.mojang.com/session/minecraft/profile/"
}
};
module.exports = config;

View File

@ -1,7 +1,6 @@
var logging = require("./logging");
var node_redis = require("redis");
var config = require("../config");
var url = require("url");
var redis = null;
@ -9,19 +8,10 @@ var redis = null;
// 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_url = redis_env ? url.parse(redis_env) : {};
redis_url.port = redis_url.port || 6379;
redis_url.hostname = redis_url.hostname || "localhost";
// connect to redis
redis = node_redis.createClient(redis_url.port, redis_url.hostname);
if (redis_url.auth) {
redis.auth(redis_url.auth.split(":")[1]);
}
redis = node_redis.createClient(config.redis);
redis.on("ready", function() {
logging.log("Redis connection established.");
if (process.env.EPHEMERAL_STORAGE) {
if (config.caching.ephemeral) {
logging.log("Storage is ephemeral, flushing redis");
redis.flushall();
}
@ -41,35 +31,6 @@ exp.get_redis = function() {
return redis;
};
// updates the redis instance's server_info object
// callback: error, info object
exp.info = function(callback) {
redis.info(function(err, res) {
// parse the info command and store it in redis.server_info
// this code block was taken from mranney/node_redis#on_info_cmd
// http://git.io/LBUNbg
var lines = res.toString().split("\r\n");
var obj = {};
lines.forEach(function(line) {
var parts = line.split(":");
if (parts[1]) {
obj[parts[0]] = parts[1];
}
});
obj.versions = [];
if (obj.redis_version) {
obj.redis_version.split(".").forEach(function(num) {
obj.versions.push(+num);
});
}
redis.server_info = obj;
callback(err, redis.server_info);
});
};
// set model type to value of *slim*
exp.set_slim = function(rid, userId, slim, callback) {
logging.debug(rid, "setting slim for", userId, "to " + slim);

View File

@ -1,126 +0,0 @@
var logging = require("./logging");
var config = require("../config");
var cache = require("./cache");
var path = require("path");
var df = require("node-df");
var fs = require("fs");
var redis = cache.get_redis();
var exp = {};
// does nothing
function nil() {}
// compares redis' used_memory with cleaning_redis_limit
// callback: error, true|false
function should_clean_redis(callback) {
cache.info(function(err, info) {
if (err) {
callback(err, false);
} else {
try {
// logging.debug(info.toString());
var used = parseInt(info.used_memory) / 1024;
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);
}
}
});
}
// uses `df` to get the available fisk space
// callback: error, true|false
function should_clean_disk(callback) {
df({
file: config.directories.faces,
prefixMultiplier: "KiB",
isDisplayPrefixMultiplier: false,
precision: 2
}, function(err, response) {
if (err) {
callback(err, false);
} else {
var available = response[0].available;
var result = available < config.cleaner.disk_limit;
var msg = "DiskCleaner: " + available + "KB available";
(result ? logging.log : logging.debug)(msg);
callback(err, result);
}
});
}
// check if redis limit reached, then flush redis
// check if disk limit reached, then delete images
exp.run = function() {
should_clean_redis(function(err, clean) {
if (err) {
logging.error("Failed to run RedisCleaner");
logging.error(err);
} else if (clean) {
logging.warn("RedisCleaner: Redis limit reached! flushing now");
redis.flushall();
} else {
logging.log("RedisCleaner: Nothing to clean");
}
});
should_clean_disk(function(err, clean) {
if (err) {
logging.error("Failed to run DiskCleaner");
logging.error(err);
} else if (clean) {
logging.warn("DiskCleaner: Disk limit reached! Cleaning images now");
// 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(helmsdir, filename), nil);
fs.unlink(path.join(skinsdir, filename), nil);
}
}
}
});
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(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);
}
}
}
});
} else {
logging.log("DiskCleaner: Nothing to clean");
}
});
};
module.exports = exp;

View File

@ -7,8 +7,8 @@ var skins = require("./skins");
var path = require("path");
var fs = require("fs");
// 0098cb60-fa8e-427c-b299-793cbd302c9a
var valid_user_id = /^[0-9a-f-A-F-]{32,36}$/; // uuid
// 0098cb60fa8e427cb299793cbd302c9a
var valid_user_id = /^[0-9a-fA-F]{32}$/; // uuid
var hash_pattern = /[0-9a-f]+$/;
// gets the hash from the textures.minecraft.net +url+
@ -122,13 +122,22 @@ var requests = {
cape: {}
};
function push_request(userId, type, fun) {
var loginterval = setInterval(function(){
var skinreqs = Object.keys(requests.skin).length;
var capereqs = Object.keys(requests.cape).length;
if (skinreqs || capereqs) {
logging.log("Currently waiting for " + skinreqs + " skin requests and " + capereqs + " cape requests.");
}
}, 1000);
// add a request for +userId+ and +type+ to the queue
function push_request(userId, type, callback) {
// avoid special properties (e.g. 'constructor')
var userId_safe = "!" + userId;
if (!requests[type][userId_safe]) {
requests[type][userId_safe] = [];
}
requests[type][userId_safe].push(fun);
requests[type][userId_safe].push(callback);
}
// calls back all queued requests that match userId and type
@ -162,7 +171,6 @@ function store_images(rid, userId, cache_details, type, callback) {
logging.debug(rid, "adding to request queue");
push_request(userId, type, callback);
} else {
// add request to the queue
push_request(userId, type, callback);
networking.get_profile(rid, userId, function(err, profile) {
@ -176,7 +184,7 @@ function store_images(rid, userId, cache_details, type, callback) {
resume(userId, "cape", cache_err, null, false);
});
} else {
// an error occured, not caching. we can try in 60 seconds
// an error occured, not caching. we can try again in 60 seconds
resume(userId, type, err, null, false);
}
} else {
@ -240,10 +248,13 @@ exp.get_image_hash = function(rid, userId, type, callback) {
}
store_images(rid, userId, cache_details, type, function(store_err, new_hash, slim) {
if (store_err) {
// we might have a cached hash although an error occured
// an error occured, but we have a cached hash
// (e.g. Mojang servers not reachable, using outdated hash)
cache.update_timestamp(rid, userId, true, function(err2) {
callback(err2 || store_err, -1, cache_details && cached_hash, slim);
// bump the TTL after hitting the rate limit
var ratelimited = store_err.code === "RATELIMIT";
cache.update_timestamp(rid, userId, !ratelimited, function(err2) {
callback(err2 || store_err, 4, cache_details && cached_hash, slim);
});
} else {
var status = cache_details && (cached_hash === new_hash) ? 3 : 2;
@ -321,7 +332,7 @@ function get_type(overlay, body) {
}
// handles creations of 3D renders
// callback: error, skin hash, image buffer
// callback: error, status, skin hash, image buffer
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) {
@ -347,7 +358,7 @@ exp.get_render = function(rid, userId, scale, overlay, body, callback) {
callback(null, 0, skin_hash, null);
} else {
fs.writeFile(renderpath, drawn_img, "binary", function(write_err) {
callback(write_err, 2, skin_hash, drawn_img);
callback(write_err, status, skin_hash, drawn_img);
});
}
});
@ -384,4 +395,8 @@ exp.get_cape = function(rid, userId, callback) {
});
};
exp.stoplog = function() {
clearInterval(loginterval);
}
module.exports = exp;

View File

@ -23,16 +23,20 @@ function log(level, args, logger) {
}
}
// log with INFO level
exp.log = function() {
log(" INFO", arguments);
};
// log with WARN level
exp.warn = function() {
log(" WARN", arguments, console.warn);
};
// log with ERROR level
exp.error = function() {
log("ERROR", arguments, console.error);
};
if (config.server.debug_enabled || process.env.DEBUG === "true") {
// log with DEBUG level if debug logging is enabled
if (config.server.debug_enabled) {
exp.debug = function() {
log("DEBUG", arguments);
};

View File

@ -1,80 +1,121 @@
var http_code = require("http").STATUS_CODES;
var logging = require("./logging");
var request = require("request");
var config = require("../config");
var skins = require("./skins");
var http = require("http");
require("./object-patch");
var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/";
var textures_url = "http://textures.minecraft.net/texture/";
var session_url = config.endpoints.session_url;
var textures_url = config.endpoints.textures_url;
// count requests made to session_url in the last 1000ms
var session_requests = [];
var exp = {};
// returns the amount of outgoing session requests made in the last 1000ms
function req_count() {
var index = session_requests.findIndex((i) => i >= Date.now() - 1000);
if (index >= 0) {
return session_requests.length - index;
} else {
return 0;
}
}
// deletes all entries in session_requests, should be called every 1000ms
exp.resetCounter = function() {
var count = req_count();
if (count) {
var logfunc = count >= config.server.sessions_rate_limit ? logging.warn : logging.debug;
logfunc('Clearing old session requests (count was ' + count + ')');
session_requests.splice(0, session_requests.length - count);
} else {
session_requests = []
}
}
// performs a GET request to the +url+
// +options+ object includes these options:
// encoding (string), default is to return a buffer
// callback: the body, response,
// and error buffer. get_from helper method is available
exp.get_from_options = function(rid, url, options, callback) {
request.get({
url: url,
headers: {
"User-Agent": "Crafatar (+https://crafatar.com)"
},
timeout: config.server.http_timeout,
followRedirect: false,
encoding: options.encoding || null,
}, function(error, response, body) {
// log url + code + description
var code = response && response.statusCode;
var is_session_req = config.server.sessions_rate_limit && url.startsWith(session_url);
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);
// This is to prevent being blocked by CloudFront for exceeding the rate limit
if (is_session_req && req_count() >= config.server.sessions_rate_limit) {
var e = new Error("Skipped, rate limit exceeded");
e.name = "HTTP";
e.code = "HTTPERROR";
e.code = "RATELIMIT";
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 502: // CloudFront can't reach mojang origin
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;
}
var response = new http.IncomingMessage();
response.statusCode = 403;
if (body && !body.length) {
// empty response
body = null;
}
callback(null, response, e);
} else {
is_session_req && session_requests.push(Date.now());
request.get({
url: url,
headers: {
"User-Agent": "Crafatar (+https://crafatar.com)"
},
timeout: config.server.http_timeout,
followRedirect: false,
encoding: options.encoding || null,
}, function(error, response, body) {
// log url + code + description
var code = response && response.statusCode;
callback(body, response, error);
});
var logfunc = code && (code < 400 || code === 404) ? logging.debug : logging.warn;
logfunc(rid, url, code || error && error.code, http.STATUS_CODES[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 403: // Blocked by CloudFront :(
case 429: // this shouldn't usually happen, but occasionally does
case 500:
case 502: // CloudFront can't reach mojang origin
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;
}
if (body && !body.length) {
// empty response
body = null;
}
callback(body, response, error);
});
}
};
// helper method for get_from_options, no options required

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 B

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,34 +1,64 @@
var valid_user_id = /^[0-9a-f-A-F-]{32,36}$/; // uuid
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 quotes = [
["Crafatar is the best at what it does.", "Shotbow Network", "https://twitter.com/ShotbowNetwork/status/565201303555829762"],
["Crafatar seems to stand out from others", "Dabsunter", "https://github.com/crafatar/crafatar/wiki/What-people-say-about-Crafatar"],
["I cant tell you how much Crafatar helped me along the way! You guys do some amazing work.", "Luke Chatton", "https://github.com/lukechatton"],
["It's just awesome! Keep up the good work", "Dannyps", "https://forums.spongepowered.org/t/title-cant-be-empty/4964/22"],
["It's one of the few services that actually does HTTP header caching correctly", "confuser", "https://github.com/BanManagement/BanManager-WebUI/issues/16#issuecomment-73230674"],
["It's so beautiful. &lt;3", "FerusGrim", "https://twitter.com/FerusGrim/status/642824817683656704"],
["Love it! It's great!", "Reddit User", "https://reddit.com/comments/2nth0j/-/cmh5771"],
["Such a useful service!", "Tim Z, NameMC", "https://twitter.com/CoderTimZ/status/602682146793349120"],
["Thanks for providing us with such a reliable service :)", "BeanBlockz", "https://twitter.com/BeanBlockz/status/743927789422845952"],
["This is excellent for my website! Good work.", "cyanide43", "https://reddit.com/comments/2nth0j/-/cmgpq85"],
["This is really cool!", "AlexWebber", "https://forums.spongepowered.org/t/crafatar-a-new-minecraft-avatar-service/4964/19"],
["This really is looking amazing. Absolutely love it!", "Enter_", "https://forums.spongepowered.org/t/crafatar-a-new-minecraft-avatar-service/4964/21"],
["We couldn't believe how flawless your API is, Good job!", "SenceServers", "https://twitter.com/SenceServers/status/697132506626265089"],
["WOW, Crafatar is FAST", "Rileriscool", "https://twitter.com/rileriscool/status/562057234986065921"],
["You deserve way more popularity", "McSlushie", "https://github.com/crafatar/crafatar/wiki/Credit/a8f37373531b1d2c2cb3557ba809542a2ed81626"],
["You do excellent work on Crafatar and are awesome! A very polished, concise & clean project.", "DrCorporate", "https://reddit.com/comments/2r1ns6/-/cnbq5f1"]
];
// shuffle quotes
for (i = quotes.length -1; i > 0; i--) {
var a = Math.floor(Math.random() * i);
var b = quotes[i];
quotes[i] = quotes[a];
quotes[a] = b;
}
var textures_err = status["textures.minecraft.net"] !== "green";
var session_err = status["sessionserver.mojang.com"] !== "green";
var current_quote = 0;
function changeQuote() {
var elem = document.querySelector("#quote");
var quote = quotes[current_quote];
elem.innerHTML = "<b>“" + quote[0] + "”</b><br>― <i>" + quote[1] + "</i>";
elem.href = quote[2];
current_quote = (current_quote + 1) % quotes.length;
}
fetch('https://mc-heads.net/json/mc_status').then(r => r.json()).then(data => {
var textures_err = data.report.skins.status !== "up";
var session_err = data.report.session.status !== "up";
if (textures_err || session_err) {
var warn = document.createElement("div");
warn.setAttribute("class", "alert alert-warning");
warn.setAttribute("role", "alert");
warn.innerHTML = "<h5>Mojang issues</h5> Mojang's servers are having trouble <i>right now</i>, this may affect requests at Crafatar. <small><a href=\"https://help.mojang.com\" target=\"_blank\">check status</a>";
warn.innerHTML = "<h5>Mojang issues</h5> Mojang's servers are having trouble <i>right now</i>, this may affect requests at Crafatar. <small><a href=\"https://mc-heads.net/mcstatus\" target=\"_blank\">check status</a>";
document.querySelector("#alerts").appendChild(warn);
}
};
});
document.addEventListener("DOMContentLoaded", function(event) {
var avatars = document.querySelector("#avatar-wrapper");
// shuffle avatars
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]);
}
setInterval(changeQuote, 5000);
changeQuote();
var tryit = document.querySelector("#tryit");
var tryname = document.querySelector("#tryname");
var images = document.querySelectorAll(".tryit");
@ -44,7 +74,4 @@ document.addEventListener("DOMContentLoaded", function(event) {
images[j].src = images[j].dataset.src.replace("$", value);
}
};
xhr.open("GET", "https://status.mojang.com/check", true);
xhr.send();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 691 B

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -35,7 +35,6 @@ a.forkme:hover {
a.sponsor {
position: fixed;
z-index: 1041;
width: 48px;
height: 48px;
right: 0px;
@ -43,6 +42,11 @@ a.sponsor {
margin: 5px;
}
.sponsor img {
width: 100%;
height: 100%;
}
a.sponsor-item {
color: #aa7100 !important;
font-weight: initial;
@ -54,6 +58,22 @@ a.sponsor-item {
background: #fff8ec !important;
}
#quote-wrapper {
line-height: 9.5em;
}
#quote {
display: inline-block;
vertical-align: middle;
line-height: initial;
background: #d4e7ff;
border-color: #94cbfc;
}
#quote:hover {
background: #dcedff;
}
.alert {
font-size: 1rem;
}

View File

@ -3,17 +3,18 @@ var config = require("../config");
var crc = require("crc").crc32;
var human_status = {
"-2": "user error", // e.g. invalid size
"-1": "server error", // e.g. mojang/network issues
0: "none", // cached as null (user has no skin)
1: "cached", // found on disk
2: "downloaded", // profile downloaded, skin downloaded from mojang servers
3: "checked", // profile re-downloaded (was too old), has no skin or skin cached
"-2": "user error", // e.g. invalid size
"-1": "server error", // e.g. mojang/network issues
0: "none", // cached as null (user has no skin)
1: "cached", // found on disk
2: "downloaded", // profile downloaded, skin downloaded from mojang servers
3: "checked", // profile re-downloaded (was too old), has no skin or skin cached
4: "server error;cached" // tried to check but ran into server error, using cached version
};
// print these, but without stacktrace
var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED", "HTTPERROR"];
var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED", "HTTPERROR", "RATELIMIT"];
// handles HTTP responses
// +request+ a http.IncomingMessage
@ -30,16 +31,13 @@ module.exports = function(request, response, result) {
// These headers are the same for every response
var headers = {
"Content-Type": result.body && result.type || "text/plain",
"Content-Length": Buffer.from(result.body || "").length,
"Cache-Control": "max-age=" + config.caching.browser,
"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] || "-") + ")");
});
@ -87,13 +85,30 @@ module.exports = function(request, response, result) {
if (result.status === -2) {
response.writeHead(result.code || 422, headers);
} else if (result.status === -1) {
// 500 responses shouldn't be cached
headers["Cache-Control"] = "private, max-age=0, no-cache";
response.writeHead(result.code || 500, headers);
// server errors shouldn't be cached
headers["Cache-Control"] = "no-cache, max-age=0";
if (result.body && result.hash && !result.hash.startsWith("mhf_")) {
headers["Warning"] = '110 Crafatar "Response is Stale"'
headers["Etag"] = etag;
result.code = result.code || 200;
}
if (result.err && result.err.code === "ENOENT") {
result.code = result.code || 500;
}
if (!result.code) {
// Don't use 502 on Cloudflare
// As they will show their own error page instead
// https://support.cloudflare.com/hc/en-us/articles/200172706
result.code = config.caching.cloudflare ? 500 : 502;
}
response.writeHead(result.code, headers);
} else {
if (result.body) {
headers.Etag = etag;
response.writeHead(result.status === 2 ? 201 : 200, headers);
if (result.status === 4) {
headers["Warning"] = '111 Crafatar "Revalidation Failed"'
}
headers["Etag"] = etag;
response.writeHead(200, headers);
} else {
response.writeHead(404, headers);
}

View File

@ -14,12 +14,10 @@ function handle_default(img_status, userId, size, def, req, err, callback) {
if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
if (helpers.id_valid(def)) {
// clean up the old URL to match new image
var parsed = req.url;
delete parsed.query.default;
delete parsed.search;
parsed.path_list[1] = def;
parsed.pathname = "/" + parsed.path_list.join("/");
var newUrl = url.format(parsed);
req.url.searchParams.delete('default');
req.url.path_list[1] = def;
req.url.pathname = req.url.path_list.join('/');
var newUrl = req.url.toString();
callback({
status: img_status,
redirect: newUrl,
@ -53,9 +51,9 @@ function handle_default(img_status, userId, size, def, req, err, callback) {
// GET avatar request
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 overlay = Object.prototype.hasOwnProperty.call(req.url.query, "overlay") || Object.prototype.hasOwnProperty.call(req.url.query, "helm");
var size = parseInt(req.url.searchParams.get("size")) || config.avatars.default_size;
var def = req.url.searchParams.get("default");
var overlay = req.url.searchParams.has("overlay") || req.url.searchParams.has("helm");
// check for extra paths
if (req.url.path_list.length > 2) {
@ -67,6 +65,9 @@ module.exports = function(req, callback) {
return;
}
// strip dashes
userId = userId.replace(/-/g, "");
// Prevent app from crashing/freezing
if (size < config.avatars.min_size || size > config.avatars.max_size) {
// "Unprocessable Entity", valid request, but semantically erroneous:
@ -84,9 +85,6 @@ module.exports = function(req, callback) {
return;
}
// strip dashes
userId = userId.replace(/-/g, "");
try {
helpers.get_avatar(req.id, userId, overlay, size, function(err, status, image, hash) {
if (err) {

View File

@ -4,7 +4,7 @@ var cache = require("../cache");
// GET cape request
module.exports = function(req, callback) {
var userId = (req.url.path_list[1] || "").split(".")[0];
var def = req.url.query.default;
var def = req.url.searchParams.get('default');
var rid = req.id;
// check for extra paths
@ -17,6 +17,8 @@ module.exports = function(req, callback) {
return;
}
// strip dashes
userId = userId.replace(/-/g, "");
if (!helpers.id_valid(userId)) {
callback({
status: -2,
@ -25,9 +27,6 @@ module.exports = function(req, callback) {
return;
}
// strip dashes
userId = userId.replace(/-/g, "");
try {
helpers.get_cape(rid, userId, function(err, hash, status, image) {
if (err) {

View File

@ -7,6 +7,7 @@ var ejs = require("ejs");
var str;
var index;
// pre-compile the index page
function compile() {
logging.log("Compiling index page");
str = read(path.join(__dirname, "..", "views", "index.html.ejs"), "utf-8");

View File

@ -17,12 +17,10 @@ function handle_default(rid, scale, overlay, body, img_status, userId, size, def
if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
if (helpers.id_valid(def)) {
// clean up the old URL to match new image
var parsed = req.url;
delete parsed.query.default;
delete parsed.search;
parsed.path_list[2] = def;
parsed.pathname = "/" + parsed.path_list.join("/");
var newUrl = url.format(parsed);
req.url.searchParams.delete('default');
req.url.path_list[2] = def;
req.url.pathname = req.url.path_list.join('/');
var newUrl = req.url.toString();
callback({
status: img_status,
redirect: newUrl,
@ -62,9 +60,9 @@ module.exports = function(req, callback) {
var rid = req.id;
var body = raw_type === "body";
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 overlay = Object.prototype.hasOwnProperty.call(req.url.query, "overlay") || Object.prototype.hasOwnProperty.call(req.url.query, "helm");
var def = req.url.searchParams.get("default");
var scale = parseInt(req.url.searchParams.get("scale")) || config.renders.default_scale;
var overlay = req.url.searchParams.has("overlay") || req.url.searchParams.has("helm");
// check for extra paths
if (req.url.path_list.length > 3) {
@ -85,6 +83,9 @@ module.exports = function(req, callback) {
return;
}
// strip dashes
userId = userId.replace(/-/g, "");
if (scale < config.renders.min_scale || scale > config.renders.max_scale) {
callback({
status: -2,
@ -99,9 +100,6 @@ module.exports = function(req, callback) {
return;
}
// strip dashes
userId = userId.replace(/-/g, "");
try {
helpers.get_render(rid, userId, scale, overlay, body, function(err, status, hash, image) {
if (err) {

View File

@ -14,12 +14,10 @@ function handle_default(img_status, userId, def, req, err, callback) {
if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
if (helpers.id_valid(def)) {
// clean up the old URL to match new image
var parsed = req.url;
delete parsed.query.default;
delete parsed.search;
parsed.path_list[1] = def;
parsed.pathname = "/" + parsed.path_list.join("/");
var newUrl = url.format(parsed);
req.url.searchParams.delete('default');
req.url.path_list[1] = def;
req.url.pathname = req.url.path_list.join('/');
var newUrl = req.url.toString();
callback({
status: img_status,
redirect: newUrl,
@ -62,7 +60,7 @@ function handle_default(img_status, userId, def, req, err, callback) {
// GET skin request
module.exports = function(req, callback) {
var userId = (req.url.path_list[1] || "").split(".")[0];
var def = req.url.query.default;
var def = req.url.searchParams.get("default");
var rid = req.id;
// check for extra paths
@ -75,6 +73,8 @@ module.exports = function(req, callback) {
return;
}
// strip dashes
userId = userId.replace(/-/g, "");
if (!helpers.id_valid(userId)) {
callback({
status: -2,
@ -83,9 +83,6 @@ module.exports = function(req, callback) {
return;
}
// strip dashes
userId = userId.replace(/-/g, "");
try {
helpers.get_skin(rid, userId, function(err, hash, status, image, slim) {
if (err) {

View File

@ -1,6 +1,7 @@
#!/usr/bin/env node
var querystring = require("querystring");
var response = require("./response");
var helpers = require("./helpers.js");
var toobusy = require("toobusy-js");
var logging = require("./logging");
var config = require("../config");
@ -21,24 +22,33 @@ var routes = {
// serves assets from lib/public
function asset_request(req, callback) {
var filename = path.join(__dirname, "public", req.url.path_list.join("/"));
fs.access(filename, function(fs_err) {
if (!fs_err) {
fs.readFile(filename, function(err, data) {
callback({
body: data,
type: mime.getType(filename),
err: err,
const filename = path.join(__dirname, "public", ...req.url.path_list);
const relative = path.relative(path.join(__dirname, "public"), filename);
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
fs.access(filename, function(fs_err) {
if (!fs_err) {
fs.readFile(filename, function(err, data) {
callback({
body: data,
type: mime.getType(filename),
err: err,
});
});
});
} else {
callback({
body: "Not found",
status: -2,
code: 404,
});
}
});
} else {
callback({
body: "Not found",
status: -2,
code: 404,
});
}
});
} else {
callback({
body: "Forbidden",
status: -2,
code: 403,
});
}
}
// generates a 12 character random string
@ -46,26 +56,18 @@ function request_id() {
return Math.random().toString(36).substring(2, 14);
}
// splits a URL path into an Array
// the path is resolved and decoded
// splits decoded URL path into an Array
function path_list(pathname) {
// remove double and trailing slashes
pathname = pathname.replace(/\/\/+/g, "/").replace(/(.)\/$/, "$1");
var list = pathname.split("/");
list.shift();
for (var i = 0; i < list.length; i++) {
// URL decode
list[i] = querystring.unescape(list[i]);
}
return list;
}
// handles the +req+ by routing to the request to the appropriate module
function requestHandler(req, res) {
req.url = url.parse(req.url, true);
req.url.query = req.url.query || {};
req.url = new URL(decodeURI(req.url), 'http://' + req.headers.host);
req.url.pathname = path.resolve('/', req.url.pathname);
req.url.path_list = path_list(req.url.pathname);
req.id = request_id();
req.start = Date.now();
@ -135,9 +137,10 @@ function requestHandler(req, res) {
var exp = {};
// Start the server
exp.boot = function(callback) {
var port = process.env.PORT || 3000;
var bind_ip = process.env.BIND || "0.0.0.0";
var port = config.server.port;
var bind_ip = config.server.bind;
server = http.createServer(requestHandler).listen(port, bind_ip, function() {
logging.log("Server running on http://" + bind_ip + ":" + port + "/");
if (callback) {
@ -149,7 +152,7 @@ exp.boot = function(callback) {
// wait for established connections to finish (30s max),
// then exit
process.on("SIGTERM", function() {
logging.warn("Got SIGTERM, no longer accepting connections!");
logging.warn("Got SIGTERM, no longer accepting new connections!");
setTimeout(function() {
logging.error("Dropping connections after 30s. Force quit.");
@ -163,7 +166,9 @@ exp.boot = function(callback) {
});
};
// Close the server
exp.close = function(callback) {
helpers.stoplog();
server.close(callback);
};

View File

@ -3,7 +3,7 @@
<head>
<title>Crafatar A blazing fast API for Minecraft faces!</title>
<meta charset="utf-8">
<link rel="icon" sizes="16x16" type="image/png" href="/favicon.png">
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="stylesheet" href="/stylesheets/bootstrap.min.css">
<link rel="stylesheet" href="/stylesheets/style.css">
<meta name="description" content="A blazing fast API for Minecraft faces with support for avatars, skins, and 3D renders!">
@ -61,11 +61,7 @@
<div class="col-md-9">
<section id="documentation">
<div id="alerts">
<div class="alert alert-warning" role="alert">
<h5>Crafatar is rate limited by Mojang!</h5>
Please consider installing your own instance of Crafatar!<br>
For more information visit <a href="https://github.com/crafatar/crafatar/issues/260">our issue tracker</a>.
</div>
</div>
<section id="try">
@ -80,6 +76,7 @@
</div>
</div>
</form>
<p>You can use <a rel="nofollow" target="_blank" href="https://minecraftuuid.com">minecraftuuid.com</a> to find the UUID of a username.</p>
</section>
<section id="avatars">
@ -214,10 +211,12 @@
<h3><a href="#meta-caching">About Caching</a></h3>
<p>
Crafatar checks for skin updates every <%= config.caching.local / 60 %> minutes.<br>
Images are cached in your browser for <%= config.caching.browser / 60 %> minutes until a new request to Crafatar is made.<br>
In addition, <span title="A CDN and caching proxy">CloudFlare</span> caches up to 2 hours on a per-url basis.
Images are also cached in your browser for <%= config.caching.browser / 60 %> minutes unless you clear your browser cache.
<% if (config.caching.cloudflare) { %>
<br>In addition, <span title="A CDN and caching proxy">Cloudflare</span> may cache images as long as your browser would.
<% } %>
</p>
<p>When you changed your skin you can try clearing your browser cache to see the change faster.</p>
<p>After changing your Minecraft skin, you can try clearing your browser cache to see the change faster.</p>
</section>
<section id="meta-cors">
@ -228,11 +227,30 @@
<section id="meta-http-headers">
<h3><a href="#meta-http-headers">HTTP Headers</a></h3>
<p>
Responses come with some custom HTTP headers, useful for debugging.<br>
Please note that these headers may be cached by <span title="A CDN and caching proxy">CloudFlare</span>.
Crafatar always replies with a <code>200 OK</code> status code when the requested user's skin/cape was found. This is also used in some rare cases when Mojang servers are having issues and the image couldn't be checked for changes, but Crafatar still had a cached version.
<% if (config.caching.cloudflare) { %>
<code>500 Server Error</code> is used when no skin/cape was found because of Mojang or Crafatar server issues.
<% } else { %>
<code>502 Bad Gateway</code> and <code>500 Server Error</code> are used when no skin/cape was found because of Mojang or Crafatar server issues.
<% } %>
</p>
<p>
Note that requests are usually answered with an image (with Steve/Alex skin), even if an error occured!
</p>
<p>
Responses come with some HTTP headers that are useful for debugging.
<% if (config.caching.cloudflare) { %>
<br>Please note that these headers may be cached by <span title="A CDN and caching proxy">Cloudflare</span>.
<% } %>
</p>
<ul>
<li>
<b>Warning</b>: When using a cached image after an error occured. One of:
<ul>
<li><code>110 Crafatar "Response is Stale"</code></li>
<li><code>111 Crafatar "Revalidation Failed"</code></li>
</ul>
<li>
<b>X-Storage-Type</b>: Details about how the requested image was stored on the server
<ul>
@ -241,52 +259,56 @@
<li><b>checked</b>: Requested skin details, skin cached. (1 external request)<br>
This happens either when the user removed their skin or when it didn't change.</li>
<li><b>downloaded</b>: Requested skin details, skin downloaded. (2 external requests)</li>
<li><b>server error</b>: This can happen, for example, when Mojang's servers are down.<br>
If possible, a cached image is served instead.</li>
<li><b>server error</b>: This can happen, for example, when Mojang's servers are down.</li>
<li><b>server error;cached</b>: Same as server error, but a cached skin was available.</li>
<li><b>user error</b>: You have done something wrong, such as requesting a malformed uuid.<br>
Check the response body for details.</li>
</ul>
<li>
<b>X-Request-ID</b>: The internal ID assigned to this request.<br>
If you think something is wrong with your request, please <a href="#contact">contact us</a> and provide this ID.
If you think something is wrong with your request, please contact us and provide this ID.
<li>
<b>Response-Time</b>: How long it took Crafatar to answer the request, in ms.
</ul>
</section>
</section>
<section id="contact">
<h2><a href="#contact">Contact</a></h2>
<ul>
<li>Follow us on twitter <a href="https://twitter.com/crafatar" target="_blank">@crafatar</a></li>
<li>Open an issue <a href="https://github.com/crafatar/crafatar/issues" target="_blank">on GitHub</a></li>
<li><a href="https://webchat.esper.net/?channels=crafatar" target="_blank">Join us</a> in <a href="irc://irc.esper.net/crafatar">#crafatar</a> on irc.esper.net</li>
</ul>
</section>
</section>
</div>
<div class="col-md-3">
<h4>Popular Crafatar users</h4>
<div class="list-group">
<a rel="nofollow" href="http://technicpack.net" target="_blank" class="list-group-item">Technic</a>
<a rel="nofollow" href="https://hypixel.net" target="_blank" class="list-group-item">Hypixel</a>
<a rel="nofollow" href="https://www.minecraft-index.com" target="_blank" class="list-group-item">MC Index</a>
<a rel="nofollow" href="https://www.minehq.com/hcteams/leaderboards" target="_blank" class="list-group-item">MineHQ</a>
<a rel="nofollow" href="https://shotbow.net" target="_blank" class="list-group-item">Shotbow</a>
<a rel="nofollow" href="https://mineplex.com" target="_blank" class="list-group-item">Mineplex</a>
<a rel="nofollow" href="https://hivemc.com" target="_blank" class="list-group-item">The Hive</a>
<a rel="nofollow" href="https://www.technicpack.net" target="_blank" class="list-group-item">Technic Pack</a>
<a rel="nofollow" href="https://namemc.com" target="_blank" class="list-group-item">NameMC</a>
<a rel="nofollow" href="https://mcuuid.net/" target="_blank" class="list-group-item">MCUUID</a>
<a href="https://github.com/crafatar/crafatar/wiki/Who-uses-crafatar%3F" target="_blank" class="list-group-item">and many more…</a>
</div>
<p>See also: <a rel="nofollow" href="https://github.com/crafatar/crafatar/wiki/What-people-say-about-Crafatar" target="_blank">what users say</a> about Crafatar</p>
<hr>
<h4>Quotes</h4>
<div id="quote-wrapper" class="list-group">
<a id="quote" rel="nofollow" target="_blank" class="list-group-item"></a>
</div>
<p>See <a rel="nofollow" href="https://github.com/crafatar/crafatar/wiki/What-people-say-about-Crafatar" target="_blank">all quotes</a>.</p>
<hr>
<h4>Crafatar Tools & Plugins</h4>
<div class="list-group">
<a rel="nofollow" href="https://github.com/DiscordSRV/DiscordSRV#readme" target="_blank" class="list-group-item">DiscordSRV</a>
<a rel="nofollow" href="https://github.com/the-obsidian/discourse-minecraft-avatar" target="_blank" class="list-group-item">Discourse Minecraft Avatar</a>
<a rel="nofollow" href="https://xenforo.com/community/resources/associationmc.3232/" target="_blank" class="list-group-item">AssociationMc <i>(XenForo)</i></a>
<a rel="nofollow" href="https://github.com/yeahwhat-mc/discourse-yeahwhat" target="_blank" class="list-group-item">Minecraft Heads <i>(Discourse)</i></a>
<a rel="nofollow" href="http://vanillaforums.org/addon/crafatar-plugin" target="_blank" class="list-group-item">Crafatar Avatars <i>(Vanilla)</i></a>
<a rel="nofollow" href="https://www.spigotmc.org/resources/picture-login.4514/" target="_blank" class="list-group-item">Picture Login <i>(Bukkit)</i></a>
<a rel="nofollow" href="https://github.com/sk89q/Plumeria" target="_blank" class="list-group-item">Plumeria <i>(Discord)</i></a>
<a rel="nofollow" href="https://open.vanillaforums.com/addon/crafatar-plugin" target="_blank" class="list-group-item">Crafatar Avatars <i>(Vanilla)</i></a>
<a rel="nofollow" href="https://www.spigotmc.org/resources/picture-login.4514/" target="_blank" class="list-group-item">Picture Login <i>(Spigot/Bukkit)</i></a>
<a href="https://github.com/crafatar/crafatar/wiki/Who-uses-crafatar%3F#other-services-using-crafatar" target="_blank" class="list-group-item">and many more…</a>
</div>
<hr>
<h4>Contact</h4>
<div class="list-group">
<a class="list-group-item" href="https://twitter.com/crafatar" target="_blank">@crafatar on Twitter</a>
<a class="list-group-item" href="https://github.com/crafatar/crafatar/issues" target="_blank">Issue tracker</a>
</div>
<% if (config.sponsor.sidebar) { %>
<hr>
<%- config.sponsor.sidebar %>
<% } %>
</div>
@ -299,4 +321,4 @@
</div>
</footer>
</body>
</html>
</html>

3380
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,53 +1,25 @@
{
"name": "crafatar",
"version": "2.1.0",
"version": "2.1.5",
"private": true,
"description": "A blazing fast API for Minecraft faces!",
"contributors": [
{
"name": "jomo",
"url": "https://github.com/jomo"
},
{
"name": "Jake",
"url": "https://github.com/Jake0oo0"
}
],
"repository": {
"type": "git",
"url": "https://github.com/crafatar/crafatar"
},
"issues": {
"url": "https://github.com/crafatar/crafatar/issues"
},
"keywords": [
"minecraft",
"avatar"
],
"scripts": {
"postinstall": "cp 'config.example.js' 'config.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"
"test": "mocha"
},
"engines": {
"node": "10.19.0"
"node": "12.16.1"
},
"dependencies": {
"@randy.tarampi/lwip": "^1.3.1",
"canvas": "^2.6.1",
"crc": "^3.8.0",
"ejs": "^3.0.1",
"@randy.tarampi/lwip": "^1.1.0",
"mime": "^2.4.4",
"node-df": "crafatar/node-df",
"ejs": "^3.1.5",
"mime": "^2.4.6",
"redis": "^3.0.2",
"request": "^2.88.2",
"toobusy-js": "^0.5.1"
},
"devDependencies": {
"coveralls": "^3.0.11",
"istanbul": "^0.4.5",
"mocha": "^7.1.1",
"mocha-lcov-reporter": "^1.3.0"
"mocha": "^7.2.0"
}
}
}

View File

@ -1,36 +1,55 @@
#!/usr/bin/env bash
hostname="crafatar.com"
async="true"
random="false"
interval="0.1"
if [ "$1" = "-s" ]; then
async=""
shift
elif [ "$1" = "-i" ]; then
interval="$2"
shift 2
fi
host="$1"
shift
if [ -z "$host" ] || [ ! -z "$@" ]; then
echo "Usage: $0 [-s | -i <interval>] <host uri>"
exit 1
fi
# insert newline after uuids
ids="$(cat 'uuids.txt')"
# `brew install coreutils` on OS X
ids="$(shuf <<< "$ids" 2>/dev/null || gshuf <<< "$ids")"
usage() {
echo "Usage: $0 [-s | -r | -i <interval> | -h <hostname>]... <host uri>" >&2
exit 1
}
get_ids() {
local shuf
if [ "$random" = "true" ]; then
while true; do uuid -v 4; done
else
# `brew install coreutils` on OS X for gshuf
shuf=$(command -v shuf gshuf)
# randomize ids
$shuf < uuids.txt
fi
}
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?overlay"
trap return INT # return from this function on Ctrl+C
get_ids | while read id; do
if [ "$async" = "false" ]; then
curl -H "Host: $hostname" -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?overlay" &
curl -H "Host: $hostname" -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?overlay" &
sleep "$interval"
fi
done
}
while [ $# != 0 ]; do
case "$1" in
-s)
async="false";;
-r)
random="true";;
-i)
interval="$2"
shift;;
*)
[ -n "$host" ] && usage
host="$1";;
esac
shift
done
[ -z "$host" ] && usage
time bulk

View File

@ -3,13 +3,12 @@
// no spam
var logging = require("../lib/logging");
if (process.env.VERBOSE_TEST !== "true" && process.env.TRAVIS !== "true") {
if (process.env.VERBOSE_TEST !== "true") {
logging.log = logging.debug = logging.warn = logging.error = function() {};
}
var networking = require("../lib/networking");
var helpers = require("../lib/helpers");
var cleaner = require("../lib/cleaner");
var request = require("request");
var config = require("../config");
var server = require("../lib/server");
@ -68,10 +67,6 @@ describe("Crafatar", function() {
console.log("Flushing and waiting for redis ...");
cache.get_redis().flushall(function() {
console.log("Redis flushed!");
// cause I don't know how big hard drives are these days
config.cleaner.disk_limit = Infinity;
config.cleaner.redis_limit = Infinity;
cleaner.run();
done();
});
});
@ -93,8 +88,8 @@ describe("Crafatar", function() {
assert.strictEqual(helpers.id_valid("1DCEF164FF0A47F2B9A691385C774EE7"), true);
done();
});
it("dashed uuid is valid", function(done) {
assert.strictEqual(helpers.id_valid("0098cb60-fa8e-427c-b299-793cbd302c9a"), true);
it("dashed uuid is not valid", function(done) {
assert.strictEqual(helpers.id_valid("0098cb60-fa8e-427c-b299-793cbd302c9a"), false);
done();
});
it("username is invalid", function(done) {
@ -163,7 +158,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(), config.endpoints.textures_url + "477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", function(body, res, error) {
assert.notStrictEqual(["ETIMEDOUT", "ESOCKETTIMEDOUT"].indexOf(error.code), -1);
config.server.http_timeout = original_timeout;
done();
@ -171,7 +166,7 @@ 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(), config.endpoints.textures_url + "this-does-not-exist", function(img, response, err) {
assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions
done();
});
@ -263,7 +258,7 @@ describe("Crafatar", function() {
var url = "http://localhost:3000/%61%76%61%74%61%72%73/%61%65%37%39%35%61%61%38%36%33%32%37%34%30%38%65%39%32%61%62%32%35%63%38%61%35%39%66%33%62%61%31"; // avatars/ae795aa86327408e92ab25c8a59f3ba1
request.get(url, function(error, res, body) {
assert.ifError(error);
assert.strictEqual(res.statusCode, 201);
assert.strictEqual(res.statusCode, 200);
assert_headers(res);
assert(res.headers.etag);
assert.strictEqual(res.headers["content-type"], "image/png");
@ -286,7 +281,7 @@ describe("Crafatar", function() {
function req() {
request.get(url, function(error, res, body) {
assert.ifError(error);
assert.strictEqual(res.statusCode === 201 || res.statusCode === 200, true);
assert.strictEqual(res.statusCode, 200);
assert_headers(res);
assert(res.headers.etag);
assert.strictEqual(res.headers["content-type"], "image/png");
@ -303,20 +298,24 @@ describe("Crafatar", function() {
var server_tests = {
"avatar with existing uuid": {
url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16",
crc32: [3337292777],
crc32: [4264176600],
},
"avatar with existing dashed uuid": {
url: "http://localhost:3000/avatars/853c80ef-3c37-49fd-aa49938b674adae6?size=16",
crc32: [4264176600],
},
"avatar with non-existent uuid": {
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16",
crc32: [2416827277, 1243826040],
crc32: [3348154329],
},
"avatar with non-existent uuid defaulting to mhf_alex": {
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=mhf_alex",
crc32: [862751081, 809395677],
crc32: [73899130],
},
"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",
redirect: "http://localhost:3000/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%2FCaseSensitive",
@ -325,20 +324,20 @@ describe("Crafatar", function() {
},
"overlay avatar with existing uuid": {
url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&overlay",
crc32: [1710265722],
crc32: [575355728],
},
"overlay avatar with non-existent uuid": {
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay",
crc32: [2416827277, 1243826040],
crc32: [3348154329],
},
"overlay avatar with non-existent uuid defaulting to mhf_alex": {
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay&default=mhf_alex",
crc32: [862751081, 809395677],
crc32: [73899130],
},
"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",
redirect: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16",
},
"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",
@ -347,7 +346,7 @@ describe("Crafatar", function() {
},
"cape with existing uuid": {
url: "http://localhost:3000/capes/853c80ef3c3749fdaa49938b674adae6",
crc32: [2556702429],
crc32: [985789174, 2099310578],
},
"cape with non-existent uuid": {
url: "http://localhost:3000/capes/00000000000000000000000000000000",
@ -360,20 +359,20 @@ describe("Crafatar", function() {
},
"skin with existing uuid": {
url: "http://localhost:3000/skins/853c80ef3c3749fdaa49938b674adae6",
crc32: [26500336],
crc32: [1759176487],
},
"skin with non-existent uuid": {
url: "http://localhost:3000/skins/00000000000000000000000000000000",
crc32: [981937087],
crc32: [1853029228],
},
"skin with non-existent uuid defaulting to mhf_alex": {
url: "http://localhost:3000/skins/00000000000000000000000000000000?default=mhf_alex",
crc32: [2298915739],
crc32: [427506205],
},
"skin with non-existent uuid defaulting to uuid": {
url: "http://localhost:3000/skins/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6",
crc32: [0],
redirect: "/skins/853c80ef3c3749fdaa49938b674adae6?size=16",
redirect: "http://localhost:3000/skins/853c80ef3c3749fdaa49938b674adae6?size=16",
},
"skin with non-existent uuid defaulting to url": {
url: "http://localhost:3000/skins/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com%2FCaseSensitive",
@ -395,7 +394,7 @@ describe("Crafatar", function() {
"head render with non-existent uuid defaulting to uuid": {
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=853c80ef3c3749fdaa49938b674adae6",
crc32: [0],
redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2",
redirect: "http://localhost:3000/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%2FCaseSensitive",
@ -417,7 +416,7 @@ describe("Crafatar", function() {
"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&overlay=",
redirect: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay=",
},
"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",
@ -426,7 +425,7 @@ describe("Crafatar", function() {
},
"body render with existing uuid": {
url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2",
crc32: [2745192436],
crc32: [1144887125],
},
"body render with non-existent uuid": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2",
@ -434,12 +433,12 @@ describe("Crafatar", function() {
},
"body render with non-existent uuid defaulting to mhf_alex": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=mhf_alex",
crc32: [1255106465],
crc32: [4280894468],
},
"body render with non-existent uuid defaulting to uuid": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=853c80ef3c3749fdaa49938b674adae6",
crc32: [0],
redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2",
redirect: "http://localhost:3000/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%2FCaseSensitive",
@ -448,7 +447,7 @@ describe("Crafatar", function() {
},
"overlay body render with existing uuid": {
url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay",
crc32: [2441671793],
crc32: [1107696668],
},
"overlay body render with non-existent uuid": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay",
@ -456,7 +455,7 @@ describe("Crafatar", function() {
},
"overlay body render with non-existent uuid defaulting to mhf_alex": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay&default=mhf_alex",
crc32: [1255106465],
crc32: [4280894468],
},
"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",
@ -494,7 +493,7 @@ describe("Crafatar", function() {
done();
} else {
assert.strictEqual(res.headers["content-type"], "image/png");
assert.strictEqual(res.statusCode, res.headers["x-storage-type"] === "downloaded" ? 201 : 200);
assert.strictEqual(res.statusCode, 200);
assert(res.headers.etag);
assert.strictEqual(res.headers.etag, '"' + hash + '"');
assert_cache(location.url, res.headers.etag, function() {
@ -540,7 +539,7 @@ describe("Crafatar", function() {
});
it("should return a 422 (invalid render type)", function(done) {
request.get("http://localhost:3000/renders/invalid/Jake_0", function(error, res, body) {
request.get("http://localhost:3000/renders/invalid/2d5aa9cdaeb049189930461fc9b91cc5", function(error, res, body) {
assert.ifError(error);
assert.strictEqual(res.statusCode, 422);
done();
@ -569,6 +568,30 @@ describe("Crafatar", function() {
});
}(loc));
}
it("should return /public resources", function(done) {
request.get("http://localhost:3000/javascript/crafatar.js", function(error, res, body) {
assert.ifError(error);
assert.strictEqual(res.statusCode, 200);
done();
});
});
it("should not allow path traversal on /public", function(done) {
request.get("http://localhost:3000/../server.js", function(error, res, body) {
assert.ifError(error);
assert.strictEqual(res.statusCode, 404);
done();
});
});
it("should not allow encoded path traversal on /public", function(done) {
request.get("http://localhost:3000/%2E%2E/server.js", function(error, res, body) {
assert.ifError(error);
assert.strictEqual(res.statusCode, 404);
done();
});
});
});
// we have to make sure that we test both a 32x64 and 64x64 skin
@ -589,7 +612,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(), "61699b2ed3274a019f1e0ea8c3f06bc6", function(err, hash, status, img) {
assert.strictEqual(err, null);
done();
});
@ -598,13 +621,13 @@ describe("Crafatar", function() {
before(function() {
cache.get_redis().flushall();
});
helpers.get_cape(rid(), "Dinnerbone", function(err, hash, status, img) {
helpers.get_cape(rid(), "61699b2ed3274a019f1e0ea8c3f06bc6", 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(), "2d5aa9cdaeb049189930461fc9b91cc5", function(err, hash, status, img) {
assert.ifError(err);
assert.strictEqual(img, null);
done();
@ -614,7 +637,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(), "2d5aa9cdaeb049189930461fc9b91cc5", function(err, hash, status, img) {
assert.strictEqual(err, null);
done();
});
@ -623,7 +646,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(), "2d5aa9cdaeb049189930461fc9b91cc5", function(err, hash, status, img) {
assert.strictEqual(err, null);
done();
});
@ -687,15 +710,28 @@ describe("Crafatar", function() {
describe("Errors", function() {
before(function() {
before(function() {
cache.get_redis().flushall();
});
it("uuid SHOULD be rate limited", function(done) {
// Mojang has changed its rate limiting, so we no longer expect to hit the rate limit
// it("uuid SHOULD be rate limited", function(done) {
// networking.get_profile(rid(), uuid, function() {
// networking.get_profile(rid(), uuid, function(err, profile) {
// assert.strictEqual(err.toString(), "HTTP: 429");
// assert.strictEqual(profile, null);
// done();
// });
// });
// });
it("CloudFront rate limit is handled", function(done) {
var original_rate_limit = config.server.sessions_rate_limit;
config.server.sessions_rate_limit = 1;
networking.get_profile(rid(), uuid, function() {
networking.get_profile(rid(), uuid, function(err, profile) {
assert.strictEqual(err.toString(), "HTTP: 429");
assert.strictEqual(profile, null);
assert.strictEqual(err.code, "RATELIMIT");
config.server.sessions_rate_limit = original_rate_limit;
done();
});
});

View File

@ -4424,4 +4424,4 @@ ffdd082bf54e415b943a8713f2885913
ffe0be5f0cab4b3785f67974c23660bb
ffe3d4c861354928b932794d85a30567
ffe72a222ac9463d81d3ee5eafb7f68e
fff854a189644f12b92764fdb4573f8b
fff854a189644f12b92764fdb4573f8b

4
www.js
View File

@ -1,5 +1,5 @@
var networking = require("./lib/networking");
var logging = require("./lib/logging");
var cleaner = require("./lib/cleaner");
var config = require("./config");
process.on("uncaughtException", function(err) {
@ -7,6 +7,6 @@ process.on("uncaughtException", function(err) {
process.exit(1);
});
setInterval(cleaner.run, config.cleaner.interval * 1000);
setInterval(networking.resetCounter, 1000);
require("./lib/server.js").boot();