mirror of
https://github.com/elyby/chrly.git
synced 2025-05-31 14:11:51 +05:30
Compare commits
57 Commits
2.0.0-fina
...
3.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b51c1bd0c | ||
|
|
cbe940f8ec | ||
|
|
8693673a71 | ||
|
|
73205648d2 | ||
|
|
3d73cc9402 | ||
|
|
39f5ec5bee | ||
|
|
e652691b29 | ||
|
|
d3b4bee3b0 | ||
|
|
ae50e90ea7 | ||
|
|
c74151c558 | ||
|
|
445bd18fbc | ||
|
|
6a881a62e3 | ||
|
|
201a257d69 | ||
|
|
5d46094643 | ||
|
|
1694403c79 | ||
|
|
66c61dc3cd | ||
|
|
a0d940f8cd | ||
|
|
58a1c6ec33 | ||
|
|
34179ae1fe | ||
|
|
6a54af62aa | ||
|
|
e05c5f200c | ||
|
|
9c4930a0be | ||
|
|
aab7ba9517 | ||
|
|
dea674f52e | ||
|
|
e8a7008e11 | ||
|
|
9467911025 | ||
|
|
a9acfb954f | ||
|
|
98b787fa99 | ||
|
|
4bcd0495ed | ||
|
|
e03832b4e8 | ||
|
|
0d6ca356d1 | ||
|
|
2477433dc9 | ||
|
|
3e3ba296d5 | ||
|
|
e8bd90d8d9 | ||
|
|
408d411846 | ||
|
|
45007ba1c5 | ||
|
|
4bdab704a5 | ||
|
|
89ea6e5ee8 | ||
|
|
24438fdedf | ||
|
|
eeffd17ea9 | ||
|
|
8abb5f6bc5 | ||
|
|
58c05533f3 | ||
|
|
22f80576bd | ||
|
|
c03021403a | ||
|
|
64bf7deb79 | ||
|
|
c2d0cb93cb | ||
|
|
283f4e0e3f | ||
|
|
915c465224 | ||
|
|
4da7a566f7 | ||
|
|
e3f744ed10 | ||
|
|
2b8266b224 | ||
|
|
3d65529d2e | ||
|
|
c4cd95cddc | ||
|
|
87ca1191eb | ||
|
|
6a7cc9ae77 | ||
|
|
baa1cd3010 | ||
|
|
b38b78bd1e |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Игнорим данные, т.к. они не нужны для внутреннего содержимого этого контейнера
|
||||||
|
data
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,2 +1,6 @@
|
|||||||
|
# IDEA
|
||||||
/.idea
|
/.idea
|
||||||
/awstat
|
|
||||||
|
# Docker Compose file
|
||||||
|
docker-compose.yml
|
||||||
|
docker-compose.override.yml
|
||||||
|
|||||||
33
.gitlab-ci.yml
Normal file
33
.gitlab-ci.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
image: docker:latest
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- push
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- docker login -u gitlab-ci -p $CI_BUILD_TOKEN registry.ely.by
|
||||||
|
|
||||||
|
variables:
|
||||||
|
CONTAINER_IMAGE: registry.ely.by/elyby/skinsystem
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- export IMAGE_NAME="$CONTAINER_IMAGE:dev"
|
||||||
|
- docker build --pull -t $IMAGE_NAME .
|
||||||
|
- docker push $IMAGE_NAME
|
||||||
|
only:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
push_tags:
|
||||||
|
stage: push
|
||||||
|
variables:
|
||||||
|
GIT_STRATEGY: none
|
||||||
|
script:
|
||||||
|
- export IMAGE_NAME="$CONTAINER_IMAGE:$CI_BUILD_TAG"
|
||||||
|
- docker tag $CONTAINER_IMAGE:dev $CONTAINER_IMAGE:latest
|
||||||
|
- docker tag $CONTAINER_IMAGE:latest $IMAGE_NAME
|
||||||
|
- docker push $IMAGE_NAME
|
||||||
|
- docker push $CONTAINER_IMAGE:latest
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<IfModule mod_rewrite.c>
|
|
||||||
RewriteEngine on
|
|
||||||
RewriteRule ^$ public/ [L]
|
|
||||||
RewriteRule (.*) public/$1 [L]
|
|
||||||
</IfModule>
|
|
||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
FROM golang:1.7-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
RUN mkdir -p /go/src/elyby/minecraft-skinsystem \
|
||||||
|
/go/src/elyby/minecraft-skinsystem/data/capes \
|
||||||
|
&& ln -s /go/src/elyby/minecraft-skinsystem /go/src/app
|
||||||
|
|
||||||
|
WORKDIR /go/src/app
|
||||||
|
|
||||||
|
COPY ./minecraft-skinsystem.go /go/src/app/
|
||||||
|
COPY ./lib /go/src/app/lib
|
||||||
|
|
||||||
|
RUN go-wrapper download
|
||||||
|
RUN go-wrapper install
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
VOLUME ["/go/src/app"]
|
||||||
|
|
||||||
|
CMD ["go-wrapper", "run"]
|
||||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Это заготовка для нормального файла
|
||||||
|
|
||||||
|
Для настройки Dev-окружения нужно склонировать проект в удобное место,
|
||||||
|
за тем сделать символьную ссылку в свой GOPATH:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Выполнять, находясь внутри директории репозитория
|
||||||
|
mkdir -p $GOPATH/src/elyby
|
||||||
|
ln -s $PWD $GOPATH/src/elyby/minecraft-skinsystem
|
||||||
|
```
|
||||||
|
|
||||||
|
Или можно склонировать репозиторий сразу в нужную локацию:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone git@bitbucket.org:elyby/minecraft-skinsystem.git $GOPATH/src/elyby/minecraft-skinsystem
|
||||||
|
```
|
||||||
|
|
||||||
|
Нужно скопировать правильный docker-compose файл для желаемого окружения:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp docker-compose.dev.yml docker-compose.yml # dev env
|
||||||
|
cp docker-compose.prod.yml docker-compose.yml # prod env
|
||||||
|
```
|
||||||
|
|
||||||
|
И за тем всё это поднять:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Если нужно пересобрать весь контейнер, то выполняем следующее:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose stop app # Останавливаем конейтнер, если он ещё работает
|
||||||
|
docker-compose rm -f app # Удаляем конейтнер
|
||||||
|
docker-compose build app # Запускаем билд по новой
|
||||||
|
docker-compose up -d app # Поднимаем свежесобранный контейнер обратно
|
||||||
|
```
|
||||||
98
app.php
98
app.php
@@ -1,98 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
define('ENCODING', 'UTF-8');
|
|
||||||
|
|
||||||
$app->get('/skins/{nickname}', function ($nickname) use ($app) {
|
|
||||||
// $systemVersion = $app->request->get('version', 'int');
|
|
||||||
// $minecraftVersion = $app->request->get('minecraft_version', 'string');
|
|
||||||
|
|
||||||
// На всякий случай проверка на наличие .png для файла
|
|
||||||
if (strrpos($nickname, '.png') != -1) {
|
|
||||||
$nickname = explode('.', $nickname)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: восстановить функцию деградации скинов
|
|
||||||
|
|
||||||
$skin = Skins::findByNickname($nickname);
|
|
||||||
if (!$skin || $skin->skinId == 0) {
|
|
||||||
return $app->response->redirect('http://skins.minecraft.net/MinecraftSkins/' . $nickname . '.png', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $app->response->redirect($skin->url);
|
|
||||||
})->setName('skinSystem');
|
|
||||||
|
|
||||||
$app->get('/cloaks/{nickname}', function ($nickname) use ($app) {
|
|
||||||
// На всякий случай проверка на наличие .png для файла
|
|
||||||
if (strrpos($nickname, '.png') != -1) {
|
|
||||||
$nickname = explode('.', $nickname)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $app->response->redirect('http://skins.minecraft.net/MinecraftCloaks/'.$nickname.'.png');
|
|
||||||
});
|
|
||||||
|
|
||||||
$app->get('/textures/{nickname}', function($nickname) use ($app) {
|
|
||||||
$skin = Skins::findByNickname($nickname);
|
|
||||||
if ($skin && $skin->skinId != 0) {
|
|
||||||
$url = $skin->url;
|
|
||||||
$hash = $skin->hash;
|
|
||||||
} else {
|
|
||||||
$url = 'http://skins.minecraft.net/MinecraftSkins/'.$nickname.'.png';
|
|
||||||
$hash = md5('non-ely-' . mktime(date('H'), 0, 0) . '-' . $nickname);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: в authserver.ely.by есть готовый класс для работы с форматом текстур. Так что если мы его вынесем в
|
|
||||||
// common library, то нужно будет заменить его здесь
|
|
||||||
|
|
||||||
$textures = [
|
|
||||||
'SKIN' => [
|
|
||||||
'url' => $url,
|
|
||||||
'hash' => $hash,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$capePath = __DIR__ . '/cloaks/' . $nickname . '.png';
|
|
||||||
if (file_exists($capePath)) {
|
|
||||||
$textures['CAPE'] = [
|
|
||||||
'url' => '/cloaks/' . mb_convert_case($nickname, MB_CASE_LOWER) . '.png',
|
|
||||||
'hash' => md5_file($capePath),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($skin && $skin->isSlim) {
|
|
||||||
$textures['SKIN']['metadata']['model'] = 'slim';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $app->response->setContentType('application/json')->setJsonContent($textures);
|
|
||||||
});
|
|
||||||
|
|
||||||
$app->post('/system/setSkin', function() use ($app) {
|
|
||||||
$headers = getallheaders();
|
|
||||||
if (!array_key_exists('X-Ely-key', $headers) || $headers['X-Ely-key'] != '43fd2ce61b3f5704dfd729c1f2d6ffdb') {
|
|
||||||
return $app->response->setStatusCode(403, 'Forbidden')->setContent('Хорошая попытка, мерзкий хакер.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$request = $app->request;
|
|
||||||
$nickname = mb_convert_case($request->getPost('nickname', 'string'), MB_CASE_LOWER, ENCODING);
|
|
||||||
|
|
||||||
$skin = Skins::findByNickname($nickname);
|
|
||||||
if (!$skin) {
|
|
||||||
$skin = new Skins();
|
|
||||||
$skin->nickname = $nickname;
|
|
||||||
}
|
|
||||||
|
|
||||||
$skin->userId = (int) $request->getPost('userId', 'int');
|
|
||||||
$skin->skinId = (int) $request->getPost('skinId', 'int');
|
|
||||||
$skin->hash = $request->getPost('hash', 'string');
|
|
||||||
$skin->is1_8 = (bool) $request->getPost('is1_8', 'int');
|
|
||||||
$skin->isSlim = (bool) $request->getPost('isSlim', 'int');
|
|
||||||
$skin->url = $request->getPost('url', 'string');
|
|
||||||
|
|
||||||
return $app->response->setContent($skin->save() ? 'OK' : 'ERROR');
|
|
||||||
});
|
|
||||||
|
|
||||||
$app->notFound(function () use ($app) {
|
|
||||||
$app->response
|
|
||||||
->setStatusCode(404, 'Not Found')
|
|
||||||
->setContent('Not Found<br /> <a href="http://ely.by">Система скинов Ely.by</a>.')
|
|
||||||
->send();
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
return new \Phalcon\Config([
|
|
||||||
'mongo' => [
|
|
||||||
'host' => 'localhost',
|
|
||||||
'port' => 27017,
|
|
||||||
'username' => '',
|
|
||||||
'password' => '',
|
|
||||||
'dbname' => 'ely_skins',
|
|
||||||
],
|
|
||||||
'application' => [
|
|
||||||
'modelsDir' => __DIR__ . '/../models/',
|
|
||||||
'baseUri' => '/',
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @var \Phalcon\Config $config
|
|
||||||
*/
|
|
||||||
|
|
||||||
$loader = new \Phalcon\Loader();
|
|
||||||
|
|
||||||
$loader->registerDirs(array(
|
|
||||||
$config->application->modelsDir
|
|
||||||
));
|
|
||||||
|
|
||||||
$loader->register();
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @var \Phalcon\Config $config
|
|
||||||
*/
|
|
||||||
|
|
||||||
use Phalcon\Mvc\Collection\Manager;
|
|
||||||
use Phalcon\Mvc\View;
|
|
||||||
use Phalcon\Mvc\Url as UrlResolver;
|
|
||||||
use Phalcon\DI\FactoryDefault;
|
|
||||||
|
|
||||||
$di = new FactoryDefault();
|
|
||||||
|
|
||||||
$di->set('view', function () {
|
|
||||||
$view = new View();
|
|
||||||
$view->disable();
|
|
||||||
|
|
||||||
return $view;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL component is used to generate all kind of urls in the application
|
|
||||||
*/
|
|
||||||
$di->set('url', function () use ($config) {
|
|
||||||
$url = new UrlResolver();
|
|
||||||
$url->setBaseUri($config->application->baseUri);
|
|
||||||
|
|
||||||
return $url;
|
|
||||||
});
|
|
||||||
|
|
||||||
$di->set('mongo', function() use ($config) {
|
|
||||||
/** @var StdClass $mongoConfig */
|
|
||||||
$mongoConfig = $config->mongo;
|
|
||||||
$connectionString = 'mongodb://';
|
|
||||||
if ($mongoConfig->username && $mongoConfig->password) {
|
|
||||||
$connectionString .= "{$mongoConfig->username}:{$mongoConfig->password}@";
|
|
||||||
}
|
|
||||||
|
|
||||||
$connectionString .= $mongoConfig->host . ':' . $mongoConfig->port;
|
|
||||||
$mongo = new MongoClient($connectionString);
|
|
||||||
|
|
||||||
return $mongo->selectDb($mongoConfig->dbname);
|
|
||||||
});
|
|
||||||
|
|
||||||
$di->setShared('collectionManager', function() {
|
|
||||||
return new Manager();
|
|
||||||
});
|
|
||||||
2
data/redis/.gitignore
vendored
Normal file
2
data/redis/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
2
data/statsd/.gitignore
vendored
Normal file
2
data/statsd/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
20
docker-compose.base.yml
Normal file
20
docker-compose.base.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:3.0-alpine
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: rabbitmq:3.6
|
||||||
|
environment:
|
||||||
|
RABBITMQ_DEFAULT_USER: "ely-skinsystem-app"
|
||||||
|
RABBITMQ_DEFAULT_PASS: "ely-skinsystem-app-password"
|
||||||
|
RABBITMQ_DEFAULT_VHOST: "/ely"
|
||||||
|
|
||||||
|
statsd:
|
||||||
|
image: hopsoft/graphite-statsd
|
||||||
|
volumes:
|
||||||
|
- ./data/statsd:/opt/graphite/storage
|
||||||
|
- ./data/graphite-config:/opt/graphite/conf
|
||||||
|
- ./data/statsd-config/config.json:/opt/statsd/config.js
|
||||||
35
docker-compose.dev.yml
Normal file
35
docker-compose.dev.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
image: registry.ely.by/elyby/skinsystem:latest
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./:/go/src/app
|
||||||
|
command: ["go", "run", "minecraft-skinsystem.go"]
|
||||||
|
links:
|
||||||
|
- redis
|
||||||
|
- rabbitmq
|
||||||
|
- statsd
|
||||||
|
environment:
|
||||||
|
ACCOUNTS_API_ID: ""
|
||||||
|
ACCOUNTS_API_SECRET: ""
|
||||||
|
STATSD_ADDR: ""
|
||||||
|
|
||||||
|
redis:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: redis
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: rabbitmq
|
||||||
|
|
||||||
|
statsd:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: statsd
|
||||||
|
ports:
|
||||||
|
- "8123:80"
|
||||||
26
docker-compose.prod.yml
Normal file
26
docker-compose.prod.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: registry.ely.by/elyby/skinsystem:latest
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
links:
|
||||||
|
- redis
|
||||||
|
- rabbitmq
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
ACCOUNTS_API_ID: ""
|
||||||
|
ACCOUNTS_API_SECRET: ""
|
||||||
|
STATSD_ADDR: ""
|
||||||
|
|
||||||
|
redis:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: redis
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.base.yml
|
||||||
|
service: rabbitmq
|
||||||
|
restart: always
|
||||||
43
lib/data/CapeItem.go
Normal file
43
lib/data/CapeItem.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/lib/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CapeItem struct {
|
||||||
|
File *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindCapeByUsername(username string) (CapeItem, error) {
|
||||||
|
var record CapeItem
|
||||||
|
file, err := os.Open(services.RootFolder + "/data/capes/" + strings.ToLower(username) + ".png")
|
||||||
|
if (err != nil) {
|
||||||
|
return record, CapeNotFound{username}
|
||||||
|
}
|
||||||
|
|
||||||
|
record.File = file
|
||||||
|
|
||||||
|
return record, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cape *CapeItem) CalculateHash() string {
|
||||||
|
hasher := md5.New()
|
||||||
|
io.Copy(hasher, cape.File)
|
||||||
|
|
||||||
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
type CapeNotFound struct {
|
||||||
|
Who string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e CapeNotFound) Error() string {
|
||||||
|
return fmt.Sprintf("Cape file not found. Required username \"%v\"", e.Who)
|
||||||
|
}
|
||||||
14
lib/data/SignedTexturesResponse.go
Normal file
14
lib/data/SignedTexturesResponse.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
type SignedTexturesResponse struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IsEly bool `json:"ely,omitempty"`
|
||||||
|
Props []Property `json:"properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Property struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Signature string `json:"signature,omitempty"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
106
lib/data/SkinItem.go
Normal file
106
lib/data/SkinItem.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"fmt"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/lib/services"
|
||||||
|
"elyby/minecraft-skinsystem/lib/tools"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/radix.v2/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SkinItem struct {
|
||||||
|
UserId int `json:"userId"`
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
SkinId int `json:"skinId"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Is1_8 bool `json:"is1_8"`
|
||||||
|
IsSlim bool `json:"isSlim"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
MojangTextures string `json:"mojangTextures"`
|
||||||
|
MojangSignature string `json:"mojangSignature"`
|
||||||
|
oldUsername string
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountIdToUsernameKey string = "hash:username-to-account-id"
|
||||||
|
|
||||||
|
func (s *SkinItem) Save() {
|
||||||
|
str, _ := json.Marshal(s)
|
||||||
|
pool, _ := services.RedisPool.Get()
|
||||||
|
pool.Cmd("MULTI")
|
||||||
|
|
||||||
|
// Если пользователь сменил ник, то мы должны удать его ключ
|
||||||
|
if (s.oldUsername != "" && s.oldUsername != s.Username) {
|
||||||
|
pool.Cmd("DEL", tools.BuildKey(s.oldUsername))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это новая запись или если пользователь сменил ник, то обновляем значение в хэш-таблице
|
||||||
|
if (s.oldUsername != "" || s.oldUsername != s.Username) {
|
||||||
|
pool.Cmd("HSET", accountIdToUsernameKey, s.UserId, s.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.Cmd("SET", tools.BuildKey(s.Username), str)
|
||||||
|
|
||||||
|
pool.Cmd("EXEC")
|
||||||
|
|
||||||
|
s.oldUsername = s.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SkinItem) Delete() {
|
||||||
|
if (s.oldUsername == "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, _ := services.RedisPool.Get()
|
||||||
|
pool.Cmd("MULTI")
|
||||||
|
|
||||||
|
pool.Cmd("DEL", tools.BuildKey(s.oldUsername))
|
||||||
|
pool.Cmd("HDEL", accountIdToUsernameKey, s.UserId)
|
||||||
|
|
||||||
|
pool.Cmd("EXEC")
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindSkinByUsername(username string) (SkinItem, error) {
|
||||||
|
var record SkinItem;
|
||||||
|
services.Logger.IncCounter("storage.query", 1)
|
||||||
|
response := services.RedisPool.Cmd("GET", tools.BuildKey(username));
|
||||||
|
if (response.IsType(redis.Nil)) {
|
||||||
|
services.Logger.IncCounter("storage.not_found", 1)
|
||||||
|
return record, SkinNotFound{username}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := response.Str()
|
||||||
|
if (err == nil) {
|
||||||
|
services.Logger.IncCounter("storage.found", 1)
|
||||||
|
decodeErr := json.Unmarshal([]byte(result), &record)
|
||||||
|
if (decodeErr != nil) {
|
||||||
|
log.Println("Cannot decode record data")
|
||||||
|
}
|
||||||
|
|
||||||
|
record.oldUsername = record.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
return record, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindSkinById(id int) (SkinItem, error) {
|
||||||
|
response := services.RedisPool.Cmd("HGET", accountIdToUsernameKey, id);
|
||||||
|
if (response.IsType(redis.Nil)) {
|
||||||
|
return SkinItem{}, SkinNotFound{"unknown"}
|
||||||
|
}
|
||||||
|
|
||||||
|
username, _ := response.Str()
|
||||||
|
|
||||||
|
return FindSkinByUsername(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkinNotFound struct {
|
||||||
|
Who string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e SkinNotFound) Error() string {
|
||||||
|
return fmt.Sprintf("Skin data not found. Required username \"%v\"", e.Who)
|
||||||
|
}
|
||||||
21
lib/data/TexturesResponse.go
Normal file
21
lib/data/TexturesResponse.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
type TexturesResponse struct {
|
||||||
|
Skin *Skin `json:"SKIN"`
|
||||||
|
Cape *Cape `json:"CAPE,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Skin struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Metadata *SkinMetadata `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkinMetadata struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cape struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
44
lib/external/accounts/AccountInfo.go
vendored
Normal file
44
lib/external/accounts/AccountInfo.go
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"io/ioutil"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountInfoResponse struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const internalAccountInfoUrl = domain + "/api/internal/accounts/info"
|
||||||
|
|
||||||
|
func (token *Token) AccountInfo(attribute string, value string) (AccountInfoResponse, error) {
|
||||||
|
request, err := http.NewRequest("GET", internalAccountInfoUrl, nil)
|
||||||
|
request.Header.Add("Authorization", "Bearer " + token.AccessToken)
|
||||||
|
query := request.URL.Query()
|
||||||
|
query.Add(attribute, value)
|
||||||
|
request.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
response, err := Client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
var info AccountInfoResponse
|
||||||
|
|
||||||
|
responseError := handleResponse(response)
|
||||||
|
if responseError != nil {
|
||||||
|
return info, responseError
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
println("Raw account info response is " + string(body))
|
||||||
|
json.Unmarshal(body, &info)
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
49
lib/external/accounts/GetToken.go
vendored
Normal file
49
lib/external/accounts/GetToken.go
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"net/url"
|
||||||
|
"io/ioutil"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenRequest struct {
|
||||||
|
Id string
|
||||||
|
Secret string
|
||||||
|
Scopes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenUrl = domain + "/api/oauth2/v1/token"
|
||||||
|
|
||||||
|
func GetToken(request TokenRequest) (Token, error) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("client_id", request.Id)
|
||||||
|
form.Add("client_secret", request.Secret)
|
||||||
|
form.Add("grant_type", "client_credentials")
|
||||||
|
form.Add("scope", strings.Join(request.Scopes, ","))
|
||||||
|
|
||||||
|
response, err := Client.Post(tokenUrl, "application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
var result Token
|
||||||
|
responseError := handleResponse(response)
|
||||||
|
if responseError != nil {
|
||||||
|
return result, responseError
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
|
||||||
|
json.Unmarshal(body, &result)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
51
lib/external/accounts/base.go
vendored
Normal file
51
lib/external/accounts/base.go
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const domain = "https://dev.account.ely.by"
|
||||||
|
|
||||||
|
var Client = &http.Client{}
|
||||||
|
|
||||||
|
type UnauthorizedResponse struct {}
|
||||||
|
|
||||||
|
func (err UnauthorizedResponse) Error() string {
|
||||||
|
return "Unauthorized response"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForbiddenResponse struct {}
|
||||||
|
|
||||||
|
func (err ForbiddenResponse) Error() string {
|
||||||
|
return "Forbidden response"
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotFoundResponse struct {}
|
||||||
|
|
||||||
|
func (err NotFoundResponse) Error() string {
|
||||||
|
return "Not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotSuccessResponse struct {
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err NotSuccessResponse) Error() string {
|
||||||
|
return fmt.Sprintf("Response code is \"%d\"", err.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleResponse(response *http.Response) error {
|
||||||
|
switch status := response.StatusCode; status {
|
||||||
|
case 200:
|
||||||
|
return nil
|
||||||
|
case 401:
|
||||||
|
return &UnauthorizedResponse{}
|
||||||
|
case 403:
|
||||||
|
return &ForbiddenResponse{}
|
||||||
|
case 404:
|
||||||
|
return &NotFoundResponse{}
|
||||||
|
default:
|
||||||
|
return &NotSuccessResponse{status}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
lib/routes/Cape.go
Normal file
40
lib/routes/Cape.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/lib/tools"
|
||||||
|
"elyby/minecraft-skinsystem/lib/data"
|
||||||
|
"elyby/minecraft-skinsystem/lib/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Cape(response http.ResponseWriter, request *http.Request) {
|
||||||
|
if (mux.Vars(request)["converted"] == "") {
|
||||||
|
services.Logger.IncCounter("capes.request", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
username := tools.ParseUsername(mux.Vars(request)["username"])
|
||||||
|
rec, err := data.FindCapeByUsername(username)
|
||||||
|
if (err != nil) {
|
||||||
|
http.Redirect(response, request, "http://skins.minecraft.net/MinecraftCloaks/" + username + ".png", 301)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set("Content-Type", "image/png")
|
||||||
|
io.Copy(response, rec.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CapeGET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
services.Logger.IncCounter("capes.get_request", 1)
|
||||||
|
username := r.URL.Query().Get("name")
|
||||||
|
if username == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.Vars(r)["username"] = username
|
||||||
|
mux.Vars(r)["converted"] = "1"
|
||||||
|
Cape(w, r)
|
||||||
|
}
|
||||||
29
lib/routes/Face.go
Normal file
29
lib/routes/Face.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/lib/tools"
|
||||||
|
"elyby/minecraft-skinsystem/lib/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultHash = "default"
|
||||||
|
|
||||||
|
func Face(w http.ResponseWriter, r *http.Request) {
|
||||||
|
username := tools.ParseUsername(mux.Vars(r)["username"])
|
||||||
|
rec, err := data.FindSkinByUsername(username)
|
||||||
|
var hash string
|
||||||
|
if (err != nil || rec.SkinId == 0) {
|
||||||
|
hash = defaultHash;
|
||||||
|
} else {
|
||||||
|
hash = rec.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, tools.BuildElyUrl(buildFaceUrl(hash)), 301);
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFaceUrl(hash string) string {
|
||||||
|
return "/minecraft/skin_buffer/faces/" + hash + ".png"
|
||||||
|
}
|
||||||
34
lib/routes/MinecraftPHP.go
Normal file
34
lib/routes/MinecraftPHP.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/lib/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Метод-наследие от первой версии системы скинов.
|
||||||
|
// Всё ещё иногда используется
|
||||||
|
// Просто конвертируем данные и отправляем их в основной обработчик
|
||||||
|
func MinecraftPHP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
username := r.URL.Query().Get("name")
|
||||||
|
required := r.URL.Query().Get("type")
|
||||||
|
if username == "" || required == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.Vars(r)["username"] = username
|
||||||
|
mux.Vars(r)["converted"] = "1"
|
||||||
|
switch required {
|
||||||
|
case "skin":
|
||||||
|
services.Logger.IncCounter("skins.minecraft-php-request", 1)
|
||||||
|
Skin(w, r)
|
||||||
|
case "cloack":
|
||||||
|
services.Logger.IncCounter("capes.minecraft-php-request", 1)
|
||||||
|
Cape(w, r)
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/routes/NotFound.go
Normal file
18
lib/routes/NotFound.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
json, _ := json.Marshal(map[string]string{
|
||||||
|
"status": "404",
|
||||||
|
"message": "Not Found",
|
||||||
|
"link": "http://docs.ely.by/skin-system.html",
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write(json)
|
||||||
|
}
|
||||||
44
lib/routes/SignedTextures.go
Normal file
44
lib/routes/SignedTextures.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"net/http"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/lib/data"
|
||||||
|
"elyby/minecraft-skinsystem/lib/tools"
|
||||||
|
"elyby/minecraft-skinsystem/lib/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SignedTextures(w http.ResponseWriter, r *http.Request) {
|
||||||
|
services.Logger.IncCounter("signed_textures.request", 1)
|
||||||
|
username := tools.ParseUsername(mux.Vars(r)["username"])
|
||||||
|
|
||||||
|
rec, err := data.FindSkinByUsername(username)
|
||||||
|
if (err != nil || rec.SkinId == 0 || rec.MojangTextures == "") {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData:= data.SignedTexturesResponse{
|
||||||
|
Id: strings.Replace(rec.Uuid, "-", "", -1),
|
||||||
|
Name: rec.Username,
|
||||||
|
Props: []data.Property{
|
||||||
|
{
|
||||||
|
Name: "textures",
|
||||||
|
Signature: rec.MojangSignature,
|
||||||
|
Value: rec.MojangTextures,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ely",
|
||||||
|
Value: "but why are you asking?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response,_ := json.Marshal(responseData)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(response)
|
||||||
|
}
|
||||||
39
lib/routes/Skin.go
Normal file
39
lib/routes/Skin.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/lib/tools"
|
||||||
|
"elyby/minecraft-skinsystem/lib/data"
|
||||||
|
"elyby/minecraft-skinsystem/lib/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Skin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if (mux.Vars(r)["converted"] == "") {
|
||||||
|
services.Logger.IncCounter("skins.request", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
username := tools.ParseUsername(mux.Vars(r)["username"])
|
||||||
|
rec, err := data.FindSkinByUsername(username)
|
||||||
|
if (err != nil) {
|
||||||
|
http.Redirect(w, r, "http://skins.minecraft.net/MinecraftSkins/" + username + ".png", 301)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, tools.BuildElyUrl(rec.Url), 301);
|
||||||
|
}
|
||||||
|
|
||||||
|
func SkinGET(w http.ResponseWriter, r *http.Request) {
|
||||||
|
services.Logger.IncCounter("skins.get_request", 1)
|
||||||
|
username := r.URL.Query().Get("name")
|
||||||
|
if username == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.Vars(r)["username"] = username
|
||||||
|
mux.Vars(r)["converted"] = "1"
|
||||||
|
Skin(w, r)
|
||||||
|
}
|
||||||
61
lib/routes/Textures.go
Normal file
61
lib/routes/Textures.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/lib/data"
|
||||||
|
"elyby/minecraft-skinsystem/lib/tools"
|
||||||
|
"elyby/minecraft-skinsystem/lib/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Textures(w http.ResponseWriter, r *http.Request) {
|
||||||
|
services.Logger.IncCounter("textures.request", 1)
|
||||||
|
username := tools.ParseUsername(mux.Vars(r)["username"])
|
||||||
|
|
||||||
|
rec, err := data.FindSkinByUsername(username)
|
||||||
|
if (err != nil || rec.SkinId == 0) {
|
||||||
|
rec.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png"
|
||||||
|
rec.Hash = string(tools.BuildNonElyTexturesHash(username))
|
||||||
|
} else {
|
||||||
|
rec.Url = tools.BuildElyUrl(rec.Url)
|
||||||
|
}
|
||||||
|
|
||||||
|
textures := data.TexturesResponse{
|
||||||
|
Skin: &data.Skin{
|
||||||
|
Url: rec.Url,
|
||||||
|
Hash: rec.Hash,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rec.IsSlim) {
|
||||||
|
textures.Skin.Metadata = &data.SkinMetadata{
|
||||||
|
Model: "slim",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
capeRec, err := data.FindCapeByUsername(username)
|
||||||
|
if (err == nil) {
|
||||||
|
capeUrl, err := services.Router.Get("cloaks").URL("username", username)
|
||||||
|
if (err != nil) {
|
||||||
|
log.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var scheme string = "http://";
|
||||||
|
if (r.TLS != nil) {
|
||||||
|
scheme = "https://"
|
||||||
|
}
|
||||||
|
|
||||||
|
textures.Cape = &data.Cape{
|
||||||
|
Url: scheme + r.Host + capeUrl.String(),
|
||||||
|
Hash: capeRec.CalculateHash(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response,_ := json.Marshal(textures)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(response)
|
||||||
|
}
|
||||||
18
lib/services/services.go
Normal file
18
lib/services/services.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mediocregopher/radix.v2/pool"
|
||||||
|
"github.com/streadway/amqp"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/mono83/slf/wd"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Router *mux.Router
|
||||||
|
|
||||||
|
var RedisPool *pool.Pool
|
||||||
|
|
||||||
|
var RabbitMQChannel *amqp.Channel
|
||||||
|
|
||||||
|
var RootFolder string
|
||||||
|
|
||||||
|
var Logger wd.Watchdog
|
||||||
40
lib/tools/tools.go
Normal file
40
lib/tools/tools.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"crypto/md5"
|
||||||
|
"strconv"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseUsername(username string) string {
|
||||||
|
const suffix = ".png"
|
||||||
|
if strings.HasSuffix(username, suffix) {
|
||||||
|
username = strings.TrimSuffix(username, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return username
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildNonElyTexturesHash(username string) string {
|
||||||
|
hour := getCurrentHour()
|
||||||
|
hasher := md5.New()
|
||||||
|
hasher.Write([]byte("non-ely-" + strconv.FormatInt(hour, 10) + "-" + username))
|
||||||
|
|
||||||
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildKey(username string) string {
|
||||||
|
return "username:" + strings.ToLower(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildElyUrl(route string) string {
|
||||||
|
return "http://ely.by" + route
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentHour() int64 {
|
||||||
|
n := time.Now()
|
||||||
|
return time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), 0, 0, 0, time.UTC).Unix()
|
||||||
|
}
|
||||||
|
|
||||||
22
lib/tools/tools_test.go
Normal file
22
lib/tools/tools_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package tools_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
. "elyby/minecraft-skinsystem/lib/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseUsername(t *testing.T) {
|
||||||
|
if ParseUsername("test.png") != "test" {
|
||||||
|
t.Error("Function should trim .png at end")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ParseUsername("test") != "test" {
|
||||||
|
t.Error("Function should return string itself, if it not contains .png at end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildKey(t *testing.T) {
|
||||||
|
if BuildKey("Test") != "username:test" {
|
||||||
|
t.Error("Function shound convert string to lower case and concatenate it with usernmae:")
|
||||||
|
}
|
||||||
|
}
|
||||||
82
lib/worker/handlers.go
Normal file
82
lib/worker/handlers.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"elyby/minecraft-skinsystem/lib/data"
|
||||||
|
"elyby/minecraft-skinsystem/lib/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleChangeUsername(model usernameChanged) (bool) {
|
||||||
|
if (model.OldUsername == "") {
|
||||||
|
services.Logger.IncCounter("worker.change_username.empty_old_username", 1)
|
||||||
|
record := data.SkinItem{
|
||||||
|
UserId: model.AccountId,
|
||||||
|
Username: model.NewUsername,
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Save()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := data.FindSkinById(model.AccountId)
|
||||||
|
if (err != nil) {
|
||||||
|
services.Logger.IncCounter("worker.change_username.id_not_found", 1)
|
||||||
|
fmt.Println("Cannot find user id. Trying to search.")
|
||||||
|
response, err := getById(model.AccountId)
|
||||||
|
if err != nil {
|
||||||
|
services.Logger.IncCounter("worker.change_username.id_not_restored", 1)
|
||||||
|
fmt.Printf("Cannot restore user info. %T\n", err)
|
||||||
|
// TODO: логгировать в какой-нибудь Sentry, если там не 404
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
services.Logger.IncCounter("worker.change_username.id_restored", 1)
|
||||||
|
fmt.Println("User info successfully restored.")
|
||||||
|
record = data.SkinItem{
|
||||||
|
UserId: response.Id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Username = model.NewUsername
|
||||||
|
record.Save()
|
||||||
|
|
||||||
|
services.Logger.IncCounter("worker.change_username.processed", 1)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSkinChanged(model skinChanged) bool {
|
||||||
|
record, err := data.FindSkinById(model.AccountId)
|
||||||
|
if err != nil {
|
||||||
|
services.Logger.IncCounter("worker.skin_changed.id_not_found", 1)
|
||||||
|
fmt.Println("Cannot find user id. Trying to search.")
|
||||||
|
response, err := getById(model.AccountId)
|
||||||
|
if err != nil {
|
||||||
|
services.Logger.IncCounter("worker.skin_changed.id_not_restored", 1)
|
||||||
|
fmt.Printf("Cannot restore user info. %T\n", err)
|
||||||
|
// TODO: логгировать в какой-нибудь Sentry, если там не 404
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
services.Logger.IncCounter("worker.skin_changed.id_restored", 1)
|
||||||
|
fmt.Println("User info successfully restored.")
|
||||||
|
record.UserId = response.Id
|
||||||
|
record.Username = response.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Uuid = model.Uuid
|
||||||
|
record.SkinId = model.SkinId
|
||||||
|
record.Hash = model.Hash
|
||||||
|
record.Is1_8 = model.Is1_8
|
||||||
|
record.IsSlim = model.IsSlim
|
||||||
|
record.Url = model.Url
|
||||||
|
record.MojangTextures = model.MojangTextures
|
||||||
|
record.MojangSignature = model.MojangSignature
|
||||||
|
|
||||||
|
record.Save()
|
||||||
|
|
||||||
|
services.Logger.IncCounter("worker.skin_changed.processed", 1)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
20
lib/worker/models.go
Normal file
20
lib/worker/models.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package worker
|
||||||
|
|
||||||
|
type usernameChanged struct {
|
||||||
|
AccountId int `json:"accountId"`
|
||||||
|
OldUsername string `json:"oldUsername"`
|
||||||
|
NewUsername string `json:"newUsername"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type skinChanged struct {
|
||||||
|
AccountId int `json:"userId"`
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
SkinId int `json:"skinId"`
|
||||||
|
OldSkinId int `json:"oldSkinId"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Is1_8 bool `json:"is1_8"`
|
||||||
|
IsSlim bool `json:"isSlim"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
MojangTextures string `json:"mojangTextures"`
|
||||||
|
MojangSignature string `json:"mojangSignature"`
|
||||||
|
}
|
||||||
58
lib/worker/supports.go
Normal file
58
lib/worker/supports.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"elyby/minecraft-skinsystem/lib/external/accounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AccountsTokenConfig *accounts.TokenRequest
|
||||||
|
|
||||||
|
var token *accounts.Token
|
||||||
|
|
||||||
|
const repeatsLimit = 3
|
||||||
|
var repeatsCount = 0
|
||||||
|
|
||||||
|
func getById(id int) (accounts.AccountInfoResponse, error) {
|
||||||
|
return _getByField("id", strconv.Itoa(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getByField(field string, value string) (accounts.AccountInfoResponse, error) {
|
||||||
|
defer resetRepeatsCount()
|
||||||
|
|
||||||
|
apiToken, err := getToken()
|
||||||
|
if err != nil {
|
||||||
|
return accounts.AccountInfoResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := apiToken.AccountInfo(field, value)
|
||||||
|
if err != nil {
|
||||||
|
_, ok := err.(*accounts.UnauthorizedResponse)
|
||||||
|
if !ok || repeatsCount >= repeatsLimit {
|
||||||
|
return accounts.AccountInfoResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
repeatsCount++
|
||||||
|
token = nil
|
||||||
|
|
||||||
|
return _getByField(field, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getToken() (*accounts.Token, error) {
|
||||||
|
if token == nil {
|
||||||
|
tempToken, err := accounts.GetToken(*AccountsTokenConfig)
|
||||||
|
if err != nil {
|
||||||
|
return &accounts.Token{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
token = &tempToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetRepeatsCount() {
|
||||||
|
repeatsCount = 0
|
||||||
|
}
|
||||||
88
lib/worker/worker.go
Normal file
88
lib/worker/worker.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/lib/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
const exchangeName string = "events"
|
||||||
|
const queueName string = "skinsystem-accounts-events"
|
||||||
|
|
||||||
|
func Listen() {
|
||||||
|
var err error
|
||||||
|
ch := services.RabbitMQChannel
|
||||||
|
|
||||||
|
err = ch.ExchangeDeclare(
|
||||||
|
exchangeName, // name
|
||||||
|
"topic", // type
|
||||||
|
true, // durable
|
||||||
|
false, // auto-deleted
|
||||||
|
false, // internal
|
||||||
|
false, // no-wait
|
||||||
|
nil, // arguments
|
||||||
|
)
|
||||||
|
failOnError(err, "Failed to declare an exchange")
|
||||||
|
|
||||||
|
_, err = ch.QueueDeclare(
|
||||||
|
queueName, // name
|
||||||
|
true, // durable
|
||||||
|
false, // delete when usused
|
||||||
|
false, // exclusive
|
||||||
|
false, // no-wait
|
||||||
|
nil, // arguments
|
||||||
|
)
|
||||||
|
failOnError(err, "Failed to declare a queue")
|
||||||
|
|
||||||
|
err = ch.QueueBind(queueName, "accounts.username-changed", exchangeName, false, nil)
|
||||||
|
failOnError(err, "Failed to bind a queue")
|
||||||
|
|
||||||
|
err = ch.QueueBind(queueName, "accounts.skin-changed", exchangeName, false, nil)
|
||||||
|
failOnError(err, "Failed to bind a queue")
|
||||||
|
|
||||||
|
msgs, err := ch.Consume(
|
||||||
|
queueName, // queue
|
||||||
|
"", // consumer
|
||||||
|
false, // auto-ack
|
||||||
|
false, // exclusive
|
||||||
|
false, // no-local
|
||||||
|
false, // no-wait
|
||||||
|
nil, // args
|
||||||
|
)
|
||||||
|
failOnError(err, "Failed to register a consumer")
|
||||||
|
|
||||||
|
forever := make(chan bool)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for d := range msgs {
|
||||||
|
log.Println("Incoming message with routing key " + d.RoutingKey)
|
||||||
|
var result bool = true;
|
||||||
|
switch d.RoutingKey {
|
||||||
|
case "accounts.username-changed":
|
||||||
|
var model usernameChanged
|
||||||
|
json.Unmarshal(d.Body, &model)
|
||||||
|
result = handleChangeUsername(model)
|
||||||
|
case "accounts.skin-changed":
|
||||||
|
var model skinChanged
|
||||||
|
json.Unmarshal(d.Body, &model)
|
||||||
|
result = handleSkinChanged(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
d.Ack(false)
|
||||||
|
} else {
|
||||||
|
d.Reject(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-forever
|
||||||
|
}
|
||||||
|
|
||||||
|
func failOnError(err error, msg string) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%s: %s", msg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
148
minecraft-skinsystem.go
Normal file
148
minecraft-skinsystem.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/streadway/amqp"
|
||||||
|
"github.com/mediocregopher/radix.v2/pool"
|
||||||
|
"github.com/mono83/slf/wd"
|
||||||
|
"github.com/mono83/slf/rays"
|
||||||
|
"github.com/mono83/slf/recievers/ansi"
|
||||||
|
"github.com/mono83/slf/recievers/statsd"
|
||||||
|
|
||||||
|
"elyby/minecraft-skinsystem/lib/routes"
|
||||||
|
"elyby/minecraft-skinsystem/lib/services"
|
||||||
|
"elyby/minecraft-skinsystem/lib/worker"
|
||||||
|
"elyby/minecraft-skinsystem/lib/external/accounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
const redisPoolSize int = 10
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("Starting...")
|
||||||
|
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
|
||||||
|
accountsApiId := os.Getenv("ACCOUNTS_API_ID")
|
||||||
|
accountsApiSecret := os.Getenv("ACCOUNTS_API_SECRET")
|
||||||
|
if accountsApiId == "" || accountsApiSecret == "" {
|
||||||
|
log.Fatal("ACCOUNTS_API params must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.AccountsTokenConfig = &accounts.TokenRequest{
|
||||||
|
Id: accountsApiId,
|
||||||
|
Secret: accountsApiSecret,
|
||||||
|
Scopes: []string{
|
||||||
|
"internal_account_info",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Connecting to redis")
|
||||||
|
|
||||||
|
var redisString = os.Getenv("REDIS_ADDR")
|
||||||
|
if (redisString == "") {
|
||||||
|
redisString = "redis:6379"
|
||||||
|
}
|
||||||
|
|
||||||
|
redisPool, redisErr := pool.New("tcp", redisString, redisPoolSize)
|
||||||
|
if (redisErr != nil) {
|
||||||
|
log.Fatal("Redis unavailable")
|
||||||
|
}
|
||||||
|
log.Println("Connected to redis")
|
||||||
|
|
||||||
|
log.Println("Connecting to rabbitmq")
|
||||||
|
// TODO: rabbitmq становится доступен не сразу. Нужно дождаться, пока он станет доступен, периодически повторяя запросы
|
||||||
|
|
||||||
|
var rabbitmqString = os.Getenv("RABBITMQ_ADDR")
|
||||||
|
if (rabbitmqString == "") {
|
||||||
|
rabbitmqString = "amqp://ely-skinsystem-app:ely-skinsystem-app-password@rabbitmq:5672/%2fely"
|
||||||
|
}
|
||||||
|
|
||||||
|
rabbitConnection, rabbitmqErr := amqp.Dial(rabbitmqString)
|
||||||
|
if (rabbitmqErr != nil) {
|
||||||
|
log.Fatalf("%s", rabbitmqErr)
|
||||||
|
}
|
||||||
|
log.Println("Connected to rabbitmq. Trying to open a channel")
|
||||||
|
rabbitChannel, rabbitmqErr := rabbitConnection.Channel()
|
||||||
|
if (rabbitmqErr != nil) {
|
||||||
|
log.Fatalf("%s", rabbitmqErr)
|
||||||
|
}
|
||||||
|
log.Println("Connected to rabbitmq channel")
|
||||||
|
|
||||||
|
// statsd
|
||||||
|
var statsdString = os.Getenv("STATSD_ADDR")
|
||||||
|
statsdString = ""
|
||||||
|
if (statsdString != "") {
|
||||||
|
log.Println("Connecting to statsd")
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
statsdReceiver, err := statsd.NewReceiver(statsd.Config{
|
||||||
|
Address: statsdString,
|
||||||
|
Prefix: "ely.skinsystem." + hostname + ".app.",
|
||||||
|
FlushEvery: 1,
|
||||||
|
})
|
||||||
|
if (err != nil) {
|
||||||
|
log.Fatal("statsd connection error")
|
||||||
|
}
|
||||||
|
|
||||||
|
wd.AddReceiver(statsdReceiver)
|
||||||
|
} else {
|
||||||
|
wd.AddReceiver(ansi.New(true, true, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := wd.New("", "").WithParams(rays.Host)
|
||||||
|
|
||||||
|
router := mux.NewRouter().StrictSlash(true)
|
||||||
|
router.HandleFunc("/skins/{username}", routes.Skin).Methods("GET").Name("skins")
|
||||||
|
router.HandleFunc("/cloaks/{username}", routes.Cape).Methods("GET").Name("cloaks")
|
||||||
|
router.HandleFunc("/textures/{username}", routes.Textures).Methods("GET").Name("textures")
|
||||||
|
router.HandleFunc("/textures/signed/{username}", routes.SignedTextures).Methods("GET").Name("signedTextures")
|
||||||
|
router.HandleFunc("/skins/{username}/face", routes.Face).Methods("GET").Name("faces")
|
||||||
|
router.HandleFunc("/skins/{username}/face.png", routes.Face).Methods("GET").Name("faces")
|
||||||
|
// Legacy
|
||||||
|
router.HandleFunc("/minecraft.php", routes.MinecraftPHP).Methods("GET")
|
||||||
|
router.HandleFunc("/skins/", routes.SkinGET).Methods("GET")
|
||||||
|
router.HandleFunc("/cloaks/", routes.CapeGET).Methods("GET")
|
||||||
|
// 404
|
||||||
|
router.NotFoundHandler = http.HandlerFunc(routes.NotFound)
|
||||||
|
|
||||||
|
services.Router = router
|
||||||
|
services.RedisPool = redisPool
|
||||||
|
services.RabbitMQChannel = rabbitChannel
|
||||||
|
services.Logger = logger
|
||||||
|
|
||||||
|
_, file, _, _ := runtime.Caller(0)
|
||||||
|
services.RootFolder = filepath.Dir(file)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
period := 5
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Duration(period) * time.Second)
|
||||||
|
|
||||||
|
resp := services.RedisPool.Cmd("PING")
|
||||||
|
if (resp.Err == nil) {
|
||||||
|
// Если редис успешно пинганулся, значит всё хорошо
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Redis not pinged. Try to reconnect")
|
||||||
|
newPool, redisErr := pool.New("tcp", redisString, redisPoolSize)
|
||||||
|
if (redisErr != nil) {
|
||||||
|
log.Printf("Cannot reconnect to redis, waiting %d seconds\n", period)
|
||||||
|
} else {
|
||||||
|
services.RedisPool = newPool
|
||||||
|
log.Println("Reconnected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go worker.Listen()
|
||||||
|
|
||||||
|
log.Println("Started");
|
||||||
|
log.Fatal(http.ListenAndServe(":80", router))
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Phalcon\Mvc\Collection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @property string $id
|
|
||||||
*/
|
|
||||||
class Skins extends Collection {
|
|
||||||
|
|
||||||
public $_id;
|
|
||||||
public $userId;
|
|
||||||
public $nickname;
|
|
||||||
public $skinId;
|
|
||||||
public $url;
|
|
||||||
public $is1_8;
|
|
||||||
public $isSlim;
|
|
||||||
public $hash;
|
|
||||||
|
|
||||||
public function getId() {
|
|
||||||
return $this->_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSource() {
|
|
||||||
return 'skins';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $nickname
|
|
||||||
* @return bool|Skins
|
|
||||||
*/
|
|
||||||
public static function findByNickname($nickname) {
|
|
||||||
return static::findFirst([
|
|
||||||
[
|
|
||||||
'nickname' => mb_convert_case($nickname, MB_CASE_LOWER, ENCODING),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
40
nginx.conf
40
nginx.conf
@@ -1,40 +0,0 @@
|
|||||||
location /minecraft.php {
|
|
||||||
if ($arg_name = "") {
|
|
||||||
return 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($arg_type = "cloack") {
|
|
||||||
rewrite .* http://skins.minecraft.net/MinecraftCloaks/$arg_name.png? permanent;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($arg_type = "skin") {
|
|
||||||
rewrite .* /skins/$arg_name last;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /cloaks/ {
|
|
||||||
try_files $uri $uri.png @cloaks;
|
|
||||||
}
|
|
||||||
|
|
||||||
location @cloaks {
|
|
||||||
rewrite ^/cloaks/(.+?)(\.[^.]*$|$)$ http://skins.minecraft.net/MinecraftCloaks/$1.png? permanent;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* ^/skins/$ {
|
|
||||||
if ($arg_name = "") {
|
|
||||||
return 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite .* /skins/$arg_name permanent;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* ^/cloaks/$ {
|
|
||||||
if ($arg_name = "") {
|
|
||||||
return 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite .* /cloaks/$arg_name permanent;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
AddDefaultCharset UTF-8
|
|
||||||
|
|
||||||
<IfModule mod_rewrite.c>
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteRule ^(.*)$ index.php?_url=/$1 [QSA,L]
|
|
||||||
</IfModule>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Phalcon\Mvc\Micro;
|
|
||||||
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
try {
|
|
||||||
/** @var \Phalcon\Config $config */
|
|
||||||
$config = include __DIR__ . '/../config/config.php';
|
|
||||||
/** @var \Phalcon\Loader $loader */
|
|
||||||
include __DIR__ . '/../config/loader.php';
|
|
||||||
/** @var Phalcon\DI\FactoryDefault $di */
|
|
||||||
include __DIR__ . '/../config/services.php';
|
|
||||||
|
|
||||||
$app = new Micro($di);
|
|
||||||
include __DIR__ . '/../app.php';
|
|
||||||
|
|
||||||
$app->handle();
|
|
||||||
|
|
||||||
} catch (Phalcon\Exception $e) {
|
|
||||||
echo $e->getMessage();
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo $e->getMessage();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user