mirror of
https://github.com/elyby/chrly.git
synced 2025-05-31 14:11:51 +05:30
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7734f2cbd5 | ||
|
|
2cd97dda8b | ||
|
|
ded50df8b8 | ||
|
|
55b8c12955 | ||
|
|
10ff6f34fb | ||
|
|
31cd75ffa7 | ||
|
|
d7bc77e5a7 | ||
|
|
befa163f0e | ||
|
|
cb7adab3df | ||
|
|
87a302c7da | ||
|
|
ce4dce49a2 | ||
|
|
11647f2eae | ||
|
|
acd0237fac | ||
|
|
55f52d0ad4 | ||
|
|
778bc615aa | ||
|
|
235f65f11c | ||
|
|
8dd6a581a9 | ||
|
|
055f3ce6c0 | ||
|
|
a9f5632743 | ||
|
|
ce99ac8cf8 | ||
|
|
6192a58f63 | ||
|
|
caebac1753 | ||
|
|
dcaa4c037d | ||
|
|
9e4f805ed3 | ||
|
|
ad7faf6e81 | ||
|
|
855302ec60 | ||
|
|
f5f8fbc65e | ||
|
|
968c83db99 | ||
|
|
1e2f30c6c7 | ||
|
|
f120064fe3 | ||
|
|
aaff88d32f | ||
|
|
b8c3cc6cf8 | ||
|
|
ca4479252f | ||
|
|
d2485df64d | ||
|
|
6a489287ba | ||
|
|
6e7a61f5f2 | ||
|
|
20b8e8da86 | ||
|
|
63df092973 | ||
|
|
378643623b | ||
|
|
e33b86b809 | ||
|
|
80fa307915 | ||
|
|
2e9520db89 | ||
|
|
74564b4747 | ||
|
|
18909776a8 | ||
|
|
d1b1f22a93 | ||
|
|
cb928a3918 | ||
|
|
d9aeaba627 | ||
|
|
645f6ac694 | ||
|
|
eab7c6ecaa | ||
|
|
ac714de8df | ||
|
|
8007b082d6 | ||
|
|
9cb6502f9c | ||
|
|
76a3f3ad26 | ||
|
|
bdd7c5e15e | ||
|
|
340b24d862 | ||
|
|
cf99a0eab2 | ||
|
|
fb4ae46e29 | ||
|
|
971155485b | ||
|
|
9ee3e93042 | ||
|
|
6128c56a0c | ||
|
|
a2e3d28580 | ||
|
|
fecfa9c4e8 | ||
|
|
04714543b8 | ||
|
|
ec461efe34 | ||
|
|
eec6b384b7 | ||
|
|
4734bfd93c | ||
|
|
b1dbee2310 | ||
|
|
78917a70d3 | ||
|
|
4bf146dd43 | ||
|
|
06b8e88346 | ||
|
|
4945b3f984 | ||
|
|
359aef4b40 | ||
|
|
b159cd327c | ||
|
|
b99697d26e | ||
|
|
d51c358ef6 | ||
|
|
d9629b5e83 | ||
|
|
428bedf301 | ||
|
|
11a7570f51 | ||
|
|
676ba03c37 | ||
|
|
07903cf9c8 | ||
|
|
e090d04dc7 | ||
|
|
a993c1d157 | ||
|
|
a661f9aac3 | ||
|
|
9ffdf99b77 | ||
|
|
ad35872fc1 | ||
|
|
a8d8fffaa5 | ||
|
|
0d41f0c347 | ||
|
|
b22f0551fa | ||
|
|
1a906cfc09 | ||
|
|
f610667aa5 | ||
|
|
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 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
data
|
||||
vendor
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
# IDEA
|
||||
/.idea
|
||||
|
||||
# Docker Compose file
|
||||
.idea
|
||||
docker-compose.yml
|
||||
docker-compose.override.yml
|
||||
vendor
|
||||
.cover
|
||||
|
||||
41
.travis.yml
Normal file
41
.travis.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
sudo: required
|
||||
|
||||
language: go
|
||||
go:
|
||||
- 1.9
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
stages:
|
||||
- test
|
||||
- name: deploy
|
||||
if: branch = master OR tag IS present
|
||||
|
||||
install:
|
||||
- go get -u github.com/golang/dep/cmd/dep
|
||||
- dep ensure
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: test
|
||||
script:
|
||||
- go test -v -race ./...
|
||||
- stage: deploy
|
||||
script:
|
||||
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
- export DOCKER_TAG="${TRAVIS_TAG:-dev}"
|
||||
- export APP_VERSION="${TRAVIS_TAG:-dev-${TRAVIS_COMMIT:0:7}}"
|
||||
- >
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64
|
||||
go build
|
||||
-o release/chrly
|
||||
-ldflags '-extldflags "-static" -X github.com/elyby/chrly/bootstrap.version=$APP_VERSION'
|
||||
main.go
|
||||
- docker build -t elyby/chrly:$DOCKER_TAG .
|
||||
- docker push elyby/chrly:$DOCKER_TAG
|
||||
- |
|
||||
if [ -z ${TRAVIS_TAG+x} ]; then
|
||||
docker tag elyby/chrly:$DOCKER_TAG elyby/chrly:latest
|
||||
docker push elyby/chrly:latest
|
||||
fi
|
||||
22
Dockerfile
22
Dockerfile
@@ -1,18 +1,12 @@
|
||||
FROM golang:1.7
|
||||
|
||||
RUN mkdir -p /go/src/elyby/minecraft-skinsystem \
|
||||
&& 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
|
||||
FROM alpine:3.7
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
VOLUME ["/go/src/app"]
|
||||
ENV STORAGE_REDIS_HOST=redis
|
||||
ENV STORAGE_FILESYSTEM_HOST=/data
|
||||
|
||||
CMD ["go-wrapper", "run"]
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
COPY release/chrly /usr/local/bin/
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["serve"]
|
||||
|
||||
184
Gopkg.lock
generated
Normal file
184
Gopkg.lock
generated
Normal file
@@ -0,0 +1,184 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/SermoDigital/jose"
|
||||
packages = [".","crypto","jws","jwt"]
|
||||
revision = "f6df55f235c24f236d11dbcf665249a59ac2021f"
|
||||
version = "1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/certifi/gocertifi"
|
||||
packages = ["."]
|
||||
revision = "3fd9e1adb12b72d2f3f82191d49be9b93c69f67c"
|
||||
version = "2017.07.27"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
packages = ["."]
|
||||
revision = "629574ca2a5df945712d3079857300b5e4da0236"
|
||||
version = "v1.4.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/getsentry/raven-go"
|
||||
packages = ["."]
|
||||
revision = "d175f85701dfbf44cb0510114c9943e665e60907"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/mock"
|
||||
packages = ["gomock"]
|
||||
revision = "13f360950a79f5864a972c786a10a50e44b69541"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/context"
|
||||
packages = ["."]
|
||||
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
|
||||
version = "v1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "bcd8bc72b08df0f70df986b97f95590779502d31"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/hcl"
|
||||
packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"]
|
||||
revision = "8f6b1344a92ff8877cf24a5de9177bf7d0a2a187"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/magiconair/properties"
|
||||
packages = ["."]
|
||||
revision = "be5ece7dd465ab0765a9682137865547526d1dfb"
|
||||
version = "v1.7.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mediocregopher/radix.v2"
|
||||
packages = ["cluster","pool","redis","util"]
|
||||
revision = "d234cfb904a91daafa4e1f92599a893b349cc0c2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
revision = "d0303fe809921458f417bcf828397a65db30a7e4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mono83/slf"
|
||||
packages = [".","filters","params","rays","recievers","recievers/sentry","recievers/statsd","recievers/writer","wd"]
|
||||
revision = "79153e9636db86e1c6b74d74dd04176f257a4f2d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mono83/udpwriter"
|
||||
packages = ["."]
|
||||
revision = "a064bd7e3acfda563ea680b913b9ef24b7a73e15"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pelletier/go-buffruneio"
|
||||
packages = ["."]
|
||||
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pelletier/go-toml"
|
||||
packages = ["."]
|
||||
revision = "5ccdfb18c776b740aecaf085c4d9a2779199c279"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/afero"
|
||||
packages = [".","mem"]
|
||||
revision = "ee1bd8ee15a1306d1f9201acc41ef39cd9f99a1b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/cast"
|
||||
packages = ["."]
|
||||
revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = ["."]
|
||||
revision = "0c34d16c3123764e413b9ed982ada58b1c3d53ea"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
packages = ["."]
|
||||
revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/viper"
|
||||
packages = ["."]
|
||||
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
||||
version = "v1.1.4"
|
||||
|
||||
[[projects]]
|
||||
branch = "issue-18"
|
||||
name = "github.com/thedevsaddam/govalidator"
|
||||
packages = ["."]
|
||||
revision = "59055296916bb3c6ad9cf3b21d5f2cf7059f8e76"
|
||||
source = "https://github.com/erickskrauch/govalidator.git"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
revision = "7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/text"
|
||||
packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"]
|
||||
revision = "bd91bbf73e9a4a801adbfb97133c992678533126"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "e6bd87f630333e3e5b03bea33720c3281a9094551bd5ced436062157fe51ab71"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
44
Gopkg.toml
Normal file
44
Gopkg.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
ignored = ["github.com/elyby/chrly"]
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/mux"
|
||||
version = "1.4.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mediocregopher/radix.v2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mono83/slf"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/cobra"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/viper"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/getsentry/raven-go"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/SermoDigital/jose"
|
||||
version = "~1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/thedevsaddam/govalidator"
|
||||
source = "https://github.com/erickskrauch/govalidator.git"
|
||||
branch = "issue-18"
|
||||
|
||||
# Testing dependencies
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "^1.1.4"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golang/mock"
|
||||
version = "^1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/h2non/gock.v1"
|
||||
version = "^1.0.6"
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Ely.by (http://ely.by)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
256
README.md
256
README.md
@@ -1,38 +1,252 @@
|
||||
# Это заготовка для нормального файла
|
||||
# Chrly
|
||||
|
||||
Для настройки Dev-окружения нужно склонировать проект в удобное место,
|
||||
за тем сделать символьную ссылку в свой GOPATH:
|
||||
Chrly is a lightweight implementation of Minecraft skins system server. It's packaged and distributed as a Docker
|
||||
image and can be downloaded from [Dockerhub](https://hub.docker.com/r/elyby/chrly/). App is written in Go, can
|
||||
withstand heavy loads and is production ready.
|
||||
|
||||
```sh
|
||||
# Выполнять, находясь внутри директории репозитория
|
||||
mkdir -p $GOPATH/src/elyby
|
||||
ln -s $PWD $GOPATH/src/elyby/minecraft-skinsystem
|
||||
## Installation
|
||||
|
||||
You can easily install Chrly using [docker-compose](https://docs.docker.com/compose/). The configuration below (save
|
||||
it as `docker-compose.yml`) can be used to start a Chrly server. It relies on `CHRLY_SECRET` environment variable
|
||||
that you must set before running `docker-compose up -d`. Other possible variables are described below.
|
||||
|
||||
```yml
|
||||
version: '2'
|
||||
services:
|
||||
app:
|
||||
image: elyby/chrly
|
||||
hostname: chrly0
|
||||
restart: always
|
||||
links:
|
||||
- redis
|
||||
volumes:
|
||||
- ./data/capes:/data/capes
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
CHRLY_SECRET: replace_this_value_in_production
|
||||
|
||||
redis:
|
||||
image: redis:4.0-32bit
|
||||
restart: always
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
```
|
||||
|
||||
Или можно склонировать репозиторий сразу в нужную локацию:
|
||||
Chrly will mount some volumes on the host machine to persist storage for capes and Redis database.
|
||||
|
||||
### Config
|
||||
|
||||
Application's configuration is based on the environment variables. You can adjust config by modifying `environment` key
|
||||
inside your `docker-compose.yml` file. After value will have been changed, container should be stopped and recreated.
|
||||
If environment variables have been changed, Docker will automatically recreate the container, so you only need to `stop`
|
||||
and `up` it:
|
||||
|
||||
```sh
|
||||
git clone git@bitbucket.org:elyby/minecraft-skinsystem.git $GOPATH/src/elyby/minecraft-skinsystem
|
||||
docker-compose stop app
|
||||
docker-compose up -d app
|
||||
```
|
||||
|
||||
Нужно скопировать правильный docker-compose файл для желаемого окружения:
|
||||
**Variables to adjust:**
|
||||
|
||||
```sh
|
||||
cp docker-compose.dev.yml docker-compose.yml # dev env
|
||||
cp docker-compose.prod.yml docker-compose.yml # prod env
|
||||
| ENV | Description | Example |
|
||||
|--------------------|------------------------------------------------------------------------------------|-------------------------------------------|
|
||||
| STORAGE_REDIS_POOL | By default, Chrly creates pool with 10 connection, but you may want to increase it | `20` |
|
||||
| STATSD_ADDR | StatsD can be used to collect metrics | `localhost:8125` |
|
||||
| SENTRY_DSN | Sentry can be used to collect app errors | `https://public:private@your.sentry.io/1` |
|
||||
|
||||
If something goes wrong, you can always access logs by executing `docker-compose logs -f app`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
Each endpoint that accepts `username` as a part of an url takes it case insensitive. `.png` part can be omitted too.
|
||||
|
||||
#### `GET /skins/{username}.png`
|
||||
|
||||
This endpoint responds to requested `username` with a skin texture. If user's skin was set as texture's link, then it'll
|
||||
respond with the `301` redirect to that url. If there is no record for requested username, it'll redirect to the
|
||||
Mojang skins system as: `http://skins.minecraft.net/MinecraftSkins/{username}.png` with the original username's case.
|
||||
|
||||
#### `GET /cloaks/{username}.png`
|
||||
|
||||
It responds to requested `username` with a cape texture. If user's cape file doesn't exists, then it'll redirect to the
|
||||
Mojang skins system as: `http://skins.minecraft.net/MinecraftCloaks/{username}.png` with the original username's case.
|
||||
|
||||
#### `GET /textures/{username}`
|
||||
|
||||
This endpoint forms response payloads as if it was the `textures`' property, but without base64 encoding. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"SKIN": {
|
||||
"url": "http://ely.by/minecraft/skins/skin.png",
|
||||
"hash": "55d2a8848764f5ff04012cdb093458bd",
|
||||
"metadata": {
|
||||
"model": "slim"
|
||||
}
|
||||
},
|
||||
"CAPE": {
|
||||
"url": "http://skinsystem.ely.by/cloaks/username",
|
||||
"hash": "424ff79dce9940af89c28ad80de8aaad"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
И за тем всё это поднять:
|
||||
If record for the requested username wasn't found, cape would be omitted and skin would be formed for Mojang skins
|
||||
system. Hash would be formed as the username plus the half-hour-ranged time of request, which is needed to improve
|
||||
caching of Mojang skins inside Minecraft.
|
||||
|
||||
That request is handy in case when your server implements authentication for a game server (e.g. join/hasJoined
|
||||
operation) and you have to respond with hasJoined request with an actual user textures. You have to simply send request
|
||||
to the Chrly server and put the result in your hasJoined response.
|
||||
|
||||
#### `GET /textures/signed/{username}`
|
||||
|
||||
Actually, it's [Ely.by](http://ely.by) feature called [Server Skins System](http://ely.by/server-skins-system), but if
|
||||
you have your own source of the Mojang signatures, then you can pass it with textures and it'll be displayed in this
|
||||
method. Received response should be directly sent to the client without any modification via game server API.
|
||||
|
||||
Response example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||
"name": "username",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "signature value",
|
||||
"value": "base64 encoded value"
|
||||
},
|
||||
{
|
||||
"name": "chrly",
|
||||
"value": "how do you tame a horse in Minecraft?"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If there is no requested `username` or `mojangSignature` field isn't set, `204` status code will be sent.
|
||||
|
||||
#### `GET /skins?name={username}`
|
||||
|
||||
Equivalent of the `GET /skins/{username}.png`, but constructed especially for old Minecraft versions, where username
|
||||
placeholder wasn't used.
|
||||
|
||||
#### `GET /cloaks?name={username}`
|
||||
|
||||
Equivalent of the `GET /cloaks/{username}.png`, but constructed especially for old Minecraft versions, where username
|
||||
placeholder wasn't used.
|
||||
|
||||
### Records manipulating API
|
||||
|
||||
Each request to the internal API should be performed with the Bearer authorization header. Example curl request:
|
||||
|
||||
```sh
|
||||
curl -X POST -i http://chrly.domain.com/api/skins \
|
||||
-H "Authorization: Bearer Ym9zY236Ym9zY28="
|
||||
```
|
||||
|
||||
You can obtain token by executing `docker-compose run --rm app token`.
|
||||
|
||||
#### `POST /api/skins`
|
||||
|
||||
> **Warning**: skin uploading via `skin` field is not implemented for now.
|
||||
|
||||
Endpoint allows you to create or update skin record for a username. To upload skin, you have to send multipart
|
||||
form data. `form-urlencoded` also supported, but, as you may know, it doesn't support files uploading.
|
||||
|
||||
**Request params:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------------|--------|--------------------------------------------------------------------------------|
|
||||
| identityId | int | Unique record identifier. |
|
||||
| username | string | Username. Case insensitive. |
|
||||
| uuid | uuid | UUID of the user. |
|
||||
| skinId | int | Skin identifier. |
|
||||
| hash | string | Skin's hash. Algorithm can be any. For example `md5`. |
|
||||
| is1_8 | bool | Does the skin have the new format (64x64). |
|
||||
| isSlim | bool | Does skin have slim arms (Alex model). |
|
||||
| mojangTextures | string | Mojang textures field. It must be a base64 encoded json string. Not required. |
|
||||
| mojangSignature | string | Signature for Mojang textures, which is required when `mojangTextures` passed. |
|
||||
| url | string | Actual url of the skin. You have to pass this parameter or `skin`. |
|
||||
| skin | file | Skin file. You have to pass this parameter or `url`. |
|
||||
|
||||
If successful you'll receive `201` status code. In the case of failure there will be `400` status code and errors list
|
||||
as json:
|
||||
|
||||
```json
|
||||
{
|
||||
"errors": {
|
||||
"identityId": [
|
||||
"The identityId field must be numeric"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `DELETE /api/skins/id:{identityId}`
|
||||
|
||||
Performs record removal by identity id. Request body is not required. On success you will receive `204` status code.
|
||||
On failure it'll be `404` with the json body:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Cannot find record for requested user id"
|
||||
}
|
||||
```
|
||||
|
||||
#### `DELETE /api/skins/{username}`
|
||||
|
||||
Same endpoint as above but it removes record by identity's username. Have the same behavior, but in case of failure
|
||||
response will be:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Cannot find record for requested username"
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
First of all you should install the [latest stable version of Go](https://golang.org/doc/install) and set `GOPATH`
|
||||
environment variable.
|
||||
|
||||
This project uses [`dep`](https://github.com/golang/dep) for dependencies management, so it
|
||||
[should be installed](https://github.com/golang/dep#installation) too.
|
||||
|
||||
Then you must fork this repository. Now follow these steps:
|
||||
|
||||
```sh
|
||||
# Get the source code
|
||||
go get github.com/elyby/chrly
|
||||
# Switch to the project folder
|
||||
cd $GOPATH/src/github.com/elyby/chrly
|
||||
# Install dependencies (it can take a while)
|
||||
dep ensure
|
||||
# Add your fork link as a remote
|
||||
git remote add fork git@github.com:your-username/chrly.git
|
||||
# Create a new branch for your task
|
||||
git checkout -b iss-123
|
||||
```
|
||||
|
||||
You only need to execute `go run main.go` to run the project, but without Redis database and a secret key it won't work
|
||||
for very long. You have to export `CHRLY_SECRET` environment variable globally or pass it via `env`:
|
||||
|
||||
```sh
|
||||
env CHRLY_SECRET=some_local_secret go run main.go serve
|
||||
```
|
||||
|
||||
Redis can be installed manually, but if you have [Docker installed](https://docs.docker.com/install/), you can run
|
||||
predefined docker-compose service. Simply execute the next commands:
|
||||
|
||||
```sh
|
||||
cp docker-compose.dev.yml docker-compose.yml
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Если нужно пересобрать весь контейнер, то выполняем следующее:
|
||||
If your Redis instance isn't located at the `localhost`, you can change host by editing environment variable
|
||||
`STORAGE_REDIS_HOST`.
|
||||
|
||||
```
|
||||
docker-compose stop app # Останавливаем конейтнер, если он ещё работает
|
||||
docker-compose rm -f app # Удаляем конейтнер
|
||||
docker-compose build app # Запускаем билд по новой
|
||||
docker-compose up -d app # Поднимаем свежесобранный контейнер обратно
|
||||
```
|
||||
After all of that `go run main.go serve` should successfully start the application.
|
||||
To run tests execute `go test ./...`. If your Go version is older than 1.9, then run a `/script/test`.
|
||||
|
||||
82
auth/jwt.go
Normal file
82
auth/jwt.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SermoDigital/jose/crypto"
|
||||
"github.com/SermoDigital/jose/jws"
|
||||
)
|
||||
|
||||
var hashAlg = crypto.SigningMethodHS256
|
||||
|
||||
const scopesClaim = "scopes"
|
||||
|
||||
type Scope string
|
||||
|
||||
var (
|
||||
SkinScope = Scope("skin")
|
||||
)
|
||||
|
||||
type JwtAuth struct {
|
||||
Key []byte
|
||||
}
|
||||
|
||||
func (t *JwtAuth) NewToken(scopes ...Scope) ([]byte, error) {
|
||||
if len(t.Key) == 0 {
|
||||
return nil, errors.New("signing key not available")
|
||||
}
|
||||
|
||||
claims := jws.Claims{}
|
||||
claims.Set(scopesClaim, scopes)
|
||||
claims.SetIssuedAt(time.Now())
|
||||
encoder := jws.NewJWT(claims, hashAlg)
|
||||
token, err := encoder.Serialize(t.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (t *JwtAuth) Check(req *http.Request) error {
|
||||
if len(t.Key) == 0 {
|
||||
return &Unauthorized{"Signing key not set"}
|
||||
}
|
||||
|
||||
bearerToken := req.Header.Get("Authorization")
|
||||
if bearerToken == "" {
|
||||
return &Unauthorized{"Authentication header not presented"}
|
||||
}
|
||||
|
||||
if !strings.EqualFold(bearerToken[0:7], "BEARER ") {
|
||||
return &Unauthorized{"Cannot recognize JWT token in passed value"}
|
||||
}
|
||||
|
||||
tokenStr := bearerToken[7:]
|
||||
token, err := jws.ParseJWT([]byte(tokenStr))
|
||||
if err != nil {
|
||||
return &Unauthorized{"Cannot parse passed JWT token"}
|
||||
}
|
||||
|
||||
err = token.Validate(t.Key, hashAlg)
|
||||
if err != nil {
|
||||
return &Unauthorized{"JWT token have invalid signature. It may be corrupted or expired."}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Unauthorized struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e *Unauthorized) Error() string {
|
||||
if e.Reason != "" {
|
||||
return e.Reason
|
||||
}
|
||||
|
||||
return "Unauthorized"
|
||||
}
|
||||
97
auth/jwt_test.go
Normal file
97
auth/jwt_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNTE2NjU4MTkzIiwic2NvcGVzIjoic2tpbiJ9.agbBS0qdyYMBaVfTZJAZcTTRgW1Y0kZty4H3N2JHBO8"
|
||||
|
||||
func TestJwtAuth_NewToken_Success(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
jwt := &JwtAuth{[]byte("secret")}
|
||||
token, err := jwt.NewToken(SkinScope)
|
||||
assert.Nil(err)
|
||||
assert.NotNil(token)
|
||||
}
|
||||
|
||||
func TestJwtAuth_NewToken_KeyNotAvailable(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
jwt := &JwtAuth{}
|
||||
token, err := jwt.NewToken(SkinScope)
|
||||
assert.Error(err, "signing key not available")
|
||||
assert.Nil(token)
|
||||
}
|
||||
|
||||
func TestJwtAuth_Check_EmptyRequest(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||
jwt := &JwtAuth{[]byte("secret")}
|
||||
|
||||
err := jwt.Check(req)
|
||||
assert.IsType(&Unauthorized{}, err)
|
||||
assert.EqualError(err, "Authentication header not presented")
|
||||
}
|
||||
|
||||
func TestJwtAuth_Check_NonBearer(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||
req.Header.Add("Authorization", "this is not jwt")
|
||||
jwt := &JwtAuth{[]byte("secret")}
|
||||
|
||||
err := jwt.Check(req)
|
||||
assert.IsType(&Unauthorized{}, err)
|
||||
assert.EqualError(err, "Cannot recognize JWT token in passed value")
|
||||
}
|
||||
|
||||
func TestJwtAuth_Check_BearerButNotJwt(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||
req.Header.Add("Authorization", "Bearer thisIs.Not.Jwt")
|
||||
jwt := &JwtAuth{[]byte("secret")}
|
||||
|
||||
err := jwt.Check(req)
|
||||
assert.IsType(&Unauthorized{}, err)
|
||||
assert.EqualError(err, "Cannot parse passed JWT token")
|
||||
}
|
||||
|
||||
func TestJwtAuth_Check_SecretNotAvailable(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||
req.Header.Add("Authorization", "Bearer " + jwt)
|
||||
jwt := &JwtAuth{}
|
||||
|
||||
err := jwt.Check(req)
|
||||
assert.Error(err, "Signing key not set")
|
||||
}
|
||||
|
||||
func TestJwtAuth_Check_SecretInvalid(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||
req.Header.Add("Authorization", "Bearer " + jwt)
|
||||
jwt := &JwtAuth{[]byte("this is another secret")}
|
||||
|
||||
err := jwt.Check(req)
|
||||
assert.IsType(&Unauthorized{}, err)
|
||||
assert.EqualError(err, "JWT token have invalid signature. It may be corrupted or expired.")
|
||||
}
|
||||
|
||||
func TestJwtAuth_Check_Valid(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||
req.Header.Add("Authorization", "Bearer " + jwt)
|
||||
jwt := &JwtAuth{[]byte("secret")}
|
||||
|
||||
err := jwt.Check(req)
|
||||
assert.Nil(err)
|
||||
}
|
||||
64
bootstrap/bootstrap.go
Normal file
64
bootstrap/bootstrap.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/mono83/slf/rays"
|
||||
"github.com/mono83/slf/recievers/sentry"
|
||||
"github.com/mono83/slf/recievers/statsd"
|
||||
"github.com/mono83/slf/recievers/writer"
|
||||
"github.com/mono83/slf/wd"
|
||||
)
|
||||
|
||||
var version = ""
|
||||
|
||||
func GetVersion() string {
|
||||
return version
|
||||
}
|
||||
|
||||
func CreateLogger(statsdAddr string, sentryAddr string) (wd.Watchdog, error) {
|
||||
wd.AddReceiver(writer.New(writer.Options{
|
||||
Marker: false,
|
||||
TimeFormat: "15:04:05.000",
|
||||
}))
|
||||
if statsdAddr != "" {
|
||||
hostname, _ := os.Hostname()
|
||||
statsdReceiver, err := statsd.NewReceiver(statsd.Config{
|
||||
Address: statsdAddr,
|
||||
Prefix: "ely.skinsystem." + hostname + ".app.",
|
||||
FlushEvery: 1,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wd.AddReceiver(statsdReceiver)
|
||||
}
|
||||
|
||||
if sentryAddr != "" {
|
||||
ravenClient, err := raven.New(sentryAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ravenClient.SetEnvironment("production")
|
||||
ravenClient.SetDefaultLoggerName("sentry-watchdog-receiver")
|
||||
programVersion := GetVersion()
|
||||
if programVersion != "" {
|
||||
raven.SetRelease(programVersion)
|
||||
}
|
||||
|
||||
sentryReceiver, err := sentry.NewReceiverWithCustomRaven(ravenClient, &sentry.Config{
|
||||
MinLevel: "warn",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wd.AddReceiver(sentryReceiver)
|
||||
}
|
||||
|
||||
return wd.New("", "").WithParams(rays.Host), nil
|
||||
}
|
||||
37
cmd/root.go
Normal file
37
cmd/root.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/elyby/chrly/bootstrap"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "chrly",
|
||||
Short: "Implementation of Minecraft skins system server",
|
||||
Version: bootstrap.GetVersion(),
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := RootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
viper.AutomaticEnv()
|
||||
replacer := strings.NewReplacer(".", "_")
|
||||
viper.SetEnvKeyReplacer(replacer)
|
||||
}
|
||||
68
cmd/serve.go
Normal file
68
cmd/serve.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/elyby/chrly/auth"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/elyby/chrly/bootstrap"
|
||||
"github.com/elyby/chrly/db"
|
||||
"github.com/elyby/chrly/http"
|
||||
)
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Starts http handler for the skins system",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logger, err := bootstrap.CreateLogger(viper.GetString("statsd.addr"), viper.GetString("sentry.dsn"))
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Printf("Cannot initialize logger: %v", err))
|
||||
}
|
||||
logger.Info("Logger successfully initialized")
|
||||
|
||||
storageFactory := db.StorageFactory{Config: viper.GetViper()}
|
||||
|
||||
logger.Info("Initializing skins repository")
|
||||
skinsRepo, err := storageFactory.CreateFactory("redis").CreateSkinsRepository()
|
||||
if err != nil {
|
||||
logger.Emergency(fmt.Sprintf("Error on creating skins repo: %+v", err))
|
||||
return
|
||||
}
|
||||
logger.Info("Skins repository successfully initialized")
|
||||
|
||||
logger.Info("Initializing capes repository")
|
||||
capesRepo, err := storageFactory.CreateFactory("filesystem").CreateCapesRepository()
|
||||
if err != nil {
|
||||
logger.Emergency(fmt.Sprintf("Error on creating capes repo: %v", err))
|
||||
return
|
||||
}
|
||||
logger.Info("Capes repository successfully initialized")
|
||||
|
||||
cfg := &http.Config{
|
||||
ListenSpec: fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port")),
|
||||
SkinsRepo: skinsRepo,
|
||||
CapesRepo: capesRepo,
|
||||
Logger: logger,
|
||||
Auth: &auth.JwtAuth{Key: []byte(viper.GetString("chrly.secret"))},
|
||||
}
|
||||
|
||||
if err := cfg.Run(); err != nil {
|
||||
logger.Error(fmt.Sprintf("Error in main(): %v", err))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(serveCmd)
|
||||
viper.SetDefault("server.host", "")
|
||||
viper.SetDefault("server.port", 80)
|
||||
viper.SetDefault("storage.redis.host", "localhost")
|
||||
viper.SetDefault("storage.redis.port", 6379)
|
||||
viper.SetDefault("storage.redis.poll", 10)
|
||||
viper.SetDefault("storage.filesystem.basePath", "data")
|
||||
viper.SetDefault("storage.filesystem.capesDirName", "capes")
|
||||
}
|
||||
29
cmd/token.go
Normal file
29
cmd/token.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/elyby/chrly/auth"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var tokenCmd = &cobra.Command{
|
||||
Use: "token",
|
||||
Short: "Creates a new token, which allows to interact with Chrly API",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jwtAuth := &auth.JwtAuth{Key: []byte(viper.GetString("chrly.secret"))}
|
||||
token, err := jwtAuth.NewToken(auth.SkinScope)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create new token. The error is %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", token)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(tokenCmd)
|
||||
}
|
||||
23
cmd/version.go
Normal file
23
cmd/version.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/elyby/chrly/bootstrap"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show the Chrly version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Version: %s\n", bootstrap.GetVersion())
|
||||
fmt.Printf("Go version: %s\n", runtime.Version())
|
||||
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
2
data/capes/.gitignore
vendored
Normal file
2
data/capes/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
25
db/commons.go
Normal file
25
db/commons.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package db
|
||||
|
||||
type ParamRequired struct {
|
||||
Param string
|
||||
}
|
||||
|
||||
func (e ParamRequired) Error() string {
|
||||
return "Required parameter not provided"
|
||||
}
|
||||
|
||||
type SkinNotFoundError struct {
|
||||
Who string
|
||||
}
|
||||
|
||||
func (e SkinNotFoundError) Error() string {
|
||||
return "Skin data not found."
|
||||
}
|
||||
|
||||
type CapeNotFoundError struct {
|
||||
Who string
|
||||
}
|
||||
|
||||
func (e CapeNotFoundError) Error() string {
|
||||
return "Cape file not found."
|
||||
}
|
||||
34
db/factory.go
Normal file
34
db/factory.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/elyby/chrly/interfaces"
|
||||
)
|
||||
|
||||
type StorageFactory struct {
|
||||
Config *viper.Viper
|
||||
}
|
||||
|
||||
type RepositoriesCreator interface {
|
||||
CreateSkinsRepository() (interfaces.SkinsRepository, error)
|
||||
CreateCapesRepository() (interfaces.CapesRepository, error)
|
||||
}
|
||||
|
||||
func (factory *StorageFactory) CreateFactory(backend string) RepositoriesCreator {
|
||||
switch backend {
|
||||
case "redis":
|
||||
return &RedisFactory{
|
||||
Host: factory.Config.GetString("storage.redis.host"),
|
||||
Port: factory.Config.GetInt("storage.redis.port"),
|
||||
PoolSize: factory.Config.GetInt("storage.redis.poolSize"),
|
||||
}
|
||||
case "filesystem":
|
||||
return &FilesystemFactory{
|
||||
BasePath : factory.Config.GetString("storage.filesystem.basePath"),
|
||||
CapesDirName: factory.Config.GetString("storage.filesystem.capesDirName"),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
59
db/filesystem.go
Normal file
59
db/filesystem.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/elyby/chrly/interfaces"
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
type FilesystemFactory struct {
|
||||
BasePath string
|
||||
CapesDirName string
|
||||
}
|
||||
|
||||
func (f FilesystemFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) {
|
||||
panic("skins repository not supported for this storage type")
|
||||
}
|
||||
|
||||
func (f FilesystemFactory) CreateCapesRepository() (interfaces.CapesRepository, error) {
|
||||
if err := f.validateFactoryConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &filesStorage{path: path.Join(f.BasePath, f.CapesDirName)}, nil
|
||||
}
|
||||
|
||||
func (f FilesystemFactory) validateFactoryConfig() error {
|
||||
if f.BasePath == "" {
|
||||
return &ParamRequired{"basePath"}
|
||||
}
|
||||
|
||||
if f.CapesDirName == "" {
|
||||
f.CapesDirName = "capes"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type filesStorage struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (repository *filesStorage) FindByUsername(username string) (*model.Cape, error) {
|
||||
if username == "" {
|
||||
return nil, &CapeNotFoundError{username}
|
||||
}
|
||||
|
||||
capePath := path.Join(repository.path, strings.ToLower(username) + ".png")
|
||||
file, err := os.Open(capePath)
|
||||
if err != nil {
|
||||
return nil, &CapeNotFoundError{username}
|
||||
}
|
||||
|
||||
return &model.Cape{
|
||||
File: file,
|
||||
}, nil
|
||||
}
|
||||
249
db/redis.go
Normal file
249
db/redis.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/radix.v2/pool"
|
||||
"github.com/mediocregopher/radix.v2/redis"
|
||||
"github.com/mediocregopher/radix.v2/util"
|
||||
|
||||
"github.com/elyby/chrly/interfaces"
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
type RedisFactory struct {
|
||||
Host string
|
||||
Port int
|
||||
PoolSize int
|
||||
connection *pool.Pool
|
||||
}
|
||||
|
||||
// TODO: maybe we should manually return connection to the pool?
|
||||
|
||||
func (f RedisFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) {
|
||||
connection, err := f.getConnection()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &redisDb{connection}, nil
|
||||
}
|
||||
|
||||
func (f RedisFactory) CreateCapesRepository() (interfaces.CapesRepository, error) {
|
||||
panic("capes repository not supported for this storage type")
|
||||
}
|
||||
|
||||
func (f RedisFactory) getConnection() (*pool.Pool, error) {
|
||||
if f.connection == nil {
|
||||
if f.Host == "" {
|
||||
return nil, &ParamRequired{"host"}
|
||||
}
|
||||
|
||||
if f.Port == 0 {
|
||||
return nil, &ParamRequired{"port"}
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", f.Host, f.Port)
|
||||
conn, err := pool.New("tcp", addr, f.PoolSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f.connection = conn
|
||||
|
||||
go func() {
|
||||
period := 5
|
||||
for {
|
||||
time.Sleep(time.Duration(period) * time.Second)
|
||||
resp := f.connection.Cmd("PING")
|
||||
if resp.Err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Println("Redis not pinged. Try to reconnect")
|
||||
conn, err := pool.New("tcp", addr, f.PoolSize)
|
||||
if err != nil {
|
||||
log.Printf("Cannot reconnect to redis: %v\n", err)
|
||||
log.Printf("Waiting %d seconds to retry\n", period)
|
||||
continue
|
||||
}
|
||||
|
||||
f.connection = conn
|
||||
log.Println("Reconnected")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return f.connection, nil
|
||||
}
|
||||
|
||||
type redisDb struct {
|
||||
conn *pool.Pool
|
||||
}
|
||||
|
||||
const accountIdToUsernameKey = "hash:username-to-account-id"
|
||||
|
||||
func (db *redisDb) FindByUsername(username string) (*model.Skin, error) {
|
||||
return findByUsername(username, db.getConn())
|
||||
}
|
||||
|
||||
func (db *redisDb) FindByUserId(id int) (*model.Skin, error) {
|
||||
return findByUserId(id, db.getConn())
|
||||
}
|
||||
|
||||
func (db *redisDb) Save(skin *model.Skin) error {
|
||||
return save(skin, db.getConn())
|
||||
}
|
||||
|
||||
func (db *redisDb) RemoveByUserId(id int) error {
|
||||
return removeByUserId(id, db.getConn())
|
||||
}
|
||||
|
||||
func (db *redisDb) RemoveByUsername(username string) error {
|
||||
return removeByUsername(username, db.getConn())
|
||||
}
|
||||
|
||||
func (db *redisDb) getConn() util.Cmder {
|
||||
conn, _ := db.conn.Get()
|
||||
return conn
|
||||
}
|
||||
|
||||
func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
|
||||
if username == "" {
|
||||
return nil, &SkinNotFoundError{username}
|
||||
}
|
||||
|
||||
redisKey := buildUsernameKey(username)
|
||||
response := conn.Cmd("GET", redisKey)
|
||||
if response.IsType(redis.Nil) {
|
||||
return nil, &SkinNotFoundError{username}
|
||||
}
|
||||
|
||||
encodedResult, err := response.Bytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := zlibDecode(encodedResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var skin *model.Skin
|
||||
err = json.Unmarshal(result, &skin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skin.OldUsername = skin.Username
|
||||
|
||||
return skin, nil
|
||||
}
|
||||
|
||||
func findByUserId(id int, conn util.Cmder) (*model.Skin, error) {
|
||||
response := conn.Cmd("HGET", accountIdToUsernameKey, id)
|
||||
if response.IsType(redis.Nil) {
|
||||
return nil, &SkinNotFoundError{"unknown"}
|
||||
}
|
||||
|
||||
username, _ := response.Str()
|
||||
|
||||
return findByUsername(username, conn)
|
||||
}
|
||||
|
||||
func removeByUserId(id int, conn util.Cmder) error {
|
||||
record, err := findByUserId(id, conn)
|
||||
if err != nil {
|
||||
if _, ok := err.(*SkinNotFoundError); !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
conn.Cmd("MULTI")
|
||||
|
||||
conn.Cmd("HDEL", accountIdToUsernameKey, id)
|
||||
if record != nil {
|
||||
conn.Cmd("DEL", buildUsernameKey(record.Username))
|
||||
}
|
||||
|
||||
conn.Cmd("EXEC")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeByUsername(username string, conn util.Cmder) error {
|
||||
record, err := findByUsername(username, conn)
|
||||
if err != nil {
|
||||
if _, ok := err.(*SkinNotFoundError); !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
conn.Cmd("MULTI")
|
||||
|
||||
conn.Cmd("DEL", buildUsernameKey(record.Username))
|
||||
if record != nil {
|
||||
conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId)
|
||||
}
|
||||
|
||||
conn.Cmd("EXEC")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func save(skin *model.Skin, conn util.Cmder) error {
|
||||
conn.Cmd("MULTI")
|
||||
|
||||
// If user has changed username, then we must delete his old username record
|
||||
if skin.OldUsername != "" && skin.OldUsername != skin.Username {
|
||||
conn.Cmd("DEL", buildUsernameKey(skin.OldUsername))
|
||||
}
|
||||
|
||||
// If this is a new record or if the user has changed username, we set the value in the hash table
|
||||
if skin.OldUsername != "" || skin.OldUsername != skin.Username {
|
||||
conn.Cmd("HSET", accountIdToUsernameKey, skin.UserId, skin.Username)
|
||||
}
|
||||
|
||||
str, _ := json.Marshal(skin)
|
||||
conn.Cmd("SET", buildUsernameKey(skin.Username), zlibEncode(str))
|
||||
|
||||
conn.Cmd("EXEC")
|
||||
|
||||
skin.OldUsername = skin.Username
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildUsernameKey(username string) string {
|
||||
return "username:" + strings.ToLower(username)
|
||||
}
|
||||
|
||||
func zlibEncode(str []byte) []byte {
|
||||
var buff bytes.Buffer
|
||||
writer := zlib.NewWriter(&buff)
|
||||
writer.Write(str)
|
||||
writer.Close()
|
||||
|
||||
return buff.Bytes()
|
||||
}
|
||||
|
||||
func zlibDecode(bts []byte) ([]byte, error) {
|
||||
buff := bytes.NewReader(bts)
|
||||
reader, readError := zlib.NewReader(buff)
|
||||
if readError != nil {
|
||||
return nil, readError
|
||||
}
|
||||
|
||||
resultBuffer := new(bytes.Buffer)
|
||||
io.Copy(resultBuffer, reader)
|
||||
reader.Close()
|
||||
|
||||
return resultBuffer.Bytes(), nil
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
version: '2'
|
||||
services:
|
||||
app:
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
redis:
|
||||
image: redis:3.0
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
@@ -1,17 +1,14 @@
|
||||
# This file can be used to start up necessary services.
|
||||
# Copy it into the docker-compose.yml:
|
||||
# > cp docker-compose.dev.yml docker-compose.yml
|
||||
# And then run it:
|
||||
# > docker-compose up -d
|
||||
|
||||
version: '2'
|
||||
services:
|
||||
app:
|
||||
extends:
|
||||
file: docker-compose.base.yml
|
||||
service: app
|
||||
build: .
|
||||
volumes:
|
||||
- ./:/go/src/app
|
||||
command: ["go", "run", "minecraft-skinsystem.go"]
|
||||
links:
|
||||
- redis
|
||||
|
||||
redis:
|
||||
extends:
|
||||
file: docker-compose.base.yml
|
||||
service: redis
|
||||
redis:
|
||||
image: redis:4.0-32bit
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
# This file can be used to run application in the production environment.
|
||||
# Copy it into the docker-compose.yml:
|
||||
# > cp docker-compose.prod.yml docker-compose.yml
|
||||
# And then run it:
|
||||
# > docker-compose up -d
|
||||
# Service will be listened at the http://localhost
|
||||
|
||||
version: '2'
|
||||
services:
|
||||
app:
|
||||
extends:
|
||||
file: docker-compose.base.yml
|
||||
service: app
|
||||
image: erickskrauch/ely-by-skinsystem:master
|
||||
links:
|
||||
- redis
|
||||
restart: always
|
||||
app:
|
||||
image: elyby/chrly
|
||||
hostname: chrly0
|
||||
restart: always
|
||||
links:
|
||||
- redis
|
||||
volumes:
|
||||
- ./data/capes:/data/capes
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
CHRLY_SECRET: replace_this_value_in_production
|
||||
|
||||
redis:
|
||||
extends:
|
||||
file: docker-compose.base.yml
|
||||
service: redis
|
||||
restart: always
|
||||
redis:
|
||||
image: redis:4.0-32bit # 32-bit version is recommended to spare some memory
|
||||
restart: always
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
|
||||
12
docker-entrypoint.sh
Executable file
12
docker-entrypoint.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ ! -d /data/capes ]; then
|
||||
mkdir -p /data/capes
|
||||
fi
|
||||
|
||||
if [ "$1" = "serve" ] || [ "$1" = "token" ] || [ "$1" = "version" ]; then
|
||||
set -- /usr/local/bin/chrly "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
256
http/api.go
Normal file
256
http/api.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/elyby/chrly/auth"
|
||||
"github.com/elyby/chrly/db"
|
||||
"github.com/elyby/chrly/interfaces"
|
||||
"github.com/elyby/chrly/model"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mono83/slf/wd"
|
||||
"github.com/thedevsaddam/govalidator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
govalidator.AddCustomRule("md5", func(field string, rule string, message string, value interface{}) error {
|
||||
val := []byte(value.(string))
|
||||
if ok, _ := regexp.Match(`^[a-f0-9]{32}$`, val); !ok {
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("The %s field must be a valid md5 hash", field)
|
||||
}
|
||||
|
||||
return errors.New(message)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error {
|
||||
if message == "" {
|
||||
message = "Skin uploading is temporary unavailable"
|
||||
}
|
||||
|
||||
return errors.New(message)
|
||||
})
|
||||
}
|
||||
|
||||
func (cfg *Config) PostSkin(resp http.ResponseWriter, req *http.Request) {
|
||||
cfg.Logger.IncCounter("api.skins.post.request", 1)
|
||||
validationErrors := validatePostSkinRequest(req)
|
||||
if validationErrors != nil {
|
||||
cfg.Logger.IncCounter("api.skins.post.validation_failed", 1)
|
||||
apiBadRequest(resp, validationErrors)
|
||||
return
|
||||
}
|
||||
|
||||
identityId, _ := strconv.Atoi(req.Form.Get("identityId"))
|
||||
username := req.Form.Get("username")
|
||||
|
||||
record, err := findIdentity(cfg.SkinsRepo, identityId, username)
|
||||
if err != nil {
|
||||
cfg.Logger.Error("Error on requesting a skin from the repository: :err", wd.ErrParam(err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
}
|
||||
|
||||
skinId, _ := strconv.Atoi(req.Form.Get("skinId"))
|
||||
is18, _ := strconv.ParseBool(req.Form.Get("is1_8"))
|
||||
isSlim, _ := strconv.ParseBool(req.Form.Get("isSlim"))
|
||||
|
||||
record.Uuid = req.Form.Get("uuid")
|
||||
record.SkinId = skinId
|
||||
record.Hash = req.Form.Get("hash")
|
||||
record.Is1_8 = is18
|
||||
record.IsSlim = isSlim
|
||||
record.Url = req.Form.Get("url")
|
||||
record.MojangTextures = req.Form.Get("mojangTextures")
|
||||
record.MojangSignature = req.Form.Get("mojangSignature")
|
||||
|
||||
err = cfg.SkinsRepo.Save(record)
|
||||
if err != nil {
|
||||
cfg.Logger.Error("Unable to save record to the repository: :err", wd.ErrParam(err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
}
|
||||
|
||||
cfg.Logger.IncCounter("api.skins.post.success", 1)
|
||||
resp.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (cfg *Config) DeleteSkinByUserId(resp http.ResponseWriter, req *http.Request) {
|
||||
cfg.Logger.IncCounter("api.skins.delete.request", 1)
|
||||
id, _ := strconv.Atoi(mux.Vars(req)["id"])
|
||||
skin, err := cfg.SkinsRepo.FindByUserId(id)
|
||||
if err != nil {
|
||||
cfg.Logger.IncCounter("api.skins.delete.not_found", 1)
|
||||
apiNotFound(resp, "Cannot find record for requested user id")
|
||||
return
|
||||
}
|
||||
|
||||
cfg.deleteSkin(skin, resp)
|
||||
}
|
||||
|
||||
func (cfg *Config) DeleteSkinByUsername(resp http.ResponseWriter, req *http.Request) {
|
||||
cfg.Logger.IncCounter("api.skins.delete.request", 1)
|
||||
username := mux.Vars(req)["username"]
|
||||
skin, err := cfg.SkinsRepo.FindByUsername(username)
|
||||
if err != nil {
|
||||
cfg.Logger.IncCounter("api.skins.delete.not_found", 1)
|
||||
apiNotFound(resp, "Cannot find record for requested username")
|
||||
return
|
||||
}
|
||||
|
||||
cfg.deleteSkin(skin, resp)
|
||||
}
|
||||
|
||||
func (cfg *Config) Authenticate(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
cfg.Logger.IncCounter("authentication.challenge", 1)
|
||||
err := cfg.Auth.Check(req)
|
||||
if err != nil {
|
||||
if _, ok := err.(*auth.Unauthorized); ok {
|
||||
cfg.Logger.IncCounter("authentication.failed", 1)
|
||||
apiForbidden(resp, err.Error())
|
||||
} else {
|
||||
cfg.Logger.Error("Unknown error on validating api request: :err", wd.ErrParam(err))
|
||||
apiServerError(resp)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cfg.Logger.IncCounter("authentication.success", 1)
|
||||
handler.ServeHTTP(resp, req)
|
||||
})
|
||||
}
|
||||
|
||||
func (cfg *Config) deleteSkin(skin *model.Skin, resp http.ResponseWriter) {
|
||||
err := cfg.SkinsRepo.RemoveByUserId(skin.UserId)
|
||||
if err != nil {
|
||||
cfg.Logger.Error("Cannot delete skin by error: :err", wd.ErrParam(err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
}
|
||||
|
||||
cfg.Logger.IncCounter("api.skins.delete.success", 1)
|
||||
resp.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func validatePostSkinRequest(request *http.Request) map[string][]string {
|
||||
const maxMultipartMemory int64 = 32 << 20
|
||||
const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both"
|
||||
|
||||
request.ParseMultipartForm(maxMultipartMemory)
|
||||
|
||||
validationRules := govalidator.MapData{
|
||||
"identityId": {"required", "numeric", "min:1"},
|
||||
"username": {"required"},
|
||||
"uuid": {"required", "uuid"},
|
||||
"skinId": {"required", "numeric", "min:1"},
|
||||
"url": {"url"},
|
||||
"file:skin": {"ext:png", "size:24576", "mime:image/png"},
|
||||
"hash": {"md5"},
|
||||
"is1_8": {"bool"},
|
||||
"isSlim": {"bool"},
|
||||
}
|
||||
|
||||
shouldAppendSkinRequiredError := false
|
||||
url := request.Form.Get("url")
|
||||
_, _, skinErr := request.FormFile("skin")
|
||||
if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) {
|
||||
shouldAppendSkinRequiredError = true
|
||||
} else if skinErr == nil {
|
||||
validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable")
|
||||
} else if url != "" {
|
||||
validationRules["hash"] = append(validationRules["hash"], "required")
|
||||
validationRules["is1_8"] = append(validationRules["is1_8"], "required")
|
||||
validationRules["isSlim"] = append(validationRules["isSlim"], "required")
|
||||
}
|
||||
|
||||
mojangTextures := request.Form.Get("mojangTextures")
|
||||
if mojangTextures != "" {
|
||||
validationRules["mojangSignature"] = []string{"required"}
|
||||
}
|
||||
|
||||
validator := govalidator.New(govalidator.Options{
|
||||
Request: request,
|
||||
Rules: validationRules,
|
||||
RequiredDefault: false,
|
||||
FormSize: maxMultipartMemory,
|
||||
})
|
||||
validationResults := validator.Validate()
|
||||
if shouldAppendSkinRequiredError {
|
||||
validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage)
|
||||
validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage)
|
||||
}
|
||||
|
||||
if len(validationResults) != 0 {
|
||||
return validationResults
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findIdentity(repo interfaces.SkinsRepository, identityId int, username string) (*model.Skin, error) {
|
||||
var record *model.Skin
|
||||
record, err := repo.FindByUserId(identityId)
|
||||
if err != nil {
|
||||
if _, isSkinNotFound := err.(*db.SkinNotFoundError); !isSkinNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
record, err = repo.FindByUsername(username)
|
||||
if err == nil {
|
||||
repo.RemoveByUsername(username)
|
||||
record.UserId = identityId
|
||||
} else {
|
||||
record = &model.Skin{
|
||||
UserId: identityId,
|
||||
Username: username,
|
||||
}
|
||||
}
|
||||
} else if record.Username != username {
|
||||
repo.RemoveByUserId(identityId)
|
||||
record.Username = username
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func apiBadRequest(resp http.ResponseWriter, errorsPerField map[string][]string) {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
resp.Header().Set("Content-Type", "application/json")
|
||||
result, _ := json.Marshal(map[string]interface{}{
|
||||
"errors": errorsPerField,
|
||||
})
|
||||
resp.Write(result)
|
||||
}
|
||||
|
||||
func apiForbidden(resp http.ResponseWriter, reason string) {
|
||||
resp.WriteHeader(http.StatusForbidden)
|
||||
resp.Header().Set("Content-Type", "application/json")
|
||||
result, _ := json.Marshal(map[string]interface{}{
|
||||
"error": reason,
|
||||
})
|
||||
resp.Write(result)
|
||||
}
|
||||
|
||||
func apiNotFound(resp http.ResponseWriter, reason string) {
|
||||
resp.WriteHeader(http.StatusNotFound)
|
||||
resp.Header().Set("Content-Type", "application/json")
|
||||
result, _ := json.Marshal([]interface{}{
|
||||
reason,
|
||||
})
|
||||
resp.Write(result)
|
||||
}
|
||||
|
||||
func apiServerError(resp http.ResponseWriter) {
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
505
http/api_test.go
Normal file
505
http/api_test.go
Normal file
@@ -0,0 +1,505 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/elyby/chrly/auth"
|
||||
"github.com/elyby/chrly/db"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfig_PostSkin_Valid(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
resultModel := createSkinModel("mock_user", false)
|
||||
resultModel.SkinId = 5
|
||||
resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a"
|
||||
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
|
||||
resultModel.MojangTextures = ""
|
||||
resultModel.MojangSignature = ""
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||
mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
|
||||
|
||||
form := url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"mock_user"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://ely.by/minecraft/skins/default.png"},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(201, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Empty(response)
|
||||
}
|
||||
|
||||
func TestConfig_PostSkin_ChangedIdentityId(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
resultModel := createSkinModel("mock_user", false)
|
||||
resultModel.UserId = 2
|
||||
resultModel.SkinId = 5
|
||||
resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a"
|
||||
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
|
||||
resultModel.MojangTextures = ""
|
||||
resultModel.MojangSignature = ""
|
||||
|
||||
form := url.Values{
|
||||
"identityId": {"2"},
|
||||
"username": {"mock_user"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://ely.by/minecraft/skins/default.png"},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||
mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{"unknown"})
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Skins.EXPECT().RemoveByUsername("mock_user").Return(nil)
|
||||
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(201, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Empty(response)
|
||||
}
|
||||
|
||||
func TestConfig_PostSkin_ChangedUsername(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
resultModel := createSkinModel("changed_username", false)
|
||||
resultModel.SkinId = 5
|
||||
resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a"
|
||||
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
|
||||
resultModel.MojangTextures = ""
|
||||
resultModel.MojangSignature = ""
|
||||
|
||||
form := url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"changed_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://ely.by/minecraft/skins/default.png"},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||
mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil)
|
||||
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(201, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Empty(response)
|
||||
}
|
||||
|
||||
func TestConfig_PostSkin_CompletelyNewIdentity(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
resultModel := createSkinModel("mock_user", false)
|
||||
resultModel.SkinId = 5
|
||||
resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a"
|
||||
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
|
||||
resultModel.MojangTextures = ""
|
||||
resultModel.MojangSignature = ""
|
||||
|
||||
form := url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"mock_user"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://ely.by/minecraft/skins/default.png"},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||
mocks.Skins.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{"unknown"})
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"})
|
||||
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(201, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Empty(response)
|
||||
}
|
||||
|
||||
func TestConfig_PostSkin_UploadSkin(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, _ := writer.CreateFormFile("skin", "char.png")
|
||||
part.Write(loadSkinFile())
|
||||
|
||||
_ = writer.WriteField("identityId", "1")
|
||||
_ = writer.WriteField("username", "mock_user")
|
||||
_ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3")
|
||||
_ = writer.WriteField("skinId", "5")
|
||||
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", body)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.validation_failed", int64(1))
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(400, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`{
|
||||
"errors": {
|
||||
"skin": [
|
||||
"Skin uploading is temporary unavailable"
|
||||
]
|
||||
}
|
||||
}`, string(response))
|
||||
}
|
||||
|
||||
func TestConfig_PostSkin_RequiredFields(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
form := url.Values{
|
||||
"hash": {"this is not md5"},
|
||||
"mojangTextures": {"someBase64EncodedString"},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.post.validation_failed", int64(1))
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(400, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`{
|
||||
"errors": {
|
||||
"identityId": [
|
||||
"The identityId field is required",
|
||||
"The identityId field must be numeric",
|
||||
"The identityId field must be minimum 1 char"
|
||||
],
|
||||
"skinId": [
|
||||
"The skinId field is required",
|
||||
"The skinId field must be numeric",
|
||||
"The skinId field must be minimum 1 char"
|
||||
],
|
||||
"username": [
|
||||
"The username field is required"
|
||||
],
|
||||
"uuid": [
|
||||
"The uuid field is required",
|
||||
"The uuid field must contain valid UUID"
|
||||
],
|
||||
"hash": [
|
||||
"The hash field must be a valid md5 hash"
|
||||
],
|
||||
"url": [
|
||||
"One of url or skin should be provided, but not both"
|
||||
],
|
||||
"skin": [
|
||||
"One of url or skin should be provided, but not both"
|
||||
],
|
||||
"mojangSignature": [
|
||||
"The mojangSignature field is required"
|
||||
]
|
||||
}
|
||||
}`, string(response))
|
||||
}
|
||||
|
||||
func TestConfig_PostSkin_Unauthorized(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", nil)
|
||||
req.Header.Add("Authorization", "Bearer invalid.jwt.token")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"Cannot parse passed JWT token"})
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1))
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(403, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`{
|
||||
"error": "Cannot parse passed JWT token"
|
||||
}`, string(response))
|
||||
}
|
||||
|
||||
func TestConfig_DeleteSkinByUserId_Success(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||
mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil)
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.delete.success", int64(1))
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(204, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Empty(response)
|
||||
}
|
||||
|
||||
func TestConfig_DeleteSkinByUserId_NotFound(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:2", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||
mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{"unknown"})
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.delete.not_found", int64(1))
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(404, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`[
|
||||
"Cannot find record for requested user id"
|
||||
]`, string(response))
|
||||
}
|
||||
|
||||
func TestConfig_DeleteSkinByUsername_Success(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/mock_user", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil)
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.delete.success", int64(1))
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(204, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Empty(response)
|
||||
}
|
||||
|
||||
func TestConfig_DeleteSkinByUsername_NotFound(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/mock_user_2", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user_2").Return(nil, &db.SkinNotFoundError{"mock_user_2"})
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("api.skins.delete.not_found", int64(1))
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(404, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`[
|
||||
"Cannot find record for requested username"
|
||||
]`, string(response))
|
||||
}
|
||||
|
||||
func TestConfig_Authenticate_SignatureKeyNotSet(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://localhost", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"signing key not available"})
|
||||
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1))
|
||||
|
||||
res := config.Authenticate(http.HandlerFunc(func (resp http.ResponseWriter, req *http.Request) {}))
|
||||
res.ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(403, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`{
|
||||
"error": "signing key not available"
|
||||
}`, string(response))
|
||||
}
|
||||
|
||||
// base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png
|
||||
var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==")
|
||||
|
||||
func loadSkinFile() []byte {
|
||||
result := make([]byte, 92)
|
||||
_, err := base64.StdEncoding.Decode(result, OnePxPng)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
38
http/cape.go
Normal file
38
http/cape.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (cfg *Config) Cape(response http.ResponseWriter, request *http.Request) {
|
||||
if mux.Vars(request)["converted"] == "" {
|
||||
cfg.Logger.IncCounter("capes.request", 1)
|
||||
}
|
||||
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
rec, err := cfg.CapesRepo.FindByUsername(username)
|
||||
if err != nil {
|
||||
http.Redirect(response, request, "http://skins.minecraft.net/MinecraftCloaks/" + username + ".png", 301)
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "image/png")
|
||||
io.Copy(response, rec.File)
|
||||
}
|
||||
|
||||
func (cfg *Config) CapeGET(response http.ResponseWriter, request *http.Request) {
|
||||
cfg.Logger.IncCounter("capes.get_request", 1)
|
||||
username := request.URL.Query().Get("name")
|
||||
if username == "" {
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
mux.Vars(request)["username"] = username
|
||||
mux.Vars(request)["converted"] = "1"
|
||||
|
||||
cfg.Cape(response, request)
|
||||
}
|
||||
138
http/cape_test.go
Normal file
138
http/cape_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/elyby/chrly/db"
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
func TestConfig_Cape(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
cape := createCape()
|
||||
|
||||
mocks.Capes.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{
|
||||
File: bytes.NewReader(cape),
|
||||
}, nil)
|
||||
mocks.Log.EXPECT().IncCounter("capes.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/mocked_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(200, resp.StatusCode)
|
||||
responseData, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(cape, responseData)
|
||||
assert.Equal("image/png", resp.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestConfig_Cape2(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"})
|
||||
mocks.Log.EXPECT().IncCounter("capes.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/notch", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(301, resp.StatusCode)
|
||||
assert.Equal("http://skins.minecraft.net/MinecraftCloaks/notch.png", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestConfig_CapeGET(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
cape := createCape()
|
||||
|
||||
mocks.Capes.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{
|
||||
File: bytes.NewReader(cape),
|
||||
}, nil)
|
||||
mocks.Log.EXPECT().IncCounter("capes.request", int64(1)).Times(0)
|
||||
mocks.Log.EXPECT().IncCounter("capes.get_request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=mocked_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(200, resp.StatusCode)
|
||||
responseData, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal(cape, responseData)
|
||||
assert.Equal("image/png", resp.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestConfig_CapeGET2(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"})
|
||||
mocks.Log.EXPECT().IncCounter("capes.request", int64(1)).Times(0)
|
||||
mocks.Log.EXPECT().IncCounter("capes.get_request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=notch", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(301, resp.StatusCode)
|
||||
assert.Equal("http://skins.minecraft.net/MinecraftCloaks/notch.png", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestConfig_CapeGET3(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/?name=notch", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
(&Config{}).CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(301, resp.StatusCode)
|
||||
assert.Equal("http://skinsystem.ely.by/cloaks?name=notch", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
// Cape md5: 424ff79dce9940af89c28ad80de8aaad
|
||||
func createCape() []byte {
|
||||
img := image.NewAlpha(image.Rect(0, 0, 64, 32))
|
||||
writer := &bytes.Buffer{}
|
||||
png.Encode(writer, img)
|
||||
|
||||
pngBytes, _ := ioutil.ReadAll(writer)
|
||||
|
||||
return pngBytes
|
||||
}
|
||||
27
http/face.go
Normal file
27
http/face.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const defaultHash = "default"
|
||||
|
||||
func (cfg *Config) Face(response http.ResponseWriter, request *http.Request) {
|
||||
cfg.Logger.IncCounter("faces.request", 1)
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
rec, err := cfg.SkinsRepo.FindByUsername(username)
|
||||
var hash string
|
||||
if err != nil || rec.SkinId == 0 {
|
||||
hash = defaultHash
|
||||
} else {
|
||||
hash = rec.Hash
|
||||
}
|
||||
|
||||
http.Redirect(response, request, buildFaceUrl(hash), 301)
|
||||
}
|
||||
|
||||
func buildFaceUrl(hash string) string {
|
||||
return "http://ely.by/minecraft/skin_buffer/faces/" + hash + ".png"
|
||||
}
|
||||
53
http/face_test.go
Normal file
53
http/face_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/elyby/chrly/db"
|
||||
)
|
||||
|
||||
func TestConfig_Face(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Log.EXPECT().IncCounter("faces.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(301, resp.StatusCode)
|
||||
assert.Equal("http://ely.by/minecraft/skin_buffer/faces/55d2a8848764f5ff04012cdb093458bd.png", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestConfig_Face2(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"})
|
||||
mocks.Log.EXPECT().IncCounter("faces.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(301, resp.StatusCode)
|
||||
assert.Equal("http://ely.by/minecraft/skin_buffer/faces/default.png", resp.Header.Get("Location"))
|
||||
}
|
||||
87
http/http.go
Normal file
87
http/http.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mono83/slf/wd"
|
||||
|
||||
"github.com/elyby/chrly/interfaces"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ListenSpec string
|
||||
|
||||
SkinsRepo interfaces.SkinsRepository
|
||||
CapesRepo interfaces.CapesRepository
|
||||
Logger wd.Watchdog
|
||||
Auth interfaces.AuthChecker
|
||||
}
|
||||
|
||||
func (cfg *Config) Run() error {
|
||||
cfg.Logger.Info(fmt.Sprintf("Starting, HTTP on: %s\n", cfg.ListenSpec))
|
||||
|
||||
listener, err := net.Listen("tcp", cfg.ListenSpec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
ReadTimeout: 60 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
MaxHeaderBytes: 1 << 16,
|
||||
Handler: cfg.CreateHandler(),
|
||||
}
|
||||
|
||||
go server.Serve(listener)
|
||||
|
||||
s := waitForSignal()
|
||||
cfg.Logger.Info(fmt.Sprintf("Got signal: %v, exiting.", s))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) CreateHandler() http.Handler {
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
|
||||
router.HandleFunc("/skins/{username}", cfg.Skin).Methods("GET")
|
||||
router.HandleFunc("/cloaks/{username}", cfg.Cape).Methods("GET").Name("cloaks")
|
||||
router.HandleFunc("/textures/{username}", cfg.Textures).Methods("GET")
|
||||
router.HandleFunc("/textures/signed/{username}", cfg.SignedTextures).Methods("GET")
|
||||
router.HandleFunc("/skins/{username}/face", cfg.Face).Methods("GET")
|
||||
router.HandleFunc("/skins/{username}/face.png", cfg.Face).Methods("GET")
|
||||
// Legacy
|
||||
router.HandleFunc("/skins", cfg.SkinGET).Methods("GET")
|
||||
router.HandleFunc("/cloaks", cfg.CapeGET).Methods("GET")
|
||||
// API
|
||||
router.Handle("/api/skins", cfg.Authenticate(http.HandlerFunc(cfg.PostSkin))).Methods("POST")
|
||||
router.Handle("/api/skins/id:{id:[0-9]+}", cfg.Authenticate(http.HandlerFunc(cfg.DeleteSkinByUserId))).Methods("DELETE")
|
||||
router.Handle("/api/skins/{username}", cfg.Authenticate(http.HandlerFunc(cfg.DeleteSkinByUsername))).Methods("DELETE")
|
||||
// 404
|
||||
router.NotFoundHandler = http.HandlerFunc(cfg.NotFound)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func parseUsername(username string) string {
|
||||
const suffix = ".png"
|
||||
if strings.HasSuffix(username, suffix) {
|
||||
username = strings.TrimSuffix(username, suffix)
|
||||
}
|
||||
|
||||
return username
|
||||
}
|
||||
|
||||
func waitForSignal() os.Signal {
|
||||
ch := make(chan os.Signal)
|
||||
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
return <-ch
|
||||
}
|
||||
46
http/http_test.go
Normal file
46
http/http_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/elyby/chrly/interfaces/mock_interfaces"
|
||||
"github.com/elyby/chrly/interfaces/mock_wd"
|
||||
)
|
||||
|
||||
func TestParseUsername(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
assert.Equal("test", parseUsername("test.png"), "Function should trim .png at end")
|
||||
assert.Equal("test", parseUsername("test"), "Function should return string itself, if it not contains .png at end")
|
||||
}
|
||||
|
||||
type mocks struct {
|
||||
Skins *mock_interfaces.MockSkinsRepository
|
||||
Capes *mock_interfaces.MockCapesRepository
|
||||
Auth *mock_interfaces.MockAuthChecker
|
||||
Log *mock_wd.MockWatchdog
|
||||
}
|
||||
|
||||
func setupMocks(ctrl *gomock.Controller) (
|
||||
*Config,
|
||||
*mocks,
|
||||
) {
|
||||
skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl)
|
||||
capesRepo := mock_interfaces.NewMockCapesRepository(ctrl)
|
||||
authChecker := mock_interfaces.NewMockAuthChecker(ctrl)
|
||||
wd := mock_wd.NewMockWatchdog(ctrl)
|
||||
|
||||
return &Config{
|
||||
SkinsRepo: skinsRepo,
|
||||
CapesRepo: capesRepo,
|
||||
Auth: authChecker,
|
||||
Logger: wd,
|
||||
}, &mocks{
|
||||
Skins: skinsRepo,
|
||||
Capes: capesRepo,
|
||||
Auth: authChecker,
|
||||
Log: wd,
|
||||
}
|
||||
}
|
||||
18
http/not_found.go
Normal file
18
http/not_found.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (cfg *Config) NotFound(response http.ResponseWriter, request *http.Request) {
|
||||
data, _ := json.Marshal(map[string]string{
|
||||
"status": "404",
|
||||
"message": "Not Found",
|
||||
"link": "http://docs.ely.by/skin-system.html",
|
||||
})
|
||||
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
response.WriteHeader(http.StatusNotFound)
|
||||
response.Write(data)
|
||||
}
|
||||
28
http/not_found_test.go
Normal file
28
http/not_found_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfig_NotFound(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
(&Config{}).CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(404, resp.StatusCode)
|
||||
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`{
|
||||
"status": "404",
|
||||
"message": "Not Found",
|
||||
"link": "http://docs.ely.by/skin-system.html"
|
||||
}`, string(response))
|
||||
}
|
||||
52
http/signed_textures.go
Normal file
52
http/signed_textures.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type signedTexturesResponse struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Props []property `json:"properties"`
|
||||
}
|
||||
|
||||
type property struct {
|
||||
Name string `json:"name"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (cfg *Config) SignedTextures(response http.ResponseWriter, request *http.Request) {
|
||||
cfg.Logger.IncCounter("signed_textures.request", 1)
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
|
||||
rec, err := cfg.SkinsRepo.FindByUsername(username)
|
||||
if err != nil || rec.SkinId == 0 || rec.MojangTextures == "" {
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
responseData:= signedTexturesResponse{
|
||||
Id: strings.Replace(rec.Uuid, "-", "", -1),
|
||||
Name: rec.Username,
|
||||
Props: []property{
|
||||
{
|
||||
Name: "textures",
|
||||
Signature: rec.MojangSignature,
|
||||
Value: rec.MojangTextures,
|
||||
},
|
||||
{
|
||||
Name: "ely",
|
||||
Value: "but why are you asking?",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
responseJson,_ := json.Marshal(responseData)
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
response.Write(responseJson)
|
||||
}
|
||||
71
http/signed_textures_test.go
Normal file
71
http/signed_textures_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/elyby/chrly/db"
|
||||
)
|
||||
|
||||
func TestConfig_SignedTextures(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/signed/mock_user", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(200, resp.StatusCode)
|
||||
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`{
|
||||
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||
"name": "mock_user",
|
||||
"properties": [
|
||||
{
|
||||
"name": "textures",
|
||||
"signature": "mocked signature",
|
||||
"value": "mocked textures base64"
|
||||
},
|
||||
{
|
||||
"name": "ely",
|
||||
"value": "but why are you asking?"
|
||||
}
|
||||
]
|
||||
}`, string(response))
|
||||
}
|
||||
|
||||
func TestConfig_SignedTextures2(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{})
|
||||
mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/signed/mock_user", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(204, resp.StatusCode)
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.Equal("", string(response))
|
||||
}
|
||||
36
http/skin.go
Normal file
36
http/skin.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (cfg *Config) Skin(response http.ResponseWriter, request *http.Request) {
|
||||
if mux.Vars(request)["converted"] == "" {
|
||||
cfg.Logger.IncCounter("skins.request", 1)
|
||||
}
|
||||
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
rec, err := cfg.SkinsRepo.FindByUsername(username)
|
||||
if err != nil || rec.SkinId == 0 {
|
||||
http.Redirect(response, request, "http://skins.minecraft.net/MinecraftSkins/" + username + ".png", 301)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(response, request, rec.Url, 301)
|
||||
}
|
||||
|
||||
func (cfg *Config) SkinGET(response http.ResponseWriter, request *http.Request) {
|
||||
cfg.Logger.IncCounter("skins.get_request", 1)
|
||||
username := request.URL.Query().Get("name")
|
||||
if username == "" {
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
mux.Vars(request)["username"] = username
|
||||
mux.Vars(request)["converted"] = "1"
|
||||
|
||||
cfg.Skin(response, request)
|
||||
}
|
||||
125
http/skin_test.go
Normal file
125
http/skin_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/elyby/chrly/db"
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
func TestConfig_Skin(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Log.EXPECT().IncCounter("skins.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(301, resp.StatusCode)
|
||||
assert.Equal("http://ely.by/minecraft/skins/skin.png", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestConfig_Skin2(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"})
|
||||
mocks.Log.EXPECT().IncCounter("skins.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/notch", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(301, resp.StatusCode)
|
||||
assert.Equal("http://skins.minecraft.net/MinecraftSkins/notch.png", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestConfig_SkinGET(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Log.EXPECT().IncCounter("skins.get_request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("skins.request", int64(1)).Times(0)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=mock_user", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(301, resp.StatusCode)
|
||||
assert.Equal("http://ely.by/minecraft/skins/skin.png", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestConfig_SkinGET2(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"})
|
||||
mocks.Log.EXPECT().IncCounter("skins.get_request", int64(1))
|
||||
mocks.Log.EXPECT().IncCounter("skins.request", int64(1)).Times(0)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=notch", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(301, resp.StatusCode)
|
||||
assert.Equal("http://skins.minecraft.net/MinecraftSkins/notch.png", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestConfig_SkinGET3(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/?name=notch", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
(&Config{}).CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(301, resp.StatusCode)
|
||||
assert.Equal("http://skinsystem.ely.by/skins?name=notch", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func createSkinModel(username string, isSlim bool) *model.Skin {
|
||||
return &model.Skin{
|
||||
UserId: 1,
|
||||
Username: username,
|
||||
Uuid: "0f657aa8-bfbe-415d-b700-5750090d3af3",
|
||||
SkinId: 1,
|
||||
Hash: "55d2a8848764f5ff04012cdb093458bd",
|
||||
Url: "http://ely.by/minecraft/skins/skin.png",
|
||||
MojangTextures: "mocked textures base64",
|
||||
MojangSignature: "mocked signature",
|
||||
IsSlim: isSlim,
|
||||
}
|
||||
}
|
||||
102
http/textures.go
Normal file
102
http/textures.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func (cfg *Config) Textures(response http.ResponseWriter, request *http.Request) {
|
||||
cfg.Logger.IncCounter("textures.request", 1)
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
|
||||
skin, err := cfg.SkinsRepo.FindByUsername(username)
|
||||
if err != nil || skin.SkinId == 0 {
|
||||
if skin == nil {
|
||||
skin = &model.Skin{}
|
||||
}
|
||||
|
||||
skin.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png"
|
||||
skin.Hash = string(buildNonElyTexturesHash(username))
|
||||
}
|
||||
|
||||
textures := texturesResponse{
|
||||
Skin: &Skin{
|
||||
Url: skin.Url,
|
||||
Hash: skin.Hash,
|
||||
},
|
||||
}
|
||||
|
||||
if skin.IsSlim {
|
||||
textures.Skin.Metadata = &skinMetadata{
|
||||
Model: "slim",
|
||||
}
|
||||
}
|
||||
|
||||
cape, err := cfg.CapesRepo.FindByUsername(username)
|
||||
if err == nil {
|
||||
var scheme string = "http://"
|
||||
if request.TLS != nil {
|
||||
scheme = "https://"
|
||||
}
|
||||
|
||||
textures.Cape = &Cape{
|
||||
Url: scheme + request.Host + "/cloaks/" + username,
|
||||
Hash: calculateCapeHash(cape),
|
||||
}
|
||||
}
|
||||
|
||||
responseData, _ := json.Marshal(textures)
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
response.Write(responseData)
|
||||
}
|
||||
|
||||
func calculateCapeHash(cape *model.Cape) string {
|
||||
hasher := md5.New()
|
||||
io.Copy(hasher, cape.File)
|
||||
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
var timeNow = time.Now
|
||||
|
||||
func getCurrentHour() int64 {
|
||||
n := timeNow()
|
||||
return time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), 0, 0, 0, time.UTC).Unix()
|
||||
}
|
||||
166
http/textures_test.go
Normal file
166
http/textures_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/elyby/chrly/db"
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
func TestConfig_Textures(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Capes.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"})
|
||||
mocks.Log.EXPECT().IncCounter("textures.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(200, resp.StatusCode)
|
||||
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`{
|
||||
"SKIN": {
|
||||
"url": "http://ely.by/minecraft/skins/skin.png",
|
||||
"hash": "55d2a8848764f5ff04012cdb093458bd"
|
||||
}
|
||||
}`, string(response))
|
||||
}
|
||||
|
||||
func TestConfig_Textures2(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", true), nil)
|
||||
mocks.Capes.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"})
|
||||
mocks.Log.EXPECT().IncCounter("textures.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(200, resp.StatusCode)
|
||||
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`{
|
||||
"SKIN": {
|
||||
"url": "http://ely.by/minecraft/skins/skin.png",
|
||||
"hash": "55d2a8848764f5ff04012cdb093458bd",
|
||||
"metadata": {
|
||||
"model": "slim"
|
||||
}
|
||||
}
|
||||
}`, string(response))
|
||||
}
|
||||
|
||||
func TestConfig_Textures3(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
|
||||
mocks.Capes.EXPECT().FindByUsername("mock_user").Return(&model.Cape{
|
||||
File: bytes.NewReader(createCape()),
|
||||
}, nil)
|
||||
mocks.Log.EXPECT().IncCounter("textures.request", int64(1))
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(200, resp.StatusCode)
|
||||
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`{
|
||||
"SKIN": {
|
||||
"url": "http://ely.by/minecraft/skins/skin.png",
|
||||
"hash": "55d2a8848764f5ff04012cdb093458bd"
|
||||
},
|
||||
"CAPE": {
|
||||
"url": "http://skinsystem.ely.by/cloaks/mock_user",
|
||||
"hash": "424ff79dce9940af89c28ad80de8aaad"
|
||||
}
|
||||
}`, string(response))
|
||||
}
|
||||
|
||||
func TestConfig_Textures4(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
config, mocks := setupMocks(ctrl)
|
||||
|
||||
mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{})
|
||||
mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{})
|
||||
mocks.Log.EXPECT().IncCounter("textures.request", int64(1))
|
||||
timeNow = func() time.Time {
|
||||
return time.Date(2017, time.August, 20, 0, 15, 54, 0, time.UTC)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/notch", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
config.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(200, resp.StatusCode)
|
||||
assert.Equal("application/json", resp.Header.Get("Content-Type"))
|
||||
response, _ := ioutil.ReadAll(resp.Body)
|
||||
assert.JSONEq(`{
|
||||
"SKIN": {
|
||||
"url": "http://skins.minecraft.net/MinecraftSkins/notch.png",
|
||||
"hash": "5923cf3f7fa170a279e4d7a9483cfc52"
|
||||
}
|
||||
}`, string(response))
|
||||
}
|
||||
|
||||
func TestBuildNonElyTexturesHash(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
timeNow = func() time.Time {
|
||||
return time.Date(2017, time.November, 30, 16, 15, 34, 0, time.UTC)
|
||||
}
|
||||
|
||||
assert.Equal("686d788a5353cb636e8fdff727634d88", buildNonElyTexturesHash("username"), "Function should return fixed hash by username-time pair")
|
||||
assert.Equal("fb876f761683a10accdb17d403cef64c", buildNonElyTexturesHash("another-username"), "Function should return fixed hash by username-time pair")
|
||||
|
||||
timeNow = func() time.Time {
|
||||
return time.Date(2017, time.November, 30, 16, 20, 12, 0, time.UTC)
|
||||
}
|
||||
|
||||
assert.Equal("686d788a5353cb636e8fdff727634d88", buildNonElyTexturesHash("username"), "Function should do not change it's value if hour the same")
|
||||
assert.Equal("fb876f761683a10accdb17d403cef64c", buildNonElyTexturesHash("another-username"), "Function should return fixed hash by username-time pair")
|
||||
|
||||
timeNow = func() time.Time {
|
||||
return time.Date(2017, time.November, 30, 17, 1, 3, 0, time.UTC)
|
||||
}
|
||||
|
||||
assert.Equal("42277892fd24bc0ed86285b3bb8b8fad", buildNonElyTexturesHash("username"), "Function should change it's value if hour changed")
|
||||
}
|
||||
7
interfaces/auth.go
Normal file
7
interfaces/auth.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package interfaces
|
||||
|
||||
import "net/http"
|
||||
|
||||
type AuthChecker interface {
|
||||
Check(req *http.Request) error
|
||||
}
|
||||
45
interfaces/mock_interfaces/mock_auth.go
Normal file
45
interfaces/mock_interfaces/mock_auth.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: interfaces/auth.go
|
||||
|
||||
package mock_interfaces
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
http "net/http"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockAuthChecker is a mock of AuthChecker interface
|
||||
type MockAuthChecker struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAuthCheckerMockRecorder
|
||||
}
|
||||
|
||||
// MockAuthCheckerMockRecorder is the mock recorder for MockAuthChecker
|
||||
type MockAuthCheckerMockRecorder struct {
|
||||
mock *MockAuthChecker
|
||||
}
|
||||
|
||||
// NewMockAuthChecker creates a new mock instance
|
||||
func NewMockAuthChecker(ctrl *gomock.Controller) *MockAuthChecker {
|
||||
mock := &MockAuthChecker{ctrl: ctrl}
|
||||
mock.recorder = &MockAuthCheckerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (_m *MockAuthChecker) EXPECT() *MockAuthCheckerMockRecorder {
|
||||
return _m.recorder
|
||||
}
|
||||
|
||||
// Check mocks base method
|
||||
func (_m *MockAuthChecker) Check(req *http.Request) error {
|
||||
ret := _m.ctrl.Call(_m, "Check", req)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Check indicates an expected call of Check
|
||||
func (_mr *MockAuthCheckerMockRecorder) Check(arg0 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Check", reflect.TypeOf((*MockAuthChecker)(nil).Check), arg0)
|
||||
}
|
||||
131
interfaces/mock_interfaces/mock_interfaces.go
Normal file
131
interfaces/mock_interfaces/mock_interfaces.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: interfaces/repositories.go
|
||||
|
||||
package mock_interfaces
|
||||
|
||||
import (
|
||||
model "github.com/elyby/chrly/model"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockSkinsRepository is a mock of SkinsRepository interface
|
||||
type MockSkinsRepository struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSkinsRepositoryMockRecorder
|
||||
}
|
||||
|
||||
// MockSkinsRepositoryMockRecorder is the mock recorder for MockSkinsRepository
|
||||
type MockSkinsRepositoryMockRecorder struct {
|
||||
mock *MockSkinsRepository
|
||||
}
|
||||
|
||||
// NewMockSkinsRepository creates a new mock instance
|
||||
func NewMockSkinsRepository(ctrl *gomock.Controller) *MockSkinsRepository {
|
||||
mock := &MockSkinsRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockSkinsRepositoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (_m *MockSkinsRepository) EXPECT() *MockSkinsRepositoryMockRecorder {
|
||||
return _m.recorder
|
||||
}
|
||||
|
||||
// FindByUsername mocks base method
|
||||
func (_m *MockSkinsRepository) FindByUsername(username string) (*model.Skin, error) {
|
||||
ret := _m.ctrl.Call(_m, "FindByUsername", username)
|
||||
ret0, _ := ret[0].(*model.Skin)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindByUsername indicates an expected call of FindByUsername
|
||||
func (_mr *MockSkinsRepositoryMockRecorder) FindByUsername(arg0 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "FindByUsername", reflect.TypeOf((*MockSkinsRepository)(nil).FindByUsername), arg0)
|
||||
}
|
||||
|
||||
// FindByUserId mocks base method
|
||||
func (_m *MockSkinsRepository) FindByUserId(id int) (*model.Skin, error) {
|
||||
ret := _m.ctrl.Call(_m, "FindByUserId", id)
|
||||
ret0, _ := ret[0].(*model.Skin)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindByUserId indicates an expected call of FindByUserId
|
||||
func (_mr *MockSkinsRepositoryMockRecorder) FindByUserId(arg0 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "FindByUserId", reflect.TypeOf((*MockSkinsRepository)(nil).FindByUserId), arg0)
|
||||
}
|
||||
|
||||
// Save mocks base method
|
||||
func (_m *MockSkinsRepository) Save(skin *model.Skin) error {
|
||||
ret := _m.ctrl.Call(_m, "Save", skin)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Save indicates an expected call of Save
|
||||
func (_mr *MockSkinsRepositoryMockRecorder) Save(arg0 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Save", reflect.TypeOf((*MockSkinsRepository)(nil).Save), arg0)
|
||||
}
|
||||
|
||||
// RemoveByUserId mocks base method
|
||||
func (_m *MockSkinsRepository) RemoveByUserId(id int) error {
|
||||
ret := _m.ctrl.Call(_m, "RemoveByUserId", id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveByUserId indicates an expected call of RemoveByUserId
|
||||
func (_mr *MockSkinsRepositoryMockRecorder) RemoveByUserId(arg0 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RemoveByUserId", reflect.TypeOf((*MockSkinsRepository)(nil).RemoveByUserId), arg0)
|
||||
}
|
||||
|
||||
// RemoveByUsername mocks base method
|
||||
func (_m *MockSkinsRepository) RemoveByUsername(username string) error {
|
||||
ret := _m.ctrl.Call(_m, "RemoveByUsername", username)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveByUsername indicates an expected call of RemoveByUsername
|
||||
func (_mr *MockSkinsRepositoryMockRecorder) RemoveByUsername(arg0 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RemoveByUsername", reflect.TypeOf((*MockSkinsRepository)(nil).RemoveByUsername), arg0)
|
||||
}
|
||||
|
||||
// MockCapesRepository is a mock of CapesRepository interface
|
||||
type MockCapesRepository struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockCapesRepositoryMockRecorder
|
||||
}
|
||||
|
||||
// MockCapesRepositoryMockRecorder is the mock recorder for MockCapesRepository
|
||||
type MockCapesRepositoryMockRecorder struct {
|
||||
mock *MockCapesRepository
|
||||
}
|
||||
|
||||
// NewMockCapesRepository creates a new mock instance
|
||||
func NewMockCapesRepository(ctrl *gomock.Controller) *MockCapesRepository {
|
||||
mock := &MockCapesRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockCapesRepositoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (_m *MockCapesRepository) EXPECT() *MockCapesRepositoryMockRecorder {
|
||||
return _m.recorder
|
||||
}
|
||||
|
||||
// FindByUsername mocks base method
|
||||
func (_m *MockCapesRepository) FindByUsername(username string) (*model.Cape, error) {
|
||||
ret := _m.ctrl.Call(_m, "FindByUsername", username)
|
||||
ret0, _ := ret[0].(*model.Cape)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindByUsername indicates an expected call of FindByUsername
|
||||
func (_mr *MockCapesRepositoryMockRecorder) FindByUsername(arg0 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "FindByUsername", reflect.TypeOf((*MockCapesRepository)(nil).FindByUsername), arg0)
|
||||
}
|
||||
218
interfaces/mock_wd/mock_wd.go
Normal file
218
interfaces/mock_wd/mock_wd.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/mono83/slf/wd (interfaces: Watchdog)
|
||||
|
||||
package mock_wd
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
slf "github.com/mono83/slf"
|
||||
wd "github.com/mono83/slf/wd"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
)
|
||||
|
||||
// MockWatchdog is a mock of Watchdog interface
|
||||
type MockWatchdog struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockWatchdogMockRecorder
|
||||
}
|
||||
|
||||
// MockWatchdogMockRecorder is the mock recorder for MockWatchdog
|
||||
type MockWatchdogMockRecorder struct {
|
||||
mock *MockWatchdog
|
||||
}
|
||||
|
||||
// NewMockWatchdog creates a new mock instance
|
||||
func NewMockWatchdog(ctrl *gomock.Controller) *MockWatchdog {
|
||||
mock := &MockWatchdog{ctrl: ctrl}
|
||||
mock.recorder = &MockWatchdogMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (_m *MockWatchdog) EXPECT() *MockWatchdogMockRecorder {
|
||||
return _m.recorder
|
||||
}
|
||||
|
||||
// Alert mocks base method
|
||||
func (_m *MockWatchdog) Alert(_param0 string, _param1 ...slf.Param) {
|
||||
_s := []interface{}{_param0}
|
||||
for _, _x := range _param1 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
_m.ctrl.Call(_m, "Alert", _s...)
|
||||
}
|
||||
|
||||
// Alert indicates an expected call of Alert
|
||||
func (_mr *MockWatchdogMockRecorder) Alert(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0}, arg1...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Alert", reflect.TypeOf((*MockWatchdog)(nil).Alert), _s...)
|
||||
}
|
||||
|
||||
// Debug mocks base method
|
||||
func (_m *MockWatchdog) Debug(_param0 string, _param1 ...slf.Param) {
|
||||
_s := []interface{}{_param0}
|
||||
for _, _x := range _param1 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
_m.ctrl.Call(_m, "Debug", _s...)
|
||||
}
|
||||
|
||||
// Debug indicates an expected call of Debug
|
||||
func (_mr *MockWatchdogMockRecorder) Debug(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0}, arg1...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Debug", reflect.TypeOf((*MockWatchdog)(nil).Debug), _s...)
|
||||
}
|
||||
|
||||
// Emergency mocks base method
|
||||
func (_m *MockWatchdog) Emergency(_param0 string, _param1 ...slf.Param) {
|
||||
_s := []interface{}{_param0}
|
||||
for _, _x := range _param1 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
_m.ctrl.Call(_m, "Emergency", _s...)
|
||||
}
|
||||
|
||||
// Emergency indicates an expected call of Emergency
|
||||
func (_mr *MockWatchdogMockRecorder) Emergency(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0}, arg1...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Emergency", reflect.TypeOf((*MockWatchdog)(nil).Emergency), _s...)
|
||||
}
|
||||
|
||||
// Error mocks base method
|
||||
func (_m *MockWatchdog) Error(_param0 string, _param1 ...slf.Param) {
|
||||
_s := []interface{}{_param0}
|
||||
for _, _x := range _param1 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
_m.ctrl.Call(_m, "Error", _s...)
|
||||
}
|
||||
|
||||
// Error indicates an expected call of Error
|
||||
func (_mr *MockWatchdogMockRecorder) Error(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0}, arg1...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Error", reflect.TypeOf((*MockWatchdog)(nil).Error), _s...)
|
||||
}
|
||||
|
||||
// IncCounter mocks base method
|
||||
func (_m *MockWatchdog) IncCounter(_param0 string, _param1 int64, _param2 ...slf.Param) {
|
||||
_s := []interface{}{_param0, _param1}
|
||||
for _, _x := range _param2 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
_m.ctrl.Call(_m, "IncCounter", _s...)
|
||||
}
|
||||
|
||||
// IncCounter indicates an expected call of IncCounter
|
||||
func (_mr *MockWatchdogMockRecorder) IncCounter(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0, arg1}, arg2...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "IncCounter", reflect.TypeOf((*MockWatchdog)(nil).IncCounter), _s...)
|
||||
}
|
||||
|
||||
// Info mocks base method
|
||||
func (_m *MockWatchdog) Info(_param0 string, _param1 ...slf.Param) {
|
||||
_s := []interface{}{_param0}
|
||||
for _, _x := range _param1 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
_m.ctrl.Call(_m, "Info", _s...)
|
||||
}
|
||||
|
||||
// Info indicates an expected call of Info
|
||||
func (_mr *MockWatchdogMockRecorder) Info(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0}, arg1...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Info", reflect.TypeOf((*MockWatchdog)(nil).Info), _s...)
|
||||
}
|
||||
|
||||
// RecordTimer mocks base method
|
||||
func (_m *MockWatchdog) RecordTimer(_param0 string, _param1 time.Duration, _param2 ...slf.Param) {
|
||||
_s := []interface{}{_param0, _param1}
|
||||
for _, _x := range _param2 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
_m.ctrl.Call(_m, "RecordTimer", _s...)
|
||||
}
|
||||
|
||||
// RecordTimer indicates an expected call of RecordTimer
|
||||
func (_mr *MockWatchdogMockRecorder) RecordTimer(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0, arg1}, arg2...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RecordTimer", reflect.TypeOf((*MockWatchdog)(nil).RecordTimer), _s...)
|
||||
}
|
||||
|
||||
// Timer mocks base method
|
||||
func (_m *MockWatchdog) Timer(_param0 string, _param1 ...slf.Param) slf.Timer {
|
||||
_s := []interface{}{_param0}
|
||||
for _, _x := range _param1 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
ret := _m.ctrl.Call(_m, "Timer", _s...)
|
||||
ret0, _ := ret[0].(slf.Timer)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Timer indicates an expected call of Timer
|
||||
func (_mr *MockWatchdogMockRecorder) Timer(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0}, arg1...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Timer", reflect.TypeOf((*MockWatchdog)(nil).Timer), _s...)
|
||||
}
|
||||
|
||||
// Trace mocks base method
|
||||
func (_m *MockWatchdog) Trace(_param0 string, _param1 ...slf.Param) {
|
||||
_s := []interface{}{_param0}
|
||||
for _, _x := range _param1 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
_m.ctrl.Call(_m, "Trace", _s...)
|
||||
}
|
||||
|
||||
// Trace indicates an expected call of Trace
|
||||
func (_mr *MockWatchdogMockRecorder) Trace(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0}, arg1...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Trace", reflect.TypeOf((*MockWatchdog)(nil).Trace), _s...)
|
||||
}
|
||||
|
||||
// UpdateGauge mocks base method
|
||||
func (_m *MockWatchdog) UpdateGauge(_param0 string, _param1 int64, _param2 ...slf.Param) {
|
||||
_s := []interface{}{_param0, _param1}
|
||||
for _, _x := range _param2 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
_m.ctrl.Call(_m, "UpdateGauge", _s...)
|
||||
}
|
||||
|
||||
// UpdateGauge indicates an expected call of UpdateGauge
|
||||
func (_mr *MockWatchdogMockRecorder) UpdateGauge(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0, arg1}, arg2...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "UpdateGauge", reflect.TypeOf((*MockWatchdog)(nil).UpdateGauge), _s...)
|
||||
}
|
||||
|
||||
// Warning mocks base method
|
||||
func (_m *MockWatchdog) Warning(_param0 string, _param1 ...slf.Param) {
|
||||
_s := []interface{}{_param0}
|
||||
for _, _x := range _param1 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
_m.ctrl.Call(_m, "Warning", _s...)
|
||||
}
|
||||
|
||||
// Warning indicates an expected call of Warning
|
||||
func (_mr *MockWatchdogMockRecorder) Warning(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
_s := append([]interface{}{arg0}, arg1...)
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Warning", reflect.TypeOf((*MockWatchdog)(nil).Warning), _s...)
|
||||
}
|
||||
|
||||
// WithParams mocks base method
|
||||
func (_m *MockWatchdog) WithParams(_param0 ...slf.Param) wd.Watchdog {
|
||||
_s := []interface{}{}
|
||||
for _, _x := range _param0 {
|
||||
_s = append(_s, _x)
|
||||
}
|
||||
ret := _m.ctrl.Call(_m, "WithParams", _s...)
|
||||
ret0, _ := ret[0].(wd.Watchdog)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithParams indicates an expected call of WithParams
|
||||
func (_mr *MockWatchdogMockRecorder) WithParams(arg0 ...interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "WithParams", reflect.TypeOf((*MockWatchdog)(nil).WithParams), arg0...)
|
||||
}
|
||||
17
interfaces/repositories.go
Normal file
17
interfaces/repositories.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
type SkinsRepository interface {
|
||||
FindByUsername(username string) (*model.Skin, error)
|
||||
FindByUserId(id int) (*model.Skin, error)
|
||||
Save(skin *model.Skin) error
|
||||
RemoveByUserId(id int) error
|
||||
RemoveByUsername(username string) error
|
||||
}
|
||||
|
||||
type CapesRepository interface {
|
||||
FindByUsername(username string) (*model.Cape, error)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package data
|
||||
|
||||
import "fmt"
|
||||
|
||||
type DataNotFound struct {
|
||||
Who string
|
||||
}
|
||||
|
||||
func (e DataNotFound) Error() string {
|
||||
return fmt.Sprintf("Skin data not found. Required username \"%v\"", e.Who)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"log"
|
||||
"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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (s *SkinItem) Save() {
|
||||
str, _ := json.Marshal(s)
|
||||
services.RedisPool.Cmd("SET", tools.BuildKey(s.Username), str)
|
||||
}
|
||||
|
||||
func FindRecord(username string) (SkinItem, error) {
|
||||
var record SkinItem;
|
||||
response := services.RedisPool.Cmd("GET", tools.BuildKey(username));
|
||||
if (response.IsType(redis.Nil)) {
|
||||
return record, DataNotFound{username}
|
||||
}
|
||||
|
||||
result, err := response.Str()
|
||||
if (err == nil) {
|
||||
decodeErr := json.Unmarshal([]byte(result), &record)
|
||||
if (decodeErr != nil) {
|
||||
log.Println("Cannot decode record data")
|
||||
}
|
||||
}
|
||||
|
||||
return record, err
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package data
|
||||
|
||||
type TexturesResponse struct {
|
||||
Skin *Skin `json:"SKIN"`
|
||||
}
|
||||
|
||||
type Skin struct {
|
||||
Url string `json:"url"`
|
||||
Hash string `json:"hash"`
|
||||
Metadata *SkinMetadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type SkinMetadata struct {
|
||||
Model string `json:"model"`
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"elyby/minecraft-skinsystem/lib/tools"
|
||||
)
|
||||
|
||||
func Cape(w http.ResponseWriter, r *http.Request) {
|
||||
username := tools.ParseUsername(mux.Vars(r)["username"])
|
||||
log.Println("request cape for username " + username)
|
||||
http.Redirect(w, r, "http://skins.minecraft.net/MinecraftCloaks/" + username + ".png", 301)
|
||||
}
|
||||
|
||||
func CapeGET(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.URL.Query().Get("name")
|
||||
if username == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
mux.Vars(r)["username"] = username
|
||||
Cape(w, r)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Метод-наследие от первой версии системы скинов.
|
||||
// Всё ещё иногда используется
|
||||
// Просто конвертируем данные и отправляем их в основной обработчик
|
||||
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
|
||||
switch required {
|
||||
case "skin": Skin(w, r)
|
||||
case "cloack": Cape(w, r)
|
||||
default: {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"elyby/minecraft-skinsystem/lib/data"
|
||||
)
|
||||
|
||||
func SetSkin(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.Header.Get("X-Ely-key")
|
||||
if key != "43fd2ce61b3f5704dfd729c1f2d6ffdb" {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("Nice try"))
|
||||
return
|
||||
}
|
||||
|
||||
skin := new(data.SkinItem)
|
||||
skin.Username = r.PostFormValue("username")
|
||||
skin.UserId, _ = strconv.Atoi(r.PostFormValue("userId"))
|
||||
skin.SkinId, _ = strconv.Atoi(r.PostFormValue("skinId"))
|
||||
skin.Hash = r.PostFormValue("hash")
|
||||
skin.Is1_8, _ = strconv.ParseBool(r.PostFormValue("is1_8"))
|
||||
skin.IsSlim, _ = strconv.ParseBool(r.PostFormValue("isSlim"))
|
||||
skin.Url = r.PostFormValue("url")
|
||||
skin.Save()
|
||||
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"elyby/minecraft-skinsystem/lib/tools"
|
||||
"elyby/minecraft-skinsystem/lib/data"
|
||||
)
|
||||
|
||||
func Skin(w http.ResponseWriter, r *http.Request) {
|
||||
username := tools.ParseUsername(mux.Vars(r)["username"])
|
||||
log.Println("request skin for username " + username);
|
||||
rec, err := data.FindRecord(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) {
|
||||
username := r.URL.Query().Get("name")
|
||||
if username == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
mux.Vars(r)["username"] = username
|
||||
Skin(w, r)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"elyby/minecraft-skinsystem/lib/data"
|
||||
"elyby/minecraft-skinsystem/lib/tools"
|
||||
)
|
||||
|
||||
func Textures(w http.ResponseWriter, r *http.Request) {
|
||||
username := tools.ParseUsername(mux.Vars(r)["username"])
|
||||
log.Println("request textures for username " + username)
|
||||
|
||||
rec, err := data.FindRecord(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",
|
||||
}
|
||||
}
|
||||
|
||||
response,_ := json.Marshal(textures)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(response)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/mediocregopher/radix.v2/pool"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
var RedisPool *pool.Pool
|
||||
|
||||
var Router *mux.Router
|
||||
@@ -1,40 +0,0 @@
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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:")
|
||||
}
|
||||
}
|
||||
12
main.go
Normal file
12
main.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/elyby/chrly/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
cmd.Execute()
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
//"time"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mediocregopher/radix.v2/pool"
|
||||
|
||||
"elyby/minecraft-skinsystem/lib/routes"
|
||||
"elyby/minecraft-skinsystem/lib/services"
|
||||
//"github.com/mediocregopher/radix.v2/redis"
|
||||
)
|
||||
|
||||
const redisString string = "redis:6379"
|
||||
|
||||
func main() {
|
||||
log.Println("Starting...")
|
||||
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
log.Println("Connecting to redis")
|
||||
redisPool, redisErr := pool.New("tcp", redisString, 10)
|
||||
if redisErr != nil {
|
||||
log.Fatal("Redis unavailable")
|
||||
}
|
||||
log.Println("Connected to redis")
|
||||
|
||||
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")
|
||||
// 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)
|
||||
|
||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||
apiRouter.HandleFunc("/user/{username}/skin", routes.SetSkin).Methods("POST")
|
||||
|
||||
services.RedisPool = redisPool
|
||||
services.Router = router
|
||||
|
||||
/*go func() {
|
||||
for {
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
resp := services.Redis.Cmd("PING")
|
||||
if (resp.Err != nil) {
|
||||
log.Println("Redis not pinged. Try to reconnect")
|
||||
newClient, redisErr := redis.Dial("tcp", redisString)
|
||||
if (redisErr != nil) {
|
||||
log.Println("Cannot reconnect to redis")
|
||||
} else {
|
||||
services.Redis = newClient
|
||||
log.Println("Reconnected")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()*/
|
||||
|
||||
log.Println("Started");
|
||||
log.Fatal(http.ListenAndServe(":80", router))
|
||||
}
|
||||
9
model/cape.go
Normal file
9
model/cape.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Cape struct {
|
||||
File io.Reader
|
||||
}
|
||||
15
model/skin.go
Normal file
15
model/skin.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
type Skin 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
|
||||
}
|
||||
46
script/coverage
Executable file
46
script/coverage
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/bin/sh
|
||||
# Based on https://github.com/mlafeldt/chef-runner/blob/34269dbb726c243dff9764007e7bd7f0fe9ee331/script/coverage
|
||||
# Generate test coverage statistics for Go packages.
|
||||
#
|
||||
# Works around the fact that `go test -coverprofile` currently does not work
|
||||
# with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909
|
||||
#
|
||||
# Usage: script/coverage [--html]
|
||||
#
|
||||
# --html Additionally create HTML report and open it in browser
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
workdir=.cover
|
||||
profile="$workdir/cover.out"
|
||||
mode=count
|
||||
|
||||
generate_cover_data() {
|
||||
rm -rf "$workdir"
|
||||
mkdir "$workdir"
|
||||
|
||||
go test -i "$@" # compile dependencies first before serializing go test invocations
|
||||
for pkg in "$@"; do
|
||||
f="$workdir/$(echo $pkg | tr / -).cover"
|
||||
go test -covermode="$mode" -coverprofile="$f" "$pkg"
|
||||
done
|
||||
|
||||
echo "mode: $mode" >"$profile"
|
||||
grep -h -v "^mode:" "$workdir"/*.cover >>"$profile"
|
||||
}
|
||||
|
||||
show_cover_report() {
|
||||
go tool cover -${1}="$profile"
|
||||
}
|
||||
|
||||
generate_cover_data $(go list ./... | grep -v /vendor/)
|
||||
show_cover_report func
|
||||
case "$1" in
|
||||
"")
|
||||
;;
|
||||
--html)
|
||||
show_cover_report html ;;
|
||||
*)
|
||||
echo >&2 "error: invalid option: $1"; exit 1 ;;
|
||||
esac
|
||||
4
script/mocks
Executable file
4
script/mocks
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
mockgen -source=interfaces/repositories.go -destination=interfaces/mock_interfaces/mock_interfaces.go
|
||||
mockgen -source=interfaces/auth.go -destination=interfaces/mock_interfaces/mock_auth.go
|
||||
27
script/test
Executable file
27
script/test
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
# Based on https://github.com/mlafeldt/chef-runner/blob/34269dbb726c243dff9764007e7bd7f0fe9ee331/script/test
|
||||
# Run package tests for a file/directory, or all tests if no argument is passed.
|
||||
# Useful to e.g. execute package tests for the file currently open in Vim.
|
||||
# Usage: script/test [path]
|
||||
|
||||
set -e
|
||||
|
||||
go_pkg_from_path() {
|
||||
path=$1
|
||||
if test -d "$path"; then
|
||||
dir="$path"
|
||||
else
|
||||
dir=$(dirname "$path")
|
||||
fi
|
||||
(cd "$dir" && go list)
|
||||
}
|
||||
|
||||
if test $# -gt 0; then
|
||||
pkg=$(go_pkg_from_path "$1")
|
||||
verbose=-v
|
||||
else
|
||||
pkg=$(go list ./... | grep -v /vendor/)
|
||||
verbose=
|
||||
fi
|
||||
|
||||
exec go test ${GOTESTOPTS:-$verbose} $pkg
|
||||
Reference in New Issue
Block a user