mirror of
https://github.com/azures04/crafatar.git
synced 2026-03-22 07:51:17 +01:00
Compare commits
No commits in common. "master" and "v2.1.1" have entirely different histories.
2
.buildpacks
Normal file
2
.buildpacks
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
https://github.com/mojodna/heroku-buildpack-cairo.git
|
||||||
|
https://github.com/heroku/heroku-buildpack-nodejs.git
|
||||||
@ -1,6 +0,0 @@
|
|||||||
.*
|
|
||||||
*.md
|
|
||||||
Dockerfile
|
|
||||||
LICENSE
|
|
||||||
images/
|
|
||||||
node_modules/
|
|
||||||
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 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
6
.gitignore
vendored
@ -1,3 +1,9 @@
|
|||||||
images/*/*.png
|
images/*/*.png
|
||||||
node_modules/
|
node_modules/
|
||||||
coverage/
|
coverage/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
*.rdb
|
||||||
|
*.sublime-*
|
||||||
|
config.js
|
||||||
|
lib/public/images/sponsor.png
|
||||||
|
|||||||
24
.travis.yml
Normal file
24
.travis.yml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- 12.16.1
|
||||||
|
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
|
||||||
|
env:
|
||||||
|
- TRAVIS=true CXX=g++-4.8
|
||||||
|
services:
|
||||||
|
- redis-server
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- node_modules
|
||||||
52
Dockerfile
52
Dockerfile
@ -1,35 +1,27 @@
|
|||||||
FROM node:12-alpine AS builder
|
FROM node:12-alpine
|
||||||
|
|
||||||
RUN apk --no-cache add git python3 build-base redis cairo-dev pango-dev jpeg-dev giflib-dev
|
ARG REDIS_URL
|
||||||
|
ARG DEBUG
|
||||||
|
ARG EPHEMERAL_STORAGE
|
||||||
|
|
||||||
RUN adduser -D app
|
RUN apk --no-cache --virtual .build-deps add git python build-base
|
||||||
USER app
|
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
|
||||||
|
|
||||||
COPY --chown=app package.json package-lock.json /home/app/crafatar/
|
|
||||||
WORKDIR /home/app/crafatar
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
COPY --chown=app . .
|
EXPOSE 3000
|
||||||
RUN mkdir -p images/faces images/helms images/skins images/renders images/capes
|
ENTRYPOINT npm start
|
||||||
|
|
||||||
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
|
|
||||||
35
README.md
35
README.md
@ -1,8 +1,8 @@
|
|||||||
# Crafatar
|
# Crafatar [](https://travis-ci.org/crafatar/crafatar/) [](https://coveralls.io/r/crafatar/crafatar) [](https://codeclimate.com/github/crafatar/crafatar)
|
||||||
<img alt="logo" src="lib/public/logo.png" align="right" width="128px" height="128px">
|
[](https://david-dm.org/crafatar/crafatar) [](https://david-dm.org/crafatar/crafatar#info=devDependencies) [](https://inch-ci.org/github/crafatar/crafatar)
|
||||||
|
|
||||||
[](https://travis-ci.org/crafatar/crafatar/) [](https://coveralls.io/r/crafatar/crafatar) [](https://codeclimate.com/github/crafatar/crafatar) [](https://david-dm.org/crafatar/crafatar) [](https://david-dm.org/crafatar/crafatar#info=devDependencies) [](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.
|
<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>.
|
Inspired by <a href="https://gravatar.com">Gravatar</a> (hence the name) and <a href="https://minotar.net">Minotar</a>.
|
||||||
|
|
||||||
@ -34,14 +34,6 @@ Please [visit the website](https://crafatar.com) for details.
|
|||||||
|
|
||||||
# Installation
|
# 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
|
## Manual
|
||||||
|
|
||||||
- Install [nodejs](https://nodejs.org/) 12 (LTS)
|
- Install [nodejs](https://nodejs.org/) 12 (LTS)
|
||||||
@ -52,9 +44,26 @@ docker run --net crafatar -v crafatar-images:/home/app/crafatar/images -e REDIS_
|
|||||||
|
|
||||||
Crafatar is now available at http://0.0.0.0:3000.
|
Crafatar is now available at http://0.0.0.0:3000.
|
||||||
|
|
||||||
## Configration / Environment variables
|
## Docker
|
||||||
|
|
||||||
See the `config.js` file.
|
```sh
|
||||||
|
docker pull crafatar/crafatar
|
||||||
|
docker network create crafatar
|
||||||
|
docker run --net crafatar -d --name redis redis
|
||||||
|
docker run --net crafatar -v crafatar-images:/crafatar/images -e REDIS_URL=redis://redis -p 3000:3000 crafatar/crafatar
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| 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* |
|
||||||
|
|
||||||
|
\* Use this to avoid issues when you have a persistent redis database but an ephemeral storage
|
||||||
|
|
||||||
# Operational notes
|
# Operational notes
|
||||||
|
|
||||||
|
|||||||
30
app.json
Normal file
30
app.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
40
config.example.js
Normal file
40
config.example.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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
69
config.js
@ -1,69 +0,0 @@
|
|||||||
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;
|
|
||||||
43
lib/cache.js
43
lib/cache.js
@ -1,6 +1,7 @@
|
|||||||
var logging = require("./logging");
|
var logging = require("./logging");
|
||||||
var node_redis = require("redis");
|
var node_redis = require("redis");
|
||||||
var config = require("../config");
|
var config = require("../config");
|
||||||
|
var url = require("url");
|
||||||
|
|
||||||
var redis = null;
|
var redis = null;
|
||||||
|
|
||||||
@ -8,10 +9,19 @@ var redis = null;
|
|||||||
// flushes redis when using ephemeral storage (e.g. Heroku)
|
// flushes redis when using ephemeral storage (e.g. Heroku)
|
||||||
function connect_redis() {
|
function connect_redis() {
|
||||||
logging.log("connecting to redis...");
|
logging.log("connecting to redis...");
|
||||||
redis = node_redis.createClient(config.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.on("ready", function() {
|
redis.on("ready", function() {
|
||||||
logging.log("Redis connection established.");
|
logging.log("Redis connection established.");
|
||||||
if (config.caching.ephemeral) {
|
if (process.env.EPHEMERAL_STORAGE) {
|
||||||
logging.log("Storage is ephemeral, flushing redis");
|
logging.log("Storage is ephemeral, flushing redis");
|
||||||
redis.flushall();
|
redis.flushall();
|
||||||
}
|
}
|
||||||
@ -31,6 +41,35 @@ exp.get_redis = function() {
|
|||||||
return redis;
|
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*
|
// set model type to value of *slim*
|
||||||
exp.set_slim = function(rid, userId, slim, callback) {
|
exp.set_slim = function(rid, userId, slim, callback) {
|
||||||
logging.debug(rid, "setting slim for", userId, "to " + slim);
|
logging.debug(rid, "setting slim for", userId, "to " + slim);
|
||||||
|
|||||||
@ -7,8 +7,8 @@ var skins = require("./skins");
|
|||||||
var path = require("path");
|
var path = require("path");
|
||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
|
|
||||||
// 0098cb60fa8e427cb299793cbd302c9a
|
// 0098cb60-fa8e-427c-b299-793cbd302c9a
|
||||||
var valid_user_id = /^[0-9a-fA-F]{32}$/; // uuid
|
var valid_user_id = /^[0-9a-f-A-F-]{32,36}$/; // uuid
|
||||||
var hash_pattern = /[0-9a-f]+$/;
|
var hash_pattern = /[0-9a-f]+$/;
|
||||||
|
|
||||||
// gets the hash from the textures.minecraft.net +url+
|
// gets the hash from the textures.minecraft.net +url+
|
||||||
@ -122,22 +122,13 @@ var requests = {
|
|||||||
cape: {}
|
cape: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
var loginterval = setInterval(function(){
|
function push_request(userId, type, fun) {
|
||||||
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')
|
// avoid special properties (e.g. 'constructor')
|
||||||
var userId_safe = "!" + userId;
|
var userId_safe = "!" + userId;
|
||||||
if (!requests[type][userId_safe]) {
|
if (!requests[type][userId_safe]) {
|
||||||
requests[type][userId_safe] = [];
|
requests[type][userId_safe] = [];
|
||||||
}
|
}
|
||||||
requests[type][userId_safe].push(callback);
|
requests[type][userId_safe].push(fun);
|
||||||
}
|
}
|
||||||
|
|
||||||
// calls back all queued requests that match userId and type
|
// calls back all queued requests that match userId and type
|
||||||
@ -171,6 +162,7 @@ function store_images(rid, userId, cache_details, type, callback) {
|
|||||||
logging.debug(rid, "adding to request queue");
|
logging.debug(rid, "adding to request queue");
|
||||||
push_request(userId, type, callback);
|
push_request(userId, type, callback);
|
||||||
} else {
|
} else {
|
||||||
|
// add request to the queue
|
||||||
push_request(userId, type, callback);
|
push_request(userId, type, callback);
|
||||||
|
|
||||||
networking.get_profile(rid, userId, function(err, profile) {
|
networking.get_profile(rid, userId, function(err, profile) {
|
||||||
@ -184,7 +176,7 @@ function store_images(rid, userId, cache_details, type, callback) {
|
|||||||
resume(userId, "cape", cache_err, null, false);
|
resume(userId, "cape", cache_err, null, false);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// an error occured, not caching. we can try again in 60 seconds
|
// an error occured, not caching. we can try in 60 seconds
|
||||||
resume(userId, type, err, null, false);
|
resume(userId, type, err, null, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -248,13 +240,10 @@ exp.get_image_hash = function(rid, userId, type, callback) {
|
|||||||
}
|
}
|
||||||
store_images(rid, userId, cache_details, type, function(store_err, new_hash, slim) {
|
store_images(rid, userId, cache_details, type, function(store_err, new_hash, slim) {
|
||||||
if (store_err) {
|
if (store_err) {
|
||||||
// an error occured, but we have a cached hash
|
// we might have a cached hash although an error occured
|
||||||
// (e.g. Mojang servers not reachable, using outdated hash)
|
// (e.g. Mojang servers not reachable, using outdated hash)
|
||||||
|
cache.update_timestamp(rid, userId, true, function(err2) {
|
||||||
// bump the TTL after hitting the rate limit
|
callback(err2 || store_err, -1, cache_details && cached_hash, slim);
|
||||||
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 {
|
} else {
|
||||||
var status = cache_details && (cached_hash === new_hash) ? 3 : 2;
|
var status = cache_details && (cached_hash === new_hash) ? 3 : 2;
|
||||||
@ -332,7 +321,7 @@ function get_type(overlay, body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handles creations of 3D renders
|
// handles creations of 3D renders
|
||||||
// callback: error, status, skin hash, image buffer
|
// callback: error, skin hash, image buffer
|
||||||
exp.get_render = function(rid, userId, scale, overlay, body, callback) {
|
exp.get_render = function(rid, userId, scale, overlay, body, callback) {
|
||||||
exp.get_skin(rid, userId, function(err, skin_hash, status, img, slim) {
|
exp.get_skin(rid, userId, function(err, skin_hash, status, img, slim) {
|
||||||
if (!skin_hash) {
|
if (!skin_hash) {
|
||||||
@ -358,7 +347,7 @@ exp.get_render = function(rid, userId, scale, overlay, body, callback) {
|
|||||||
callback(null, 0, skin_hash, null);
|
callback(null, 0, skin_hash, null);
|
||||||
} else {
|
} else {
|
||||||
fs.writeFile(renderpath, drawn_img, "binary", function(write_err) {
|
fs.writeFile(renderpath, drawn_img, "binary", function(write_err) {
|
||||||
callback(write_err, status, skin_hash, drawn_img);
|
callback(write_err, 2, skin_hash, drawn_img);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -395,8 +384,4 @@ exp.get_cape = function(rid, userId, callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exp.stoplog = function() {
|
|
||||||
clearInterval(loginterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = exp;
|
module.exports = exp;
|
||||||
@ -23,20 +23,16 @@ function log(level, args, logger) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// log with INFO level
|
|
||||||
exp.log = function() {
|
exp.log = function() {
|
||||||
log(" INFO", arguments);
|
log(" INFO", arguments);
|
||||||
};
|
};
|
||||||
// log with WARN level
|
|
||||||
exp.warn = function() {
|
exp.warn = function() {
|
||||||
log(" WARN", arguments, console.warn);
|
log(" WARN", arguments, console.warn);
|
||||||
};
|
};
|
||||||
// log with ERROR level
|
|
||||||
exp.error = function() {
|
exp.error = function() {
|
||||||
log("ERROR", arguments, console.error);
|
log("ERROR", arguments, console.error);
|
||||||
};
|
};
|
||||||
// log with DEBUG level if debug logging is enabled
|
if (config.server.debug_enabled || process.env.DEBUG === "true") {
|
||||||
if (config.server.debug_enabled) {
|
|
||||||
exp.debug = function() {
|
exp.debug = function() {
|
||||||
log("DEBUG", arguments);
|
log("DEBUG", arguments);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,121 +1,80 @@
|
|||||||
|
var http_code = require("http").STATUS_CODES;
|
||||||
var logging = require("./logging");
|
var logging = require("./logging");
|
||||||
var request = require("request");
|
var request = require("request");
|
||||||
var config = require("../config");
|
var config = require("../config");
|
||||||
var skins = require("./skins");
|
var skins = require("./skins");
|
||||||
var http = require("http");
|
|
||||||
require("./object-patch");
|
require("./object-patch");
|
||||||
|
|
||||||
var session_url = config.endpoints.session_url;
|
var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/";
|
||||||
var textures_url = config.endpoints.textures_url;
|
var textures_url = "http://textures.minecraft.net/texture/";
|
||||||
|
|
||||||
// count requests made to session_url in the last 1000ms
|
|
||||||
var session_requests = [];
|
|
||||||
|
|
||||||
var exp = {};
|
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+
|
// performs a GET request to the +url+
|
||||||
// +options+ object includes these options:
|
// +options+ object includes these options:
|
||||||
// encoding (string), default is to return a buffer
|
// encoding (string), default is to return a buffer
|
||||||
// callback: the body, response,
|
// callback: the body, response,
|
||||||
// and error buffer. get_from helper method is available
|
// and error buffer. get_from helper method is available
|
||||||
exp.get_from_options = function(rid, url, options, callback) {
|
exp.get_from_options = function(rid, url, options, callback) {
|
||||||
var is_session_req = config.server.sessions_rate_limit && url.startsWith(session_url);
|
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;
|
||||||
|
|
||||||
// This is to prevent being blocked by CloudFront for exceeding the rate limit
|
var logfunc = code && code < 405 ? logging.debug : logging.warn;
|
||||||
if (is_session_req && req_count() >= config.server.sessions_rate_limit) {
|
logfunc(rid, url, code || error && error.code, http_code[code]);
|
||||||
var e = new Error("Skipped, rate limit exceeded");
|
|
||||||
|
// not necessarily used
|
||||||
|
var e = new Error(code);
|
||||||
e.name = "HTTP";
|
e.name = "HTTP";
|
||||||
e.code = "RATELIMIT";
|
e.code = "HTTPERROR";
|
||||||
|
|
||||||
var response = new http.IncomingMessage();
|
switch (code) {
|
||||||
response.statusCode = 403;
|
case 200:
|
||||||
|
case 301:
|
||||||
callback(null, response, e);
|
case 302: // never seen, but mojang might use it in future
|
||||||
} else {
|
case 307: // never seen, but mojang might use it in future
|
||||||
is_session_req && session_requests.push(Date.now());
|
case 308: // never seen, but mojang might use it in future
|
||||||
request.get({
|
// these are okay
|
||||||
url: url,
|
break;
|
||||||
headers: {
|
case 204: // no content, used like 404 by mojang. making sure it really has no content
|
||||||
"User-Agent": "Crafatar (+https://crafatar.com)"
|
case 404:
|
||||||
},
|
// can be cached as null
|
||||||
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 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;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
callback(body, response, error);
|
if (body && !body.length) {
|
||||||
});
|
// empty response
|
||||||
}
|
body = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(body, response, error);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// helper method for get_from_options, no options required
|
// 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: 2.4 KiB After Width: | Height: | Size: 428 B |
@ -1,64 +1,34 @@
|
|||||||
var valid_user_id = /^[0-9a-f-A-F-]{32,36}$/; // uuid
|
var valid_user_id = /^[0-9a-f-A-F-]{32,36}$/; // uuid
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
var quotes = [
|
xhr.onload = function() {
|
||||||
["Crafatar is the best at what it does.", "Shotbow Network", "https://twitter.com/ShotbowNetwork/status/565201303555829762"],
|
var response = JSON.parse(xhr.responseText);
|
||||||
["Crafatar seems to stand out from others", "Dabsunter", "https://github.com/crafatar/crafatar/wiki/What-people-say-about-Crafatar"],
|
var status = {};
|
||||||
["I can’t tell you how much Crafatar helped me along the way! You guys do some amazing work.", "Luke Chatton", "https://github.com/lukechatton"],
|
response.map(function(elem) {
|
||||||
["It's just awesome! Keep up the good work", "Dannyps", "https://forums.spongepowered.org/t/title-cant-be-empty/4964/22"],
|
var key = Object.keys(elem)[0];
|
||||||
["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"],
|
status[key] = elem[key];
|
||||||
["It's so beautiful. <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 current_quote = 0;
|
var textures_err = status["textures.minecraft.net"] !== "green";
|
||||||
|
var session_err = status["sessionserver.mojang.com"] !== "green";
|
||||||
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) {
|
if (textures_err || session_err) {
|
||||||
var warn = document.createElement("div");
|
var warn = document.createElement("div");
|
||||||
warn.setAttribute("class", "alert alert-warning");
|
warn.setAttribute("class", "alert alert-warning");
|
||||||
warn.setAttribute("role", "alert");
|
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://mc-heads.net/mcstatus\" 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://help.mojang.com\" target=\"_blank\">check status</a>";
|
||||||
document.querySelector("#alerts").appendChild(warn);
|
document.querySelector("#alerts").appendChild(warn);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function(event) {
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
var avatars = document.querySelector("#avatar-wrapper");
|
var avatars = document.querySelector("#avatar-wrapper");
|
||||||
// shuffle avatars
|
|
||||||
for (var i = 0; i < avatars.children.length; i++) {
|
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]);
|
avatars.appendChild(avatars.children[Math.random() * i | 0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(changeQuote, 5000);
|
|
||||||
changeQuote();
|
|
||||||
|
|
||||||
var tryit = document.querySelector("#tryit");
|
var tryit = document.querySelector("#tryit");
|
||||||
var tryname = document.querySelector("#tryname");
|
var tryname = document.querySelector("#tryname");
|
||||||
var images = document.querySelectorAll(".tryit");
|
var images = document.querySelectorAll(".tryit");
|
||||||
@ -74,4 +44,7 @@ document.addEventListener("DOMContentLoaded", function(event) {
|
|||||||
images[j].src = images[j].dataset.src.replace("$", value);
|
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: 8.3 KiB After Width: | Height: | Size: 691 B |
@ -35,6 +35,7 @@ a.forkme:hover {
|
|||||||
|
|
||||||
a.sponsor {
|
a.sponsor {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
z-index: 1041;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
@ -42,11 +43,6 @@ a.sponsor {
|
|||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sponsor img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.sponsor-item {
|
a.sponsor-item {
|
||||||
color: #aa7100 !important;
|
color: #aa7100 !important;
|
||||||
font-weight: initial;
|
font-weight: initial;
|
||||||
@ -58,22 +54,6 @@ a.sponsor-item {
|
|||||||
background: #fff8ec !important;
|
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 {
|
.alert {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,18 +3,17 @@ var config = require("../config");
|
|||||||
var crc = require("crc").crc32;
|
var crc = require("crc").crc32;
|
||||||
|
|
||||||
var human_status = {
|
var human_status = {
|
||||||
"-2": "user error", // e.g. invalid size
|
"-2": "user error", // e.g. invalid size
|
||||||
"-1": "server error", // e.g. mojang/network issues
|
"-1": "server error", // e.g. mojang/network issues
|
||||||
0: "none", // cached as null (user has no skin)
|
0: "none", // cached as null (user has no skin)
|
||||||
1: "cached", // found on disk
|
1: "cached", // found on disk
|
||||||
2: "downloaded", // profile downloaded, skin downloaded from mojang servers
|
2: "downloaded", // profile downloaded, skin downloaded from mojang servers
|
||||||
3: "checked", // profile re-downloaded (was too old), has no skin or skin cached
|
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
|
// print these, but without stacktrace
|
||||||
var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED", "HTTPERROR", "RATELIMIT"];
|
var silent_errors = ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "EHOSTUNREACH", "ECONNREFUSED", "HTTPERROR"];
|
||||||
|
|
||||||
// handles HTTP responses
|
// handles HTTP responses
|
||||||
// +request+ a http.IncomingMessage
|
// +request+ a http.IncomingMessage
|
||||||
@ -31,7 +30,6 @@ module.exports = function(request, response, result) {
|
|||||||
// These headers are the same for every response
|
// These headers are the same for every response
|
||||||
var headers = {
|
var headers = {
|
||||||
"Content-Type": result.body && result.type || "text/plain",
|
"Content-Type": result.body && result.type || "text/plain",
|
||||||
"Content-Length": Buffer.from(result.body || "").length,
|
|
||||||
"Cache-Control": "max-age=" + config.caching.browser,
|
"Cache-Control": "max-age=" + config.caching.browser,
|
||||||
"Response-Time": Date.now() - request.start,
|
"Response-Time": Date.now() - request.start,
|
||||||
"X-Request-ID": request.id,
|
"X-Request-ID": request.id,
|
||||||
@ -85,30 +83,13 @@ module.exports = function(request, response, result) {
|
|||||||
if (result.status === -2) {
|
if (result.status === -2) {
|
||||||
response.writeHead(result.code || 422, headers);
|
response.writeHead(result.code || 422, headers);
|
||||||
} else if (result.status === -1) {
|
} else if (result.status === -1) {
|
||||||
// server errors shouldn't be cached
|
// 500 responses shouldn't be cached
|
||||||
headers["Cache-Control"] = "no-cache, max-age=0";
|
headers["Cache-Control"] = "private, max-age=0, no-cache";
|
||||||
if (result.body && result.hash && !result.hash.startsWith("mhf_")) {
|
response.writeHead(result.code || 500, headers);
|
||||||
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 {
|
} else {
|
||||||
if (result.body) {
|
if (result.body) {
|
||||||
if (result.status === 4) {
|
headers.Etag = etag;
|
||||||
headers["Warning"] = '111 Crafatar "Revalidation Failed"'
|
response.writeHead(result.status === 2 ? 201 : 200, headers);
|
||||||
}
|
|
||||||
headers["Etag"] = etag;
|
|
||||||
response.writeHead(200, headers);
|
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(404, headers);
|
response.writeHead(404, headers);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,10 +14,12 @@ function handle_default(img_status, userId, size, def, req, err, callback) {
|
|||||||
if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
|
if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
|
||||||
if (helpers.id_valid(def)) {
|
if (helpers.id_valid(def)) {
|
||||||
// clean up the old URL to match new image
|
// clean up the old URL to match new image
|
||||||
req.url.searchParams.delete('default');
|
var parsed = req.url;
|
||||||
req.url.path_list[1] = def;
|
delete parsed.query.default;
|
||||||
req.url.pathname = req.url.path_list.join('/');
|
delete parsed.search;
|
||||||
var newUrl = req.url.toString();
|
parsed.path_list[1] = def;
|
||||||
|
parsed.pathname = "/" + parsed.path_list.join("/");
|
||||||
|
var newUrl = url.format(parsed);
|
||||||
callback({
|
callback({
|
||||||
status: img_status,
|
status: img_status,
|
||||||
redirect: newUrl,
|
redirect: newUrl,
|
||||||
@ -51,9 +53,9 @@ function handle_default(img_status, userId, size, def, req, err, callback) {
|
|||||||
// GET avatar request
|
// GET avatar request
|
||||||
module.exports = function(req, callback) {
|
module.exports = function(req, callback) {
|
||||||
var userId = (req.url.path_list[1] || "").split(".")[0];
|
var userId = (req.url.path_list[1] || "").split(".")[0];
|
||||||
var size = parseInt(req.url.searchParams.get("size")) || config.avatars.default_size;
|
var size = parseInt(req.url.query.size) || config.avatars.default_size;
|
||||||
var def = req.url.searchParams.get("default");
|
var def = req.url.query.default;
|
||||||
var overlay = req.url.searchParams.has("overlay") || req.url.searchParams.has("helm");
|
var overlay = Object.prototype.hasOwnProperty.call(req.url.query, "overlay") || Object.prototype.hasOwnProperty.call(req.url.query, "helm");
|
||||||
|
|
||||||
// check for extra paths
|
// check for extra paths
|
||||||
if (req.url.path_list.length > 2) {
|
if (req.url.path_list.length > 2) {
|
||||||
@ -65,9 +67,6 @@ module.exports = function(req, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip dashes
|
|
||||||
userId = userId.replace(/-/g, "");
|
|
||||||
|
|
||||||
// Prevent app from crashing/freezing
|
// Prevent app from crashing/freezing
|
||||||
if (size < config.avatars.min_size || size > config.avatars.max_size) {
|
if (size < config.avatars.min_size || size > config.avatars.max_size) {
|
||||||
// "Unprocessable Entity", valid request, but semantically erroneous:
|
// "Unprocessable Entity", valid request, but semantically erroneous:
|
||||||
@ -85,6 +84,9 @@ module.exports = function(req, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// strip dashes
|
||||||
|
userId = userId.replace(/-/g, "");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
helpers.get_avatar(req.id, userId, overlay, size, function(err, status, image, hash) {
|
helpers.get_avatar(req.id, userId, overlay, size, function(err, status, image, hash) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ var cache = require("../cache");
|
|||||||
// GET cape request
|
// GET cape request
|
||||||
module.exports = function(req, callback) {
|
module.exports = function(req, callback) {
|
||||||
var userId = (req.url.path_list[1] || "").split(".")[0];
|
var userId = (req.url.path_list[1] || "").split(".")[0];
|
||||||
var def = req.url.searchParams.get('default');
|
var def = req.url.query.default;
|
||||||
var rid = req.id;
|
var rid = req.id;
|
||||||
|
|
||||||
// check for extra paths
|
// check for extra paths
|
||||||
@ -17,8 +17,6 @@ module.exports = function(req, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip dashes
|
|
||||||
userId = userId.replace(/-/g, "");
|
|
||||||
if (!helpers.id_valid(userId)) {
|
if (!helpers.id_valid(userId)) {
|
||||||
callback({
|
callback({
|
||||||
status: -2,
|
status: -2,
|
||||||
@ -27,6 +25,9 @@ module.exports = function(req, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// strip dashes
|
||||||
|
userId = userId.replace(/-/g, "");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
helpers.get_cape(rid, userId, function(err, hash, status, image) {
|
helpers.get_cape(rid, userId, function(err, hash, status, image) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@ -7,7 +7,6 @@ var ejs = require("ejs");
|
|||||||
var str;
|
var str;
|
||||||
var index;
|
var index;
|
||||||
|
|
||||||
// pre-compile the index page
|
|
||||||
function compile() {
|
function compile() {
|
||||||
logging.log("Compiling index page");
|
logging.log("Compiling index page");
|
||||||
str = read(path.join(__dirname, "..", "views", "index.html.ejs"), "utf-8");
|
str = read(path.join(__dirname, "..", "views", "index.html.ejs"), "utf-8");
|
||||||
|
|||||||
@ -17,10 +17,12 @@ function handle_default(rid, scale, overlay, body, img_status, userId, size, def
|
|||||||
if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
|
if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
|
||||||
if (helpers.id_valid(def)) {
|
if (helpers.id_valid(def)) {
|
||||||
// clean up the old URL to match new image
|
// clean up the old URL to match new image
|
||||||
req.url.searchParams.delete('default');
|
var parsed = req.url;
|
||||||
req.url.path_list[2] = def;
|
delete parsed.query.default;
|
||||||
req.url.pathname = req.url.path_list.join('/');
|
delete parsed.search;
|
||||||
var newUrl = req.url.toString();
|
parsed.path_list[2] = def;
|
||||||
|
parsed.pathname = "/" + parsed.path_list.join("/");
|
||||||
|
var newUrl = url.format(parsed);
|
||||||
callback({
|
callback({
|
||||||
status: img_status,
|
status: img_status,
|
||||||
redirect: newUrl,
|
redirect: newUrl,
|
||||||
@ -60,9 +62,9 @@ module.exports = function(req, callback) {
|
|||||||
var rid = req.id;
|
var rid = req.id;
|
||||||
var body = raw_type === "body";
|
var body = raw_type === "body";
|
||||||
var userId = (req.url.path_list[2] || "").split(".")[0];
|
var userId = (req.url.path_list[2] || "").split(".")[0];
|
||||||
var def = req.url.searchParams.get("default");
|
var def = req.url.query.default;
|
||||||
var scale = parseInt(req.url.searchParams.get("scale")) || config.renders.default_scale;
|
var scale = parseInt(req.url.query.scale) || config.renders.default_scale;
|
||||||
var overlay = req.url.searchParams.has("overlay") || req.url.searchParams.has("helm");
|
var overlay = Object.prototype.hasOwnProperty.call(req.url.query, "overlay") || Object.prototype.hasOwnProperty.call(req.url.query, "helm");
|
||||||
|
|
||||||
// check for extra paths
|
// check for extra paths
|
||||||
if (req.url.path_list.length > 3) {
|
if (req.url.path_list.length > 3) {
|
||||||
@ -83,9 +85,6 @@ module.exports = function(req, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip dashes
|
|
||||||
userId = userId.replace(/-/g, "");
|
|
||||||
|
|
||||||
if (scale < config.renders.min_scale || scale > config.renders.max_scale) {
|
if (scale < config.renders.min_scale || scale > config.renders.max_scale) {
|
||||||
callback({
|
callback({
|
||||||
status: -2,
|
status: -2,
|
||||||
@ -100,6 +99,9 @@ module.exports = function(req, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// strip dashes
|
||||||
|
userId = userId.replace(/-/g, "");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
helpers.get_render(rid, userId, scale, overlay, body, function(err, status, hash, image) {
|
helpers.get_render(rid, userId, scale, overlay, body, function(err, status, hash, image) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@ -14,10 +14,12 @@ function handle_default(img_status, userId, def, req, err, callback) {
|
|||||||
if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
|
if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
|
||||||
if (helpers.id_valid(def)) {
|
if (helpers.id_valid(def)) {
|
||||||
// clean up the old URL to match new image
|
// clean up the old URL to match new image
|
||||||
req.url.searchParams.delete('default');
|
var parsed = req.url;
|
||||||
req.url.path_list[1] = def;
|
delete parsed.query.default;
|
||||||
req.url.pathname = req.url.path_list.join('/');
|
delete parsed.search;
|
||||||
var newUrl = req.url.toString();
|
parsed.path_list[1] = def;
|
||||||
|
parsed.pathname = "/" + parsed.path_list.join("/");
|
||||||
|
var newUrl = url.format(parsed);
|
||||||
callback({
|
callback({
|
||||||
status: img_status,
|
status: img_status,
|
||||||
redirect: newUrl,
|
redirect: newUrl,
|
||||||
@ -60,7 +62,7 @@ function handle_default(img_status, userId, def, req, err, callback) {
|
|||||||
// GET skin request
|
// GET skin request
|
||||||
module.exports = function(req, callback) {
|
module.exports = function(req, callback) {
|
||||||
var userId = (req.url.path_list[1] || "").split(".")[0];
|
var userId = (req.url.path_list[1] || "").split(".")[0];
|
||||||
var def = req.url.searchParams.get("default");
|
var def = req.url.query.default;
|
||||||
var rid = req.id;
|
var rid = req.id;
|
||||||
|
|
||||||
// check for extra paths
|
// check for extra paths
|
||||||
@ -73,8 +75,6 @@ module.exports = function(req, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip dashes
|
|
||||||
userId = userId.replace(/-/g, "");
|
|
||||||
if (!helpers.id_valid(userId)) {
|
if (!helpers.id_valid(userId)) {
|
||||||
callback({
|
callback({
|
||||||
status: -2,
|
status: -2,
|
||||||
@ -83,6 +83,9 @@ module.exports = function(req, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// strip dashes
|
||||||
|
userId = userId.replace(/-/g, "");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
helpers.get_skin(rid, userId, function(err, hash, status, image, slim) {
|
helpers.get_skin(rid, userId, function(err, hash, status, image, slim) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
var querystring = require("querystring");
|
var querystring = require("querystring");
|
||||||
var response = require("./response");
|
var response = require("./response");
|
||||||
var helpers = require("./helpers.js");
|
|
||||||
var toobusy = require("toobusy-js");
|
var toobusy = require("toobusy-js");
|
||||||
var logging = require("./logging");
|
var logging = require("./logging");
|
||||||
var config = require("../config");
|
var config = require("../config");
|
||||||
@ -22,33 +21,24 @@ var routes = {
|
|||||||
|
|
||||||
// serves assets from lib/public
|
// serves assets from lib/public
|
||||||
function asset_request(req, callback) {
|
function asset_request(req, callback) {
|
||||||
const filename = path.join(__dirname, "public", ...req.url.path_list);
|
var filename = path.join(__dirname, "public", req.url.path_list.join("/"));
|
||||||
const relative = path.relative(path.join(__dirname, "public"), filename);
|
fs.access(filename, function(fs_err) {
|
||||||
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
|
if (!fs_err) {
|
||||||
fs.access(filename, function(fs_err) {
|
fs.readFile(filename, function(err, data) {
|
||||||
if (!fs_err) {
|
|
||||||
fs.readFile(filename, function(err, data) {
|
|
||||||
callback({
|
|
||||||
body: data,
|
|
||||||
type: mime.getType(filename),
|
|
||||||
err: err,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback({
|
callback({
|
||||||
body: "Not found",
|
body: data,
|
||||||
status: -2,
|
type: mime.getType(filename),
|
||||||
code: 404,
|
err: err,
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
callback({
|
||||||
callback({
|
body: "Not found",
|
||||||
body: "Forbidden",
|
status: -2,
|
||||||
status: -2,
|
code: 404,
|
||||||
code: 403,
|
});
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// generates a 12 character random string
|
// generates a 12 character random string
|
||||||
@ -56,18 +46,26 @@ function request_id() {
|
|||||||
return Math.random().toString(36).substring(2, 14);
|
return Math.random().toString(36).substring(2, 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
// splits decoded URL path into an Array
|
// splits a URL path into an Array
|
||||||
|
// the path is resolved and decoded
|
||||||
function path_list(pathname) {
|
function path_list(pathname) {
|
||||||
|
// remove double and trailing slashes
|
||||||
|
pathname = pathname.replace(/\/\/+/g, "/").replace(/(.)\/$/, "$1");
|
||||||
var list = pathname.split("/");
|
var list = pathname.split("/");
|
||||||
list.shift();
|
list.shift();
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
// URL decode
|
||||||
|
list[i] = querystring.unescape(list[i]);
|
||||||
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handles the +req+ by routing to the request to the appropriate module
|
// handles the +req+ by routing to the request to the appropriate module
|
||||||
function requestHandler(req, res) {
|
function requestHandler(req, res) {
|
||||||
req.url = new URL(decodeURI(req.url), 'http://' + req.headers.host);
|
req.url = url.parse(req.url, true);
|
||||||
req.url.pathname = path.resolve('/', req.url.pathname);
|
req.url.query = req.url.query || {};
|
||||||
req.url.path_list = path_list(req.url.pathname);
|
req.url.path_list = path_list(req.url.pathname);
|
||||||
|
|
||||||
req.id = request_id();
|
req.id = request_id();
|
||||||
req.start = Date.now();
|
req.start = Date.now();
|
||||||
|
|
||||||
@ -137,10 +135,9 @@ function requestHandler(req, res) {
|
|||||||
|
|
||||||
var exp = {};
|
var exp = {};
|
||||||
|
|
||||||
// Start the server
|
|
||||||
exp.boot = function(callback) {
|
exp.boot = function(callback) {
|
||||||
var port = config.server.port;
|
var port = process.env.PORT || 3000;
|
||||||
var bind_ip = config.server.bind;
|
var bind_ip = process.env.BIND || "0.0.0.0";
|
||||||
server = http.createServer(requestHandler).listen(port, bind_ip, function() {
|
server = http.createServer(requestHandler).listen(port, bind_ip, function() {
|
||||||
logging.log("Server running on http://" + bind_ip + ":" + port + "/");
|
logging.log("Server running on http://" + bind_ip + ":" + port + "/");
|
||||||
if (callback) {
|
if (callback) {
|
||||||
@ -152,7 +149,7 @@ exp.boot = function(callback) {
|
|||||||
// wait for established connections to finish (30s max),
|
// wait for established connections to finish (30s max),
|
||||||
// then exit
|
// then exit
|
||||||
process.on("SIGTERM", function() {
|
process.on("SIGTERM", function() {
|
||||||
logging.warn("Got SIGTERM, no longer accepting new connections!");
|
logging.warn("Got SIGTERM, no longer accepting connections!");
|
||||||
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
logging.error("Dropping connections after 30s. Force quit.");
|
logging.error("Dropping connections after 30s. Force quit.");
|
||||||
@ -166,9 +163,7 @@ exp.boot = function(callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close the server
|
|
||||||
exp.close = function(callback) {
|
exp.close = function(callback) {
|
||||||
helpers.stoplog();
|
|
||||||
server.close(callback);
|
server.close(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>Crafatar – A blazing fast API for Minecraft faces!</title>
|
<title>Crafatar – A blazing fast API for Minecraft faces!</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" sizes="16x16" type="image/png" href="/favicon.png">
|
||||||
<link rel="stylesheet" href="/stylesheets/bootstrap.min.css">
|
<link rel="stylesheet" href="/stylesheets/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="/stylesheets/style.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!">
|
<meta name="description" content="A blazing fast API for Minecraft faces with support for avatars, skins, and 3D renders!">
|
||||||
@ -76,7 +76,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
|
|
||||||
<section id="avatars">
|
<section id="avatars">
|
||||||
@ -211,12 +210,10 @@
|
|||||||
<h3><a href="#meta-caching">About Caching</a></h3>
|
<h3><a href="#meta-caching">About Caching</a></h3>
|
||||||
<p>
|
<p>
|
||||||
Crafatar checks for skin updates every <%= config.caching.local / 60 %> minutes.<br>
|
Crafatar checks for skin updates every <%= config.caching.local / 60 %> minutes.<br>
|
||||||
Images are also cached in your browser for <%= config.caching.browser / 60 %> minutes unless you clear your browser cache.
|
Images are cached in your browser for <%= config.caching.browser / 60 %> minutes until a new request to Crafatar is made.<br>
|
||||||
<% if (config.caching.cloudflare) { %>
|
In addition, <span title="A CDN and caching proxy">CloudFlare</span> caches up to 2 hours on a per-url basis.
|
||||||
<br>In addition, <span title="A CDN and caching proxy">Cloudflare</span> may cache images as long as your browser would.
|
|
||||||
<% } %>
|
|
||||||
</p>
|
</p>
|
||||||
<p>After changing your Minecraft skin, you can try clearing your browser cache to see the change faster.</p>
|
<p>When you changed your skin you can try clearing your browser cache to see the change faster.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="meta-cors">
|
<section id="meta-cors">
|
||||||
@ -227,30 +224,11 @@
|
|||||||
<section id="meta-http-headers">
|
<section id="meta-http-headers">
|
||||||
<h3><a href="#meta-http-headers">HTTP Headers</a></h3>
|
<h3><a href="#meta-http-headers">HTTP Headers</a></h3>
|
||||||
<p>
|
<p>
|
||||||
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.
|
Responses come with some custom HTTP headers, useful for debugging.<br>
|
||||||
<% if (config.caching.cloudflare) { %>
|
Please note that these headers may be cached by <span title="A CDN and caching proxy">CloudFlare</span>.
|
||||||
<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>
|
</p>
|
||||||
|
|
||||||
<ul>
|
<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>
|
<li>
|
||||||
<b>X-Storage-Type</b>: Details about how the requested image was stored on the server
|
<b>X-Storage-Type</b>: Details about how the requested image was stored on the server
|
||||||
<ul>
|
<ul>
|
||||||
@ -259,56 +237,52 @@
|
|||||||
<li><b>checked</b>: Requested skin details, skin cached. (1 external request)<br>
|
<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>
|
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>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.</li>
|
<li><b>server error</b>: This can happen, for example, when Mojang's servers are down.<br>
|
||||||
<li><b>server error;cached</b>: Same as server error, but a cached skin was available.</li>
|
If possible, a cached image is served instead.</li>
|
||||||
<li><b>user error</b>: You have done something wrong, such as requesting a malformed uuid.<br>
|
<li><b>user error</b>: You have done something wrong, such as requesting a malformed uuid.<br>
|
||||||
Check the response body for details.</li>
|
Check the response body for details.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<li>
|
<li>
|
||||||
<b>X-Request-ID</b>: The internal ID assigned to this request.<br>
|
<b>X-Request-ID</b>: The internal ID assigned to this request.<br>
|
||||||
If you think something is wrong with your request, please contact us and provide this ID.
|
If you think something is wrong with your request, please <a href="#contact">contact us</a> and provide this ID.
|
||||||
<li>
|
|
||||||
<b>Response-Time</b>: How long it took Crafatar to answer the request, in ms.
|
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<h4>Popular Crafatar users</h4>
|
<h4>Popular Crafatar users</h4>
|
||||||
<div class="list-group">
|
<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://hypixel.net" target="_blank" class="list-group-item">Hypixel</a>
|
||||||
<a rel="nofollow" href="https://mineplex.com" target="_blank" class="list-group-item">Mineplex</a>
|
<a rel="nofollow" href="https://www.minecraft-index.com" target="_blank" class="list-group-item">MC Index</a>
|
||||||
<a rel="nofollow" href="https://hivemc.com" target="_blank" class="list-group-item">The Hive</a>
|
<a rel="nofollow" href="https://www.minehq.com/hcteams/leaderboards" target="_blank" class="list-group-item">MineHQ</a>
|
||||||
<a rel="nofollow" href="https://www.technicpack.net" target="_blank" class="list-group-item">Technic Pack</a>
|
<a rel="nofollow" href="https://shotbow.net" target="_blank" class="list-group-item">Shotbow</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 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>
|
<a href="https://github.com/crafatar/crafatar/wiki/Who-uses-crafatar%3F" target="_blank" class="list-group-item">and many more…</a>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<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>
|
||||||
<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>
|
<hr>
|
||||||
<h4>Crafatar Tools & Plugins</h4>
|
<h4>Crafatar Tools & Plugins</h4>
|
||||||
<div class="list-group">
|
<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://xenforo.com/community/resources/associationmc.3232/" target="_blank" class="list-group-item">AssociationMc <i>(XenForo)</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://github.com/yeahwhat-mc/discourse-yeahwhat" target="_blank" class="list-group-item">Minecraft Heads <i>(Discourse)</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 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 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>
|
<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>
|
</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) { %>
|
<% if (config.sponsor.sidebar) { %>
|
||||||
<hr>
|
|
||||||
<%- config.sponsor.sidebar %>
|
<%- config.sponsor.sidebar %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
@ -321,4 +295,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
2430
package-lock.json
generated
2430
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@ -1,25 +1,52 @@
|
|||||||
{
|
{
|
||||||
"name": "crafatar",
|
"name": "crafatar",
|
||||||
"version": "2.1.5",
|
"version": "2.1.1",
|
||||||
"private": true,
|
"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": {
|
"scripts": {
|
||||||
|
"postinstall": "cp 'config.example.js' 'config.js'",
|
||||||
"start": "node www.js",
|
"start": "node www.js",
|
||||||
"test": "mocha"
|
"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"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.16.1"
|
"node": "12.16.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@randy.tarampi/lwip": "^1.3.1",
|
|
||||||
"canvas": "^2.6.1",
|
"canvas": "^2.6.1",
|
||||||
"crc": "^3.8.0",
|
"crc": "^3.8.0",
|
||||||
"ejs": "^3.1.5",
|
"ejs": "^3.0.1",
|
||||||
"mime": "^2.4.6",
|
"@randy.tarampi/lwip": "^1.1.0",
|
||||||
|
"mime": "^2.4.4",
|
||||||
"redis": "^3.0.2",
|
"redis": "^3.0.2",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"toobusy-js": "^0.5.1"
|
"toobusy-js": "^0.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mocha": "^7.2.0"
|
"coveralls": "^3.0.11",
|
||||||
|
"istanbul": "^0.4.5",
|
||||||
|
"mocha": "^7.1.1",
|
||||||
|
"mocha-lcov-reporter": "^1.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
61
test/bulk.sh
61
test/bulk.sh
@ -1,55 +1,36 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
hostname="crafatar.com"
|
|
||||||
async="true"
|
async="true"
|
||||||
random="false"
|
|
||||||
interval="0.1"
|
interval="0.1"
|
||||||
|
if [ "$1" = "-s" ]; then
|
||||||
usage() {
|
async=""
|
||||||
echo "Usage: $0 [-s | -r | -i <interval> | -h <hostname>]... <host uri>" >&2
|
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
|
exit 1
|
||||||
}
|
fi
|
||||||
|
|
||||||
get_ids() {
|
# insert newline after uuids
|
||||||
local shuf
|
ids="$(cat 'uuids.txt')"
|
||||||
if [ "$random" = "true" ]; then
|
# `brew install coreutils` on OS X
|
||||||
while true; do uuid -v 4; done
|
ids="$(shuf <<< "$ids" 2>/dev/null || gshuf <<< "$ids")"
|
||||||
else
|
|
||||||
# `brew install coreutils` on OS X for gshuf
|
|
||||||
shuf=$(command -v shuf gshuf)
|
|
||||||
# randomize ids
|
|
||||||
$shuf < uuids.txt
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
bulk() {
|
bulk() {
|
||||||
trap return INT # return from this function on Ctrl+C
|
trap return INT
|
||||||
get_ids | while read id; do
|
echo "$ids" | while read id; do
|
||||||
if [ "$async" = "false" ]; then
|
if [ -z "$async" ]; then
|
||||||
curl -H "Host: $hostname" -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?overlay"
|
curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?overlay"
|
||||||
else
|
else
|
||||||
curl -H "Host: $hostname" -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?overlay" &
|
curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" -- "$host/avatars/$id?overlay" &
|
||||||
sleep "$interval"
|
sleep "$interval"
|
||||||
fi
|
fi
|
||||||
done
|
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
|
time bulk
|
||||||
117
test/test.js
117
test/test.js
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
// no spam
|
// no spam
|
||||||
var logging = require("../lib/logging");
|
var logging = require("../lib/logging");
|
||||||
if (process.env.VERBOSE_TEST !== "true") {
|
if (process.env.VERBOSE_TEST !== "true" && process.env.TRAVIS !== "true") {
|
||||||
logging.log = logging.debug = logging.warn = logging.error = function() {};
|
logging.log = logging.debug = logging.warn = logging.error = function() {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,8 +88,8 @@ describe("Crafatar", function() {
|
|||||||
assert.strictEqual(helpers.id_valid("1DCEF164FF0A47F2B9A691385C774EE7"), true);
|
assert.strictEqual(helpers.id_valid("1DCEF164FF0A47F2B9A691385C774EE7"), true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
it("dashed uuid is not valid", function(done) {
|
it("dashed uuid is valid", function(done) {
|
||||||
assert.strictEqual(helpers.id_valid("0098cb60-fa8e-427c-b299-793cbd302c9a"), false);
|
assert.strictEqual(helpers.id_valid("0098cb60-fa8e-427c-b299-793cbd302c9a"), true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
it("username is invalid", function(done) {
|
it("username is invalid", function(done) {
|
||||||
@ -158,7 +158,7 @@ describe("Crafatar", function() {
|
|||||||
it("should time out on skin download", function(done) {
|
it("should time out on skin download", function(done) {
|
||||||
var original_timeout = config.http_timeout;
|
var original_timeout = config.http_timeout;
|
||||||
config.server.http_timeout = 1;
|
config.server.http_timeout = 1;
|
||||||
networking.get_from(rid(), config.endpoints.textures_url + "477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", function(body, res, error) {
|
networking.get_from(rid(), "http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", function(body, res, error) {
|
||||||
assert.notStrictEqual(["ETIMEDOUT", "ESOCKETTIMEDOUT"].indexOf(error.code), -1);
|
assert.notStrictEqual(["ETIMEDOUT", "ESOCKETTIMEDOUT"].indexOf(error.code), -1);
|
||||||
config.server.http_timeout = original_timeout;
|
config.server.http_timeout = original_timeout;
|
||||||
done();
|
done();
|
||||||
@ -166,7 +166,7 @@ describe("Crafatar", function() {
|
|||||||
});
|
});
|
||||||
it("should not find the skin", function(done) {
|
it("should not find the skin", function(done) {
|
||||||
assert.doesNotThrow(function() {
|
assert.doesNotThrow(function() {
|
||||||
networking.get_from(rid(), config.endpoints.textures_url + "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
|
assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -258,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
|
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) {
|
request.get(url, function(error, res, body) {
|
||||||
assert.ifError(error);
|
assert.ifError(error);
|
||||||
assert.strictEqual(res.statusCode, 200);
|
assert.strictEqual(res.statusCode, 201);
|
||||||
assert_headers(res);
|
assert_headers(res);
|
||||||
assert(res.headers.etag);
|
assert(res.headers.etag);
|
||||||
assert.strictEqual(res.headers["content-type"], "image/png");
|
assert.strictEqual(res.headers["content-type"], "image/png");
|
||||||
@ -281,7 +281,7 @@ describe("Crafatar", function() {
|
|||||||
function req() {
|
function req() {
|
||||||
request.get(url, function(error, res, body) {
|
request.get(url, function(error, res, body) {
|
||||||
assert.ifError(error);
|
assert.ifError(error);
|
||||||
assert.strictEqual(res.statusCode, 200);
|
assert.strictEqual(res.statusCode === 201 || res.statusCode === 200, true);
|
||||||
assert_headers(res);
|
assert_headers(res);
|
||||||
assert(res.headers.etag);
|
assert(res.headers.etag);
|
||||||
assert.strictEqual(res.headers["content-type"], "image/png");
|
assert.strictEqual(res.headers["content-type"], "image/png");
|
||||||
@ -298,24 +298,20 @@ describe("Crafatar", function() {
|
|||||||
var server_tests = {
|
var server_tests = {
|
||||||
"avatar with existing uuid": {
|
"avatar with existing uuid": {
|
||||||
url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16",
|
url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16",
|
||||||
crc32: [4264176600],
|
crc32: [3337292777],
|
||||||
},
|
|
||||||
"avatar with existing dashed uuid": {
|
|
||||||
url: "http://localhost:3000/avatars/853c80ef-3c37-49fd-aa49938b674adae6?size=16",
|
|
||||||
crc32: [4264176600],
|
|
||||||
},
|
},
|
||||||
"avatar with non-existent uuid": {
|
"avatar with non-existent uuid": {
|
||||||
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16",
|
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16",
|
||||||
crc32: [3348154329],
|
crc32: [2416827277, 1243826040],
|
||||||
},
|
},
|
||||||
"avatar with non-existent uuid defaulting to mhf_alex": {
|
"avatar with non-existent uuid defaulting to mhf_alex": {
|
||||||
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=mhf_alex",
|
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=mhf_alex",
|
||||||
crc32: [73899130],
|
crc32: [862751081, 809395677],
|
||||||
},
|
},
|
||||||
"avatar with non-existent uuid defaulting to uuid": {
|
"avatar with non-existent uuid defaulting to uuid": {
|
||||||
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6",
|
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6",
|
||||||
crc32: [0],
|
crc32: [0],
|
||||||
redirect: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16",
|
redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16",
|
||||||
},
|
},
|
||||||
"avatar with non-existent uuid defaulting to url": {
|
"avatar with non-existent uuid defaulting to url": {
|
||||||
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
|
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
|
||||||
@ -324,20 +320,20 @@ describe("Crafatar", function() {
|
|||||||
},
|
},
|
||||||
"overlay avatar with existing uuid": {
|
"overlay avatar with existing uuid": {
|
||||||
url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&overlay",
|
url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&overlay",
|
||||||
crc32: [575355728],
|
crc32: [1710265722],
|
||||||
},
|
},
|
||||||
"overlay avatar with non-existent uuid": {
|
"overlay avatar with non-existent uuid": {
|
||||||
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay",
|
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay",
|
||||||
crc32: [3348154329],
|
crc32: [2416827277, 1243826040],
|
||||||
},
|
},
|
||||||
"overlay avatar with non-existent uuid defaulting to mhf_alex": {
|
"overlay avatar with non-existent uuid defaulting to mhf_alex": {
|
||||||
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay&default=mhf_alex",
|
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay&default=mhf_alex",
|
||||||
crc32: [73899130],
|
crc32: [862751081, 809395677],
|
||||||
},
|
},
|
||||||
"overlay 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",
|
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6",
|
||||||
crc32: [0],
|
crc32: [0],
|
||||||
redirect: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16",
|
redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16",
|
||||||
},
|
},
|
||||||
"overlay avatar with non-existent uuid defaulting to url": {
|
"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",
|
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
|
||||||
@ -346,7 +342,7 @@ describe("Crafatar", function() {
|
|||||||
},
|
},
|
||||||
"cape with existing uuid": {
|
"cape with existing uuid": {
|
||||||
url: "http://localhost:3000/capes/853c80ef3c3749fdaa49938b674adae6",
|
url: "http://localhost:3000/capes/853c80ef3c3749fdaa49938b674adae6",
|
||||||
crc32: [985789174, 2099310578],
|
crc32: [2556702429],
|
||||||
},
|
},
|
||||||
"cape with non-existent uuid": {
|
"cape with non-existent uuid": {
|
||||||
url: "http://localhost:3000/capes/00000000000000000000000000000000",
|
url: "http://localhost:3000/capes/00000000000000000000000000000000",
|
||||||
@ -359,20 +355,20 @@ describe("Crafatar", function() {
|
|||||||
},
|
},
|
||||||
"skin with existing uuid": {
|
"skin with existing uuid": {
|
||||||
url: "http://localhost:3000/skins/853c80ef3c3749fdaa49938b674adae6",
|
url: "http://localhost:3000/skins/853c80ef3c3749fdaa49938b674adae6",
|
||||||
crc32: [1759176487],
|
crc32: [26500336],
|
||||||
},
|
},
|
||||||
"skin with non-existent uuid": {
|
"skin with non-existent uuid": {
|
||||||
url: "http://localhost:3000/skins/00000000000000000000000000000000",
|
url: "http://localhost:3000/skins/00000000000000000000000000000000",
|
||||||
crc32: [1853029228],
|
crc32: [981937087],
|
||||||
},
|
},
|
||||||
"skin with non-existent uuid defaulting to mhf_alex": {
|
"skin with non-existent uuid defaulting to mhf_alex": {
|
||||||
url: "http://localhost:3000/skins/00000000000000000000000000000000?default=mhf_alex",
|
url: "http://localhost:3000/skins/00000000000000000000000000000000?default=mhf_alex",
|
||||||
crc32: [427506205],
|
crc32: [2298915739],
|
||||||
},
|
},
|
||||||
"skin with non-existent uuid defaulting to uuid": {
|
"skin with non-existent uuid defaulting to uuid": {
|
||||||
url: "http://localhost:3000/skins/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6",
|
url: "http://localhost:3000/skins/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6",
|
||||||
crc32: [0],
|
crc32: [0],
|
||||||
redirect: "http://localhost:3000/skins/853c80ef3c3749fdaa49938b674adae6?size=16",
|
redirect: "/skins/853c80ef3c3749fdaa49938b674adae6?size=16",
|
||||||
},
|
},
|
||||||
"skin with non-existent uuid defaulting to url": {
|
"skin with non-existent uuid defaulting to url": {
|
||||||
url: "http://localhost:3000/skins/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com%2FCaseSensitive",
|
url: "http://localhost:3000/skins/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com%2FCaseSensitive",
|
||||||
@ -394,7 +390,7 @@ describe("Crafatar", function() {
|
|||||||
"head render with non-existent uuid defaulting to uuid": {
|
"head render with non-existent uuid defaulting to uuid": {
|
||||||
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=853c80ef3c3749fdaa49938b674adae6",
|
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=853c80ef3c3749fdaa49938b674adae6",
|
||||||
crc32: [0],
|
crc32: [0],
|
||||||
redirect: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2",
|
redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2",
|
||||||
},
|
},
|
||||||
"head render with non-existent uuid defaulting to url": {
|
"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",
|
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
|
||||||
@ -416,7 +412,7 @@ describe("Crafatar", function() {
|
|||||||
"overlay head with non-existent uuid defaulting to uuid": {
|
"overlay head with non-existent uuid defaulting to uuid": {
|
||||||
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6",
|
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6",
|
||||||
crc32: [0],
|
crc32: [0],
|
||||||
redirect: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay=",
|
redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay=",
|
||||||
},
|
},
|
||||||
"overlay head render with non-existent uuid defaulting to url": {
|
"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",
|
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
|
||||||
@ -425,7 +421,7 @@ describe("Crafatar", function() {
|
|||||||
},
|
},
|
||||||
"body render with existing uuid": {
|
"body render with existing uuid": {
|
||||||
url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2",
|
url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2",
|
||||||
crc32: [1144887125],
|
crc32: [2745192436],
|
||||||
},
|
},
|
||||||
"body render with non-existent uuid": {
|
"body render with non-existent uuid": {
|
||||||
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2",
|
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2",
|
||||||
@ -433,12 +429,12 @@ describe("Crafatar", function() {
|
|||||||
},
|
},
|
||||||
"body render with non-existent uuid defaulting to mhf_alex": {
|
"body render with non-existent uuid defaulting to mhf_alex": {
|
||||||
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=mhf_alex",
|
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=mhf_alex",
|
||||||
crc32: [4280894468],
|
crc32: [1255106465],
|
||||||
},
|
},
|
||||||
"body render with non-existent uuid defaulting to uuid": {
|
"body render with non-existent uuid defaulting to uuid": {
|
||||||
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=853c80ef3c3749fdaa49938b674adae6",
|
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=853c80ef3c3749fdaa49938b674adae6",
|
||||||
crc32: [0],
|
crc32: [0],
|
||||||
redirect: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2",
|
redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2",
|
||||||
},
|
},
|
||||||
"body render with non-existent uuid defaulting to url": {
|
"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",
|
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
|
||||||
@ -447,7 +443,7 @@ describe("Crafatar", function() {
|
|||||||
},
|
},
|
||||||
"overlay body render with existing uuid": {
|
"overlay body render with existing uuid": {
|
||||||
url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay",
|
url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay",
|
||||||
crc32: [1107696668],
|
crc32: [2441671793],
|
||||||
},
|
},
|
||||||
"overlay body render with non-existent uuid": {
|
"overlay body render with non-existent uuid": {
|
||||||
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay",
|
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay",
|
||||||
@ -455,7 +451,7 @@ describe("Crafatar", function() {
|
|||||||
},
|
},
|
||||||
"overlay body render with non-existent uuid defaulting to mhf_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",
|
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay&default=mhf_alex",
|
||||||
crc32: [4280894468],
|
crc32: [1255106465],
|
||||||
},
|
},
|
||||||
"overlay body render with non-existent uuid defaulting to url": {
|
"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",
|
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
|
||||||
@ -493,7 +489,7 @@ describe("Crafatar", function() {
|
|||||||
done();
|
done();
|
||||||
} else {
|
} else {
|
||||||
assert.strictEqual(res.headers["content-type"], "image/png");
|
assert.strictEqual(res.headers["content-type"], "image/png");
|
||||||
assert.strictEqual(res.statusCode, 200);
|
assert.strictEqual(res.statusCode, res.headers["x-storage-type"] === "downloaded" ? 201 : 200);
|
||||||
assert(res.headers.etag);
|
assert(res.headers.etag);
|
||||||
assert.strictEqual(res.headers.etag, '"' + hash + '"');
|
assert.strictEqual(res.headers.etag, '"' + hash + '"');
|
||||||
assert_cache(location.url, res.headers.etag, function() {
|
assert_cache(location.url, res.headers.etag, function() {
|
||||||
@ -539,7 +535,7 @@ describe("Crafatar", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return a 422 (invalid render type)", function(done) {
|
it("should return a 422 (invalid render type)", function(done) {
|
||||||
request.get("http://localhost:3000/renders/invalid/2d5aa9cdaeb049189930461fc9b91cc5", function(error, res, body) {
|
request.get("http://localhost:3000/renders/invalid/Jake_0", function(error, res, body) {
|
||||||
assert.ifError(error);
|
assert.ifError(error);
|
||||||
assert.strictEqual(res.statusCode, 422);
|
assert.strictEqual(res.statusCode, 422);
|
||||||
done();
|
done();
|
||||||
@ -568,30 +564,6 @@ describe("Crafatar", function() {
|
|||||||
});
|
});
|
||||||
}(loc));
|
}(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
|
// we have to make sure that we test both a 32x64 and 64x64 skin
|
||||||
@ -612,7 +584,7 @@ describe("Crafatar", function() {
|
|||||||
|
|
||||||
describe("Networking: Cape", function() {
|
describe("Networking: Cape", function() {
|
||||||
it("should not fail (guaranteed cape)", function(done) {
|
it("should not fail (guaranteed cape)", function(done) {
|
||||||
helpers.get_cape(rid(), "61699b2ed3274a019f1e0ea8c3f06bc6", function(err, hash, status, img) {
|
helpers.get_cape(rid(), "Dinnerbone", function(err, hash, status, img) {
|
||||||
assert.strictEqual(err, null);
|
assert.strictEqual(err, null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -621,13 +593,13 @@ describe("Crafatar", function() {
|
|||||||
before(function() {
|
before(function() {
|
||||||
cache.get_redis().flushall();
|
cache.get_redis().flushall();
|
||||||
});
|
});
|
||||||
helpers.get_cape(rid(), "61699b2ed3274a019f1e0ea8c3f06bc6", function(err, hash, status, img) {
|
helpers.get_cape(rid(), "Dinnerbone", function(err, hash, status, img) {
|
||||||
assert.strictEqual(err, null);
|
assert.strictEqual(err, null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("should not be found", function(done) {
|
it("should not be found", function(done) {
|
||||||
helpers.get_cape(rid(), "2d5aa9cdaeb049189930461fc9b91cc5", function(err, hash, status, img) {
|
helpers.get_cape(rid(), "Jake_0", function(err, hash, status, img) {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
assert.strictEqual(img, null);
|
assert.strictEqual(img, null);
|
||||||
done();
|
done();
|
||||||
@ -637,7 +609,7 @@ describe("Crafatar", function() {
|
|||||||
|
|
||||||
describe("Networking: Skin", function() {
|
describe("Networking: Skin", function() {
|
||||||
it("should not fail", function(done) {
|
it("should not fail", function(done) {
|
||||||
helpers.get_cape(rid(), "2d5aa9cdaeb049189930461fc9b91cc5", function(err, hash, status, img) {
|
helpers.get_cape(rid(), "Jake_0", function(err, hash, status, img) {
|
||||||
assert.strictEqual(err, null);
|
assert.strictEqual(err, null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -646,7 +618,7 @@ describe("Crafatar", function() {
|
|||||||
before(function() {
|
before(function() {
|
||||||
cache.get_redis().flushall();
|
cache.get_redis().flushall();
|
||||||
});
|
});
|
||||||
helpers.get_cape(rid(), "2d5aa9cdaeb049189930461fc9b91cc5", function(err, hash, status, img) {
|
helpers.get_cape(rid(), "Jake_0", function(err, hash, status, img) {
|
||||||
assert.strictEqual(err, null);
|
assert.strictEqual(err, null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -710,28 +682,15 @@ describe("Crafatar", function() {
|
|||||||
|
|
||||||
|
|
||||||
describe("Errors", function() {
|
describe("Errors", function() {
|
||||||
before(function() {
|
before(function() {
|
||||||
cache.get_redis().flushall();
|
cache.get_redis().flushall();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mojang has changed its rate limiting, so we no longer expect to hit the rate limit
|
it("uuid SHOULD be rate limited", function(done) {
|
||||||
// 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() {
|
||||||
networking.get_profile(rid(), uuid, function(err, profile) {
|
networking.get_profile(rid(), uuid, function(err, profile) {
|
||||||
assert.strictEqual(err.code, "RATELIMIT");
|
assert.strictEqual(err.toString(), "HTTP: 429");
|
||||||
config.server.sessions_rate_limit = original_rate_limit;
|
assert.strictEqual(profile, null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4424,4 +4424,4 @@ ffdd082bf54e415b943a8713f2885913
|
|||||||
ffe0be5f0cab4b3785f67974c23660bb
|
ffe0be5f0cab4b3785f67974c23660bb
|
||||||
ffe3d4c861354928b932794d85a30567
|
ffe3d4c861354928b932794d85a30567
|
||||||
ffe72a222ac9463d81d3ee5eafb7f68e
|
ffe72a222ac9463d81d3ee5eafb7f68e
|
||||||
fff854a189644f12b92764fdb4573f8b
|
fff854a189644f12b92764fdb4573f8b
|
||||||
3
www.js
3
www.js
@ -1,4 +1,3 @@
|
|||||||
var networking = require("./lib/networking");
|
|
||||||
var logging = require("./lib/logging");
|
var logging = require("./lib/logging");
|
||||||
var config = require("./config");
|
var config = require("./config");
|
||||||
|
|
||||||
@ -7,6 +6,4 @@ process.on("uncaughtException", function(err) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(networking.resetCounter, 1000);
|
|
||||||
|
|
||||||
require("./lib/server.js").boot();
|
require("./lib/server.js").boot();
|
||||||
Loading…
x
Reference in New Issue
Block a user