mirror of
				https://github.com/elyby/chrly.git
				synced 2025-05-31 14:11:51 +05:30 
			
		
		
		
	Merge branch 'sign_textures'
This commit is contained in:
		
							
								
								
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -5,11 +5,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 | 
				
			|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 | 
					and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## [Unreleased] - xxxx-xx-xx
 | 
					## [Unreleased] - xxxx-xx-xx
 | 
				
			||||||
 | 
					### Added
 | 
				
			||||||
 | 
					- `/profile/{username}` endpoint, which returns a profile and its textures, equivalent of the Mojang's
 | 
				
			||||||
 | 
					  [UUID -> Profile + Skin/Cape endpoint](https://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape).
 | 
				
			||||||
 | 
					- `/signature-verification-key` endpoint, which returns the public key in `DER` format for signature verification.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Fixed
 | 
					### Fixed
 | 
				
			||||||
- [#29](https://github.com/elyby/chrly/issues/29) If a previously cached UUID no longer exists,
 | 
					- [#29](https://github.com/elyby/chrly/issues/29) If a previously cached UUID no longer exists,
 | 
				
			||||||
  it will be invalidated and re-requested.
 | 
					  it will be invalidated and re-requested.
 | 
				
			||||||
- Use correct status code for error about empty response from Mojang's API.
 | 
					- Use correct status code for error about empty response from Mojang's API.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Changed
 | 
				
			||||||
 | 
					- **BREAKING**: `/cloaks/{username}` and `/textures/{username}` endpoints will no longer return a cape if there are no
 | 
				
			||||||
 | 
					  textures for the requested username.
 | 
				
			||||||
 | 
					- All endpoints are now returns `500` status code when an error occurred during request processing.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## [4.5.0] - 2020-05-01
 | 
					## [4.5.0] - 2020-05-01
 | 
				
			||||||
### Added
 | 
					### Added
 | 
				
			||||||
- [#24](https://github.com/elyby/chrly/issues/24): Implemented a new strategy for the queue in the batch provider of
 | 
					- [#24](https://github.com/elyby/chrly/issues/24): Implemented a new strategy for the queue in the batch provider of
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										84
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										84
									
								
								README.md
									
									
									
									
									
								
							@@ -15,8 +15,9 @@ production ready.
 | 
				
			|||||||
## Installation
 | 
					## Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You can easily install Chrly using [docker-compose](https://docs.docker.com/compose/). The configuration below (save
 | 
					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
 | 
					it as `docker-compose.yml`) can be used to start a Chrly server. It relies on `CHRLY_SECRET` and `CHRLY_SIGNING_KEY`
 | 
				
			||||||
that you must set before running `docker-compose up -d`. Other possible variables are described below.
 | 
					environment variables that you must set before running `docker-compose up -d`. Other possible variables are described
 | 
				
			||||||
 | 
					below.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```yml
 | 
					```yml
 | 
				
			||||||
version: '2'
 | 
					version: '2'
 | 
				
			||||||
@@ -33,6 +34,7 @@ services:
 | 
				
			|||||||
      - "80:80"
 | 
					      - "80:80"
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      CHRLY_SECRET: replace_this_value_in_production
 | 
					      CHRLY_SECRET: replace_this_value_in_production
 | 
				
			||||||
 | 
					      CHRLY_SIGNING_KEY: base64:LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT3dJQkFBSkJBTmJVcFZDWmtNS3BmdllaMDhXM2x1bWRBYVl4TEJubVVEbHpIQlFIM0RwWWVmNVdDTzMyClREVTZmZUlKNThBMGxBeXdndFo0d3dpMmRHSE96LzFoQXZjQ0F3RUFBUUpBSXRheFNIVGU2UEtieUVVLzlweGoKT05kaFlSWXdWTExvNTZnbk1ZaGt5b0VxYWFNc2ZvdjhoaG9lcGtZWkJNdlpGQjJiRE9zUTJTYUorRTJlaUJPNApBUUloQVBzc1MwK0JSOXcwYk9kbWpHcW1kRTlOck41VUpRY09XMTNzMjkrNlF6VUJBaUVBMnZXT2VwQTVBcGl1CnBFQTNwd29HZGtWQ3JOU25uS2pEUXpEWEJucGQzL2NDSUVGTmQ5c1k0cVVHNEZXZFhONlJubVhMN1NqMHVaZkgKRE13enU4ckVNNXNCQWlFQWh2ZG9ETnFMbWJNZHEzYytGc1BTT2VMMWQyMVpwL0pLOGtiUHRGbUhOZjhDSVFEVgo2RlNaRHd2V2Z1eGFNN0JzeWNRT05rakRCVFBOdStscWN0SkJHbkJ2M0E9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  redis:
 | 
					  redis:
 | 
				
			||||||
    image: redis:4.0-32bit
 | 
					    image: redis:4.0-32bit
 | 
				
			||||||
@@ -41,6 +43,11 @@ services:
 | 
				
			|||||||
      - ./data/redis:/data
 | 
					      - ./data/redis:/data
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Tip**: to generate a value for the `CHRLY_SIGNING_KEY` use the command below and then join it with a `base64:` prefix.
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					openssl genrsa 4096 | base64 -w0
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Chrly uses some volumes to persist storage for capes and Redis database. The configuration above mounts them to
 | 
					Chrly uses some volumes to persist storage for capes and Redis database. The configuration above mounts them to
 | 
				
			||||||
the host machine to do not lose data on container recreations.
 | 
					the host machine to do not lose data on container recreations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -48,11 +55,10 @@ the host machine to do not lose data on container recreations.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Application's configuration is based on the environment variables. You can adjust config by modifying `environment` key
 | 
					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.
 | 
					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`
 | 
					If environment variables have been changed, Docker will automatically recreate the container, so you only need to `up`
 | 
				
			||||||
and `up` it:
 | 
					it again:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
docker-compose stop app
 | 
					 | 
				
			||||||
docker-compose up -d app
 | 
					docker-compose up -d app
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -182,7 +188,7 @@ If something goes wrong, you can always access logs by executing `docker-compose
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Endpoints
 | 
					## Endpoints
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Each endpoint that accepts `username` as a part of an url takes it case insensitive. `.png` part can be omitted too.
 | 
					Each endpoint that accepts `username` as a part of an url takes it case-insensitive. The `.png` postfix can be omitted.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `GET /skins/{username}.png`
 | 
					#### `GET /skins/{username}.png`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -220,11 +226,71 @@ That request is handy in case when your server implements authentication for a g
 | 
				
			|||||||
operation) and you have to respond with hasJoined request with an actual user textures. You have to simply send request
 | 
					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.
 | 
					to the Chrly server and put the result in your hasJoined response.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### `GET /profile/{username}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This endpoint behaves exactly like the
 | 
				
			||||||
 | 
					[Mojang's UUID -> Profile + Skin/Cape endpoint](https://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape), but using
 | 
				
			||||||
 | 
					a username instead of the UUID. Just like in the Mojang's API, you can append `?unsigned=false` part to URL to sign
 | 
				
			||||||
 | 
					the `textures` property. If the textures for the requested username aren't found, it'll request them through the
 | 
				
			||||||
 | 
					Mojang's API, but the Mojang's signature will be discarded and the textures will be re-signed using the signature key
 | 
				
			||||||
 | 
					for your Chrly instance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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?"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The base64 `value` string for the `textures` property decoded:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```json
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    "timestamp": 1614387238630,
 | 
				
			||||||
 | 
					    "profileId": "0f657aa8bfbe415db7005750090d3af3",
 | 
				
			||||||
 | 
					    "profileName": "username",
 | 
				
			||||||
 | 
					    "textures": {
 | 
				
			||||||
 | 
					        "SKIN": {
 | 
				
			||||||
 | 
					            "url": "http://example.com/skin.png"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "CAPE": {
 | 
				
			||||||
 | 
					            "url": "http://example.com/cape.png"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If username can't be found locally and can't be obtained from the Mojang's API, empty response with `204` status code
 | 
				
			||||||
 | 
					will be sent.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Note that this endpoint will try to use the UUID for the stored profile in the database. This is an edge case, related
 | 
				
			||||||
 | 
					to the situation where the user is available in the database but has no textures, which caused them to be retrieved
 | 
				
			||||||
 | 
					from the Mojang's API.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### `GET /signature-verification-key`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This endpoint returns a public key that can be used to verify textures signatures. The key is provided in `DER` format,
 | 
				
			||||||
 | 
					so it can be used directly in the Authlib, without modifying the signature checking algorithm.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `GET /textures/signed/{username}`
 | 
					#### `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
 | 
					Actually, this is the [Ely.by](https://ely.by)'s feature called
 | 
				
			||||||
you have your own source of Mojang's signatures, then you can pass it with textures and it'll be displayed in response
 | 
					[Server Skins System](https://ely.by/server-skins-system), but if you have your own source of Mojang's signatures,
 | 
				
			||||||
of this endpoint. Received response should be directly sent to the client without any modification via game server API.
 | 
					then you can pass it with textures and it'll be displayed in response of this endpoint. Received response should be
 | 
				
			||||||
 | 
					directly sent to the client without any modification via game server API.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Response example:
 | 
					Response example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								di/di.go
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								di/di.go
									
									
									
									
									
								
							@@ -11,6 +11,7 @@ func New() (*di.Container, error) {
 | 
				
			|||||||
		mojangTextures,
 | 
							mojangTextures,
 | 
				
			||||||
		handlers,
 | 
							handlers,
 | 
				
			||||||
		server,
 | 
							server,
 | 
				
			||||||
 | 
							signer,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -104,6 +104,7 @@ func newSkinsystemHandler(
 | 
				
			|||||||
	skinsRepository SkinsRepository,
 | 
						skinsRepository SkinsRepository,
 | 
				
			||||||
	capesRepository CapesRepository,
 | 
						capesRepository CapesRepository,
 | 
				
			||||||
	mojangTexturesProvider MojangTexturesProvider,
 | 
						mojangTexturesProvider MojangTexturesProvider,
 | 
				
			||||||
 | 
						texturesSigner TexturesSigner,
 | 
				
			||||||
) *mux.Router {
 | 
					) *mux.Router {
 | 
				
			||||||
	config.SetDefault("textures.extra_param_name", "chrly")
 | 
						config.SetDefault("textures.extra_param_name", "chrly")
 | 
				
			||||||
	config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
 | 
						config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
 | 
				
			||||||
@@ -113,14 +114,14 @@ func newSkinsystemHandler(
 | 
				
			|||||||
		SkinsRepo:               skinsRepository,
 | 
							SkinsRepo:               skinsRepository,
 | 
				
			||||||
		CapesRepo:               capesRepository,
 | 
							CapesRepo:               capesRepository,
 | 
				
			||||||
		MojangTexturesProvider:  mojangTexturesProvider,
 | 
							MojangTexturesProvider:  mojangTexturesProvider,
 | 
				
			||||||
 | 
							TexturesSigner:          texturesSigner,
 | 
				
			||||||
		TexturesExtraParamName:  config.GetString("textures.extra_param_name"),
 | 
							TexturesExtraParamName:  config.GetString("textures.extra_param_name"),
 | 
				
			||||||
		TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
 | 
							TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
 | 
				
			||||||
	}).Handler()
 | 
						}).Handler()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newApiHandler(emitter Emitter, skinsRepository SkinsRepository) *mux.Router {
 | 
					func newApiHandler(skinsRepository SkinsRepository) *mux.Router {
 | 
				
			||||||
	return (&Api{
 | 
						return (&Api{
 | 
				
			||||||
		Emitter:   emitter,
 | 
					 | 
				
			||||||
		SkinsRepo: skinsRepository,
 | 
							SkinsRepo: skinsRepository,
 | 
				
			||||||
	}).Handler()
 | 
						}).Handler()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								di/server.go
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								di/server.go
									
									
									
									
									
								
							@@ -4,6 +4,7 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"runtime/debug"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/getsentry/raven-go"
 | 
						"github.com/getsentry/raven-go"
 | 
				
			||||||
@@ -42,13 +43,26 @@ func newServer(params serverParams) *http.Server {
 | 
				
			|||||||
	params.Config.SetDefault("server.host", "")
 | 
						params.Config.SetDefault("server.host", "")
 | 
				
			||||||
	params.Config.SetDefault("server.port", 80)
 | 
						params.Config.SetDefault("server.port", 80)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	handler := params.Handler
 | 
						var handler http.Handler
 | 
				
			||||||
	if params.Sentry != nil {
 | 
						if params.Sentry != nil {
 | 
				
			||||||
		// raven.Recoverer uses DefaultClient and nothing can be done about it
 | 
							// raven.Recoverer uses DefaultClient and nothing can be done about it
 | 
				
			||||||
		// To avoid code duplication, if the Sentry service is successfully initiated,
 | 
							// To avoid code duplication, if the Sentry service is successfully initiated,
 | 
				
			||||||
		// it will also replace DefaultClient, so raven.Recoverer will work with the instance
 | 
							// it will also replace DefaultClient, so raven.Recoverer will work with the instance
 | 
				
			||||||
		// created in the application constructor
 | 
							// created in the application constructor
 | 
				
			||||||
		handler = raven.Recoverer(handler)
 | 
							handler = raven.Recoverer(params.Handler)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Raven's Recoverer is prints the stacktrace and sets the corresponding status itself.
 | 
				
			||||||
 | 
							// But there is no magic and if you don't define a panic handler, Mux will just reset the connection
 | 
				
			||||||
 | 
							handler = http.HandlerFunc(func(request http.ResponseWriter, response *http.Request) {
 | 
				
			||||||
 | 
								defer func() {
 | 
				
			||||||
 | 
									if recovered := recover(); recovered != nil {
 | 
				
			||||||
 | 
										debug.PrintStack() // TODO: colorize output
 | 
				
			||||||
 | 
										request.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								params.Handler.ServeHTTP(request, response)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	address := fmt.Sprintf("%s:%d", params.Config.GetString("server.host"), params.Config.GetInt("server.port"))
 | 
						address := fmt.Sprintf("%s:%d", params.Config.GetString("server.host"), params.Config.GetInt("server.port"))
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										48
									
								
								di/signer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								di/signer.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					package di
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/x509"
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
 | 
						"encoding/pem"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"github.com/elyby/chrly/http"
 | 
				
			||||||
 | 
						. "github.com/elyby/chrly/signer"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/goava/di"
 | 
				
			||||||
 | 
						"github.com/spf13/viper"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var signer = di.Options(
 | 
				
			||||||
 | 
						di.Provide(newTexturesSigner,
 | 
				
			||||||
 | 
							di.As(new(http.TexturesSigner)),
 | 
				
			||||||
 | 
						),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newTexturesSigner(config *viper.Viper) (*Signer, error) {
 | 
				
			||||||
 | 
						keyStr := config.GetString("chrly.signing.key")
 | 
				
			||||||
 | 
						if keyStr == "" {
 | 
				
			||||||
 | 
							return nil, errors.New("chrly.signing.key must be set in order to sign textures")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var keyBytes []byte
 | 
				
			||||||
 | 
						if strings.HasPrefix(keyStr, "base64:") {
 | 
				
			||||||
 | 
							base64Value := keyStr[7:]
 | 
				
			||||||
 | 
							decodedKey, err := base64.URLEncoding.DecodeString(base64Value)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							keyBytes = decodedKey
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							keyBytes = []byte(keyStr)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rawPem, _ := pem.Decode(keyBytes)
 | 
				
			||||||
 | 
						key, err := x509.ParsePKCS1PrivateKey(rawPem.Bytes)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &Signer{Key: key}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								http/api.go
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								http/api.go
									
									
									
									
									
								
							@@ -43,7 +43,6 @@ func init() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Api struct {
 | 
					type Api struct {
 | 
				
			||||||
	Emitter
 | 
					 | 
				
			||||||
	SkinsRepo SkinsRepository
 | 
						SkinsRepo SkinsRepository
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -68,9 +67,7 @@ func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	record, err := ctx.findIdentityOrCleanup(identityId, username)
 | 
						record, err := ctx.findIdentityOrCleanup(identityId, username)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Emit("skinsystem:error", fmt.Errorf("error on requesting a skin from the repository: %w", err))
 | 
							panic(err)
 | 
				
			||||||
		apiServerError(resp)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if record == nil {
 | 
						if record == nil {
 | 
				
			||||||
@@ -94,9 +91,7 @@ func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	err = ctx.SkinsRepo.SaveSkin(record)
 | 
						err = ctx.SkinsRepo.SaveSkin(record)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Emit("skinsystem:error", fmt.Errorf("unable to save record to the repository: %w", err))
 | 
							panic(err)
 | 
				
			||||||
		apiServerError(resp)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp.WriteHeader(http.StatusCreated)
 | 
						resp.WriteHeader(http.StatusCreated)
 | 
				
			||||||
@@ -116,9 +111,7 @@ func (ctx *Api) deleteSkinByUsernameHandler(resp http.ResponseWriter, req *http.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter) {
 | 
					func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter) {
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Emit("skinsystem:error", fmt.Errorf("unable to find skin info from the repository: %w", err))
 | 
							panic(err)
 | 
				
			||||||
		apiServerError(resp)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if skin == nil {
 | 
						if skin == nil {
 | 
				
			||||||
@@ -128,9 +121,7 @@ func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	err = ctx.SkinsRepo.RemoveSkinByUserId(skin.UserId)
 | 
						err = ctx.SkinsRepo.RemoveSkinByUserId(skin.UserId)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.Emit("skinsystem:error", fmt.Errorf("cannot delete skin by error: %w", err))
 | 
							panic(err)
 | 
				
			||||||
		apiServerError(resp)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp.WriteHeader(http.StatusNoContent)
 | 
						resp.WriteHeader(http.StatusNoContent)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,7 +28,6 @@ type apiTestSuite struct {
 | 
				
			|||||||
	App *Api
 | 
						App *Api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	SkinsRepository *skinsRepositoryMock
 | 
						SkinsRepository *skinsRepositoryMock
 | 
				
			||||||
	Emitter         *emitterMock
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/********************
 | 
					/********************
 | 
				
			||||||
@@ -37,17 +36,14 @@ type apiTestSuite struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (suite *apiTestSuite) SetupTest() {
 | 
					func (suite *apiTestSuite) SetupTest() {
 | 
				
			||||||
	suite.SkinsRepository = &skinsRepositoryMock{}
 | 
						suite.SkinsRepository = &skinsRepositoryMock{}
 | 
				
			||||||
	suite.Emitter = &emitterMock{}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.App = &Api{
 | 
						suite.App = &Api{
 | 
				
			||||||
		SkinsRepo: suite.SkinsRepository,
 | 
							SkinsRepo: suite.SkinsRepository,
 | 
				
			||||||
		Emitter:   suite.Emitter,
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *apiTestSuite) TearDownTest() {
 | 
					func (suite *apiTestSuite) TearDownTest() {
 | 
				
			||||||
	suite.SkinsRepository.AssertExpectations(suite.T())
 | 
						suite.SkinsRepository.AssertExpectations(suite.T())
 | 
				
			||||||
	suite.Emitter.AssertExpectations(suite.T())
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *apiTestSuite) RunSubTest(name string, subTest func()) {
 | 
					func (suite *apiTestSuite) RunSubTest(name string, subTest func()) {
 | 
				
			||||||
@@ -72,6 +68,7 @@ type postSkinTestCase struct {
 | 
				
			|||||||
	Name       string
 | 
						Name       string
 | 
				
			||||||
	Form       io.Reader
 | 
						Form       io.Reader
 | 
				
			||||||
	BeforeTest func(suite *apiTestSuite)
 | 
						BeforeTest func(suite *apiTestSuite)
 | 
				
			||||||
 | 
						PanicErr   string
 | 
				
			||||||
	AfterTest  func(suite *apiTestSuite, response *http.Response)
 | 
						AfterTest  func(suite *apiTestSuite, response *http.Response)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -198,6 +195,22 @@ var postSkinTestsCases = []*postSkinTestCase{
 | 
				
			|||||||
	},
 | 
						},
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		Name: "Handle an error when loading the data from the repository",
 | 
							Name: "Handle an error when loading the data from the repository",
 | 
				
			||||||
 | 
							Form: bytes.NewBufferString(url.Values{
 | 
				
			||||||
 | 
								"identityId": {"1"},
 | 
				
			||||||
 | 
								"username":   {"changed_username"},
 | 
				
			||||||
 | 
								"uuid":       {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
 | 
				
			||||||
 | 
								"skinId":     {"5"},
 | 
				
			||||||
 | 
								"is1_8":      {"0"},
 | 
				
			||||||
 | 
								"isSlim":     {"0"},
 | 
				
			||||||
 | 
								"url":        {"http://example.com/skin.png"},
 | 
				
			||||||
 | 
							}.Encode()),
 | 
				
			||||||
 | 
							BeforeTest: func(suite *apiTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, errors.New("can't find skin by user id"))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							PanicErr: "can't find skin by user id",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Handle an error when saving the data into the repository",
 | 
				
			||||||
		Form: bytes.NewBufferString(url.Values{
 | 
							Form: bytes.NewBufferString(url.Values{
 | 
				
			||||||
			"identityId": {"1"},
 | 
								"identityId": {"1"},
 | 
				
			||||||
			"username":   {"mock_username"},
 | 
								"username":   {"mock_username"},
 | 
				
			||||||
@@ -209,43 +222,9 @@ var postSkinTestsCases = []*postSkinTestCase{
 | 
				
			|||||||
		}.Encode()),
 | 
							}.Encode()),
 | 
				
			||||||
		BeforeTest: func(suite *apiTestSuite) {
 | 
							BeforeTest: func(suite *apiTestSuite) {
 | 
				
			||||||
			suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil)
 | 
								suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil)
 | 
				
			||||||
			err := errors.New("mock error")
 | 
								suite.SkinsRepository.On("SaveSkin", mock.Anything).Return(errors.New("can't save textures"))
 | 
				
			||||||
			suite.SkinsRepository.On("SaveSkin", mock.Anything).Return(err)
 | 
					 | 
				
			||||||
			suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool {
 | 
					 | 
				
			||||||
				return cErr.Error() == "unable to save record to the repository: mock error" &&
 | 
					 | 
				
			||||||
					errors.Is(cErr, err)
 | 
					 | 
				
			||||||
			})).Once()
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		AfterTest: func(suite *apiTestSuite, response *http.Response) {
 | 
					 | 
				
			||||||
			suite.Equal(500, response.StatusCode)
 | 
					 | 
				
			||||||
			body, _ := ioutil.ReadAll(response.Body)
 | 
					 | 
				
			||||||
			suite.Empty(body)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		Name: "Handle an error when saving the data into the repository",
 | 
					 | 
				
			||||||
		Form: bytes.NewBufferString(url.Values{
 | 
					 | 
				
			||||||
			"identityId": {"1"},
 | 
					 | 
				
			||||||
			"username":   {"changed_username"},
 | 
					 | 
				
			||||||
			"uuid":       {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
 | 
					 | 
				
			||||||
			"skinId":     {"5"},
 | 
					 | 
				
			||||||
			"is1_8":      {"0"},
 | 
					 | 
				
			||||||
			"isSlim":     {"0"},
 | 
					 | 
				
			||||||
			"url":        {"http://example.com/skin.png"},
 | 
					 | 
				
			||||||
		}.Encode()),
 | 
					 | 
				
			||||||
		BeforeTest: func(suite *apiTestSuite) {
 | 
					 | 
				
			||||||
			err := errors.New("mock error")
 | 
					 | 
				
			||||||
			suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, err)
 | 
					 | 
				
			||||||
			suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool {
 | 
					 | 
				
			||||||
				return cErr.Error() == "error on requesting a skin from the repository: mock error" &&
 | 
					 | 
				
			||||||
					errors.Is(cErr, err)
 | 
					 | 
				
			||||||
			})).Once()
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		AfterTest: func(suite *apiTestSuite, response *http.Response) {
 | 
					 | 
				
			||||||
			suite.Equal(500, response.StatusCode)
 | 
					 | 
				
			||||||
			body, _ := ioutil.ReadAll(response.Body)
 | 
					 | 
				
			||||||
			suite.Empty(body)
 | 
					 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							PanicErr: "can't save textures",
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -258,9 +237,14 @@ func (suite *apiTestSuite) TestPostSkin() {
 | 
				
			|||||||
			req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 | 
								req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 | 
				
			||||||
			w := httptest.NewRecorder()
 | 
								w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			suite.App.Handler().ServeHTTP(w, req)
 | 
								if testCase.PanicErr != "" {
 | 
				
			||||||
 | 
									suite.PanicsWithError(testCase.PanicErr, func() {
 | 
				
			||||||
			testCase.AfterTest(suite, w.Result())
 | 
										suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									testCase.AfterTest(suite, w.Result())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,7 +36,7 @@ func StartServer(server *http.Server, logger slf.Logger) {
 | 
				
			|||||||
	go func() {
 | 
						go func() {
 | 
				
			||||||
		s := waitForExitSignal()
 | 
							s := waitForExitSignal()
 | 
				
			||||||
		logger.Info("Got signal: :signal, starting graceful shutdown", wd.StringParam("signal", s.String()))
 | 
							logger.Info("Got signal: :signal, starting graceful shutdown", wd.StringParam("signal", s.String()))
 | 
				
			||||||
		server.Shutdown(context.Background())
 | 
							_ = server.Shutdown(context.Background())
 | 
				
			||||||
		logger.Info("Graceful shutdown succeed, exiting", wd.StringParam("signal", s.String()))
 | 
							logger.Info("Graceful shutdown succeed, exiting", wd.StringParam("signal", s.String()))
 | 
				
			||||||
		close(done)
 | 
							close(done)
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
@@ -135,7 +135,3 @@ func apiNotFound(resp http.ResponseWriter, reason string) {
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
	_, _ = resp.Write(result)
 | 
						_, _ = resp.Write(result)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func apiServerError(resp http.ResponseWriter) {
 | 
					 | 
				
			||||||
	resp.WriteHeader(http.StatusInternalServerError)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,15 @@
 | 
				
			|||||||
package http
 | 
					package http
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/rsa"
 | 
				
			||||||
 | 
						"crypto/x509"
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"github.com/elyby/chrly/utils"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/gorilla/mux"
 | 
						"github.com/gorilla/mux"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,6 +17,8 @@ import (
 | 
				
			|||||||
	"github.com/elyby/chrly/model"
 | 
						"github.com/elyby/chrly/model"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var timeNow = time.Now
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SkinsRepository interface {
 | 
					type SkinsRepository interface {
 | 
				
			||||||
	FindSkinByUsername(username string) (*model.Skin, error)
 | 
						FindSkinByUsername(username string) (*model.Skin, error)
 | 
				
			||||||
	FindSkinByUserId(id int) (*model.Skin, error)
 | 
						FindSkinByUserId(id int) (*model.Skin, error)
 | 
				
			||||||
@@ -28,15 +35,30 @@ type MojangTexturesProvider interface {
 | 
				
			|||||||
	GetForUsername(username string) (*mojang.SignedTexturesResponse, error)
 | 
						GetForUsername(username string) (*mojang.SignedTexturesResponse, error)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TexturesSigner interface {
 | 
				
			||||||
 | 
						SignTextures(textures string) (string, error)
 | 
				
			||||||
 | 
						GetPublicKey() (*rsa.PublicKey, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Skinsystem struct {
 | 
					type Skinsystem struct {
 | 
				
			||||||
	Emitter
 | 
						Emitter
 | 
				
			||||||
	SkinsRepo               SkinsRepository
 | 
						SkinsRepo               SkinsRepository
 | 
				
			||||||
	CapesRepo               CapesRepository
 | 
						CapesRepo               CapesRepository
 | 
				
			||||||
	MojangTexturesProvider  MojangTexturesProvider
 | 
						MojangTexturesProvider  MojangTexturesProvider
 | 
				
			||||||
 | 
						TexturesSigner          TexturesSigner
 | 
				
			||||||
	TexturesExtraParamName  string
 | 
						TexturesExtraParamName  string
 | 
				
			||||||
	TexturesExtraParamValue string
 | 
						TexturesExtraParamValue string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type profile struct {
 | 
				
			||||||
 | 
						Id              string
 | 
				
			||||||
 | 
						Username        string
 | 
				
			||||||
 | 
						Textures        *mojang.TexturesResponse
 | 
				
			||||||
 | 
						CapeFile        io.Reader
 | 
				
			||||||
 | 
						MojangTextures  string
 | 
				
			||||||
 | 
						MojangSignature string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ctx *Skinsystem) Handler() *mux.Router {
 | 
					func (ctx *Skinsystem) Handler() *mux.Router {
 | 
				
			||||||
	router := mux.NewRouter().StrictSlash(true)
 | 
						router := mux.NewRouter().StrictSlash(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -44,40 +66,28 @@ func (ctx *Skinsystem) Handler() *mux.Router {
 | 
				
			|||||||
	router.HandleFunc("/cloaks/{username}", ctx.capeHandler).Methods(http.MethodGet).Name("cloaks")
 | 
						router.HandleFunc("/cloaks/{username}", ctx.capeHandler).Methods(http.MethodGet).Name("cloaks")
 | 
				
			||||||
	router.HandleFunc("/textures/{username}", ctx.texturesHandler).Methods(http.MethodGet)
 | 
						router.HandleFunc("/textures/{username}", ctx.texturesHandler).Methods(http.MethodGet)
 | 
				
			||||||
	router.HandleFunc("/textures/signed/{username}", ctx.signedTexturesHandler).Methods(http.MethodGet)
 | 
						router.HandleFunc("/textures/signed/{username}", ctx.signedTexturesHandler).Methods(http.MethodGet)
 | 
				
			||||||
 | 
						router.HandleFunc("/profile/{username}", ctx.profileHandler).Methods(http.MethodGet)
 | 
				
			||||||
	// Legacy
 | 
						// Legacy
 | 
				
			||||||
	router.HandleFunc("/skins", ctx.skinGetHandler).Methods(http.MethodGet)
 | 
						router.HandleFunc("/skins", ctx.skinGetHandler).Methods(http.MethodGet)
 | 
				
			||||||
	router.HandleFunc("/cloaks", ctx.capeGetHandler).Methods(http.MethodGet)
 | 
						router.HandleFunc("/cloaks", ctx.capeGetHandler).Methods(http.MethodGet)
 | 
				
			||||||
 | 
						// Utils
 | 
				
			||||||
 | 
						router.HandleFunc("/signature-verification-key", ctx.signatureVerificationKeyHandler).Methods(http.MethodGet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return router
 | 
						return router
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) {
 | 
					func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
	username := parseUsername(mux.Vars(request)["username"])
 | 
						profile, err := ctx.getProfile(request, true)
 | 
				
			||||||
	rec, err := ctx.SkinsRepo.FindSkinByUsername(username)
 | 
						if err != nil {
 | 
				
			||||||
	if err == nil && rec != nil && rec.SkinId != 0 {
 | 
							panic(err)
 | 
				
			||||||
		http.Redirect(response, request, rec.Url, 301)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
 | 
						if profile == nil || profile.Textures == nil || profile.Textures.Skin == nil {
 | 
				
			||||||
	if err != nil || mojangTextures == nil {
 | 
					 | 
				
			||||||
		response.WriteHeader(http.StatusNotFound)
 | 
							response.WriteHeader(http.StatusNotFound)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	texturesProp, _ := mojangTextures.DecodeTextures()
 | 
						http.Redirect(response, request, profile.Textures.Skin.Url, 301)
 | 
				
			||||||
	if texturesProp == nil {
 | 
					 | 
				
			||||||
		response.WriteHeader(http.StatusNotFound)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	skin := texturesProp.Textures.Skin
 | 
					 | 
				
			||||||
	if skin == nil {
 | 
					 | 
				
			||||||
		response.WriteHeader(http.StatusNotFound)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	http.Redirect(response, request, skin.Url, 301)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *http.Request) {
 | 
					func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
@@ -88,39 +98,27 @@ func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *htt
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mux.Vars(request)["username"] = username
 | 
						mux.Vars(request)["username"] = username
 | 
				
			||||||
	mux.Vars(request)["converted"] = "1"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.skinHandler(response, request)
 | 
						ctx.skinHandler(response, request)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) {
 | 
					func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
	username := parseUsername(mux.Vars(request)["username"])
 | 
						profile, err := ctx.getProfile(request, true)
 | 
				
			||||||
	rec, err := ctx.CapesRepo.FindCapeByUsername(username)
 | 
						if err != nil {
 | 
				
			||||||
	if err == nil && rec != nil {
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if profile == nil || profile.Textures == nil || (profile.CapeFile == nil && profile.Textures.Cape == nil) {
 | 
				
			||||||
 | 
							response.WriteHeader(http.StatusNotFound)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if profile.CapeFile == nil {
 | 
				
			||||||
 | 
							http.Redirect(response, request, profile.Textures.Cape.Url, 301)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
		request.Header.Set("Content-Type", "image/png")
 | 
							request.Header.Set("Content-Type", "image/png")
 | 
				
			||||||
		_, _ = io.Copy(response, rec.File)
 | 
							_, _ = io.Copy(response, profile.CapeFile)
 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
 | 
					 | 
				
			||||||
	if err != nil || mojangTextures == nil {
 | 
					 | 
				
			||||||
		response.WriteHeader(http.StatusNotFound)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	texturesProp, _ := mojangTextures.DecodeTextures()
 | 
					 | 
				
			||||||
	if texturesProp == nil {
 | 
					 | 
				
			||||||
		response.WriteHeader(http.StatusNotFound)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cape := texturesProp.Textures.Cape
 | 
					 | 
				
			||||||
	if cape == nil {
 | 
					 | 
				
			||||||
		response.WriteHeader(http.StatusNotFound)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	http.Redirect(response, request, cape.Url, 301)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *http.Request) {
 | 
					func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
@@ -131,104 +129,219 @@ func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *htt
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mux.Vars(request)["username"] = username
 | 
						mux.Vars(request)["username"] = username
 | 
				
			||||||
	mux.Vars(request)["converted"] = "1"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.capeHandler(response, request)
 | 
						ctx.capeHandler(response, request)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *http.Request) {
 | 
					func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
	username := parseUsername(mux.Vars(request)["username"])
 | 
						profile, err := ctx.getProfile(request, true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
	var textures *mojang.TexturesResponse
 | 
							panic(err)
 | 
				
			||||||
	skin, skinErr := ctx.SkinsRepo.FindSkinByUsername(username)
 | 
					 | 
				
			||||||
	cape, capeErr := ctx.CapesRepo.FindCapeByUsername(username)
 | 
					 | 
				
			||||||
	if (skinErr == nil && skin != nil && skin.SkinId != 0) || (capeErr == nil && cape != nil) {
 | 
					 | 
				
			||||||
		textures = &mojang.TexturesResponse{}
 | 
					 | 
				
			||||||
		if skinErr == nil && skin != nil && skin.SkinId != 0 {
 | 
					 | 
				
			||||||
			skinTextures := &mojang.SkinTexturesResponse{
 | 
					 | 
				
			||||||
				Url: skin.Url,
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if skin.IsSlim {
 | 
					 | 
				
			||||||
				skinTextures.Metadata = &mojang.SkinTexturesMetadata{
 | 
					 | 
				
			||||||
					Model: "slim",
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			textures.Skin = skinTextures
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if capeErr == nil && cape != nil {
 | 
					 | 
				
			||||||
			textures.Cape = &mojang.CapeTexturesResponse{
 | 
					 | 
				
			||||||
				// Use statically http since the application doesn't support TLS
 | 
					 | 
				
			||||||
				Url: "http://" + request.Host + "/cloaks/" + username,
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
 | 
					 | 
				
			||||||
		if err != nil || mojangTextures == nil {
 | 
					 | 
				
			||||||
			response.WriteHeader(http.StatusNoContent)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		texturesProp, _ := mojangTextures.DecodeTextures()
 | 
					 | 
				
			||||||
		if texturesProp == nil {
 | 
					 | 
				
			||||||
			response.WriteHeader(http.StatusNoContent)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		textures = texturesProp.Textures
 | 
					 | 
				
			||||||
		if textures.Skin == nil && textures.Cape == nil {
 | 
					 | 
				
			||||||
			response.WriteHeader(http.StatusNoContent)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	responseData, _ := json.Marshal(textures)
 | 
						if profile == nil || profile.Textures == nil || (profile.Textures.Skin == nil && profile.Textures.Cape == nil) {
 | 
				
			||||||
 | 
							response.WriteHeader(http.StatusNoContent)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						responseData, _ := json.Marshal(profile.Textures)
 | 
				
			||||||
	response.Header().Set("Content-Type", "application/json")
 | 
						response.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
	_, _ = response.Write(responseData)
 | 
						_, _ = response.Write(responseData)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ctx *Skinsystem) signedTexturesHandler(response http.ResponseWriter, request *http.Request) {
 | 
					func (ctx *Skinsystem) signedTexturesHandler(response http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
	username := parseUsername(mux.Vars(request)["username"])
 | 
						profile, err := ctx.getProfile(request, request.URL.Query().Get("proxy") != "")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
	var responseData *mojang.SignedTexturesResponse
 | 
							panic(err)
 | 
				
			||||||
 | 
					 | 
				
			||||||
	rec, err := ctx.SkinsRepo.FindSkinByUsername(username)
 | 
					 | 
				
			||||||
	if err == nil && rec != nil && rec.SkinId != 0 && rec.MojangTextures != "" {
 | 
					 | 
				
			||||||
		responseData = &mojang.SignedTexturesResponse{
 | 
					 | 
				
			||||||
			Id:   strings.Replace(rec.Uuid, "-", "", -1),
 | 
					 | 
				
			||||||
			Name: rec.Username,
 | 
					 | 
				
			||||||
			Props: []*mojang.Property{
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					Name:      "textures",
 | 
					 | 
				
			||||||
					Signature: rec.MojangSignature,
 | 
					 | 
				
			||||||
					Value:     rec.MojangTextures,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else if request.URL.Query().Get("proxy") != "" {
 | 
					 | 
				
			||||||
		mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
 | 
					 | 
				
			||||||
		if err == nil && mojangTextures != nil {
 | 
					 | 
				
			||||||
			responseData = mojangTextures
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if responseData == nil {
 | 
						if profile == nil || profile.MojangTextures == "" {
 | 
				
			||||||
		response.WriteHeader(http.StatusNoContent)
 | 
							response.WriteHeader(http.StatusNoContent)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	responseData.Props = append(responseData.Props, &mojang.Property{
 | 
						profileResponse := &mojang.SignedTexturesResponse{
 | 
				
			||||||
		Name:  ctx.TexturesExtraParamName,
 | 
							Id:   profile.Id,
 | 
				
			||||||
		Value: ctx.TexturesExtraParamValue,
 | 
							Name: profile.Username,
 | 
				
			||||||
	})
 | 
							Props: []*mojang.Property{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Name:      "textures",
 | 
				
			||||||
 | 
									Signature: profile.MojangSignature,
 | 
				
			||||||
 | 
									Value:     profile.MojangTextures,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Name:  ctx.TexturesExtraParamName,
 | 
				
			||||||
 | 
									Value: ctx.TexturesExtraParamValue,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	responseJson, _ := json.Marshal(responseData)
 | 
						responseJson, _ := json.Marshal(profileResponse)
 | 
				
			||||||
	response.Header().Set("Content-Type", "application/json")
 | 
						response.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
	_, _ = response.Write(responseJson)
 | 
						_, _ = response.Write(responseJson)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (ctx *Skinsystem) profileHandler(response http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
 | 
						profile, err := ctx.getProfile(request, true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if profile == nil {
 | 
				
			||||||
 | 
							response.WriteHeader(http.StatusNoContent)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						texturesPropContent := &mojang.TexturesProp{
 | 
				
			||||||
 | 
							Timestamp:   utils.UnixMillisecond(timeNow()),
 | 
				
			||||||
 | 
							ProfileID:   profile.Id,
 | 
				
			||||||
 | 
							ProfileName: profile.Username,
 | 
				
			||||||
 | 
							Textures:    profile.Textures,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						texturesPropValueJson, _ := json.Marshal(texturesPropContent)
 | 
				
			||||||
 | 
						texturesPropEncodedValue := base64.StdEncoding.EncodeToString(texturesPropValueJson)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						texturesProp := &mojang.Property{
 | 
				
			||||||
 | 
							Name:  "textures",
 | 
				
			||||||
 | 
							Value: texturesPropEncodedValue,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if request.URL.Query().Get("unsigned") == "false" {
 | 
				
			||||||
 | 
							signature, err := ctx.TexturesSigner.SignTextures(texturesProp.Value)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								panic(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							texturesProp.Signature = signature
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						profileResponse := &mojang.SignedTexturesResponse{
 | 
				
			||||||
 | 
							Id:   profile.Id,
 | 
				
			||||||
 | 
							Name: profile.Username,
 | 
				
			||||||
 | 
							Props: []*mojang.Property{
 | 
				
			||||||
 | 
								texturesProp,
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Name:  ctx.TexturesExtraParamName,
 | 
				
			||||||
 | 
									Value: ctx.TexturesExtraParamValue,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						responseJson, _ := json.Marshal(profileResponse)
 | 
				
			||||||
 | 
						response.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
 | 
						_, _ = response.Write(responseJson)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (ctx *Skinsystem) signatureVerificationKeyHandler(response http.ResponseWriter, request *http.Request) {
 | 
				
			||||||
 | 
						publicKey, err := ctx.TexturesSigner.GetPublicKey()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						asn1Bytes, err := x509.MarshalPKIXPublicKey(publicKey)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, _ = response.Write(asn1Bytes)
 | 
				
			||||||
 | 
						response.Header().Set("Content-Type", "application/octet-stream")
 | 
				
			||||||
 | 
						response.Header().Set("Content-Disposition", "attachment; filename=\"yggdrasil_session_pubkey.der\"")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: in v5 should be extracted into some ProfileProvider interface,
 | 
				
			||||||
 | 
					//       which will encapsulate all logics, declared in this method
 | 
				
			||||||
 | 
					func (ctx *Skinsystem) getProfile(request *http.Request, proxy bool) (*profile, error) {
 | 
				
			||||||
 | 
						username := parseUsername(mux.Vars(request)["username"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						skin, err := ctx.SkinsRepo.FindSkinByUsername(username)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						profile := &profile{
 | 
				
			||||||
 | 
							Id:              "",
 | 
				
			||||||
 | 
							Username:        "",
 | 
				
			||||||
 | 
							Textures:        &mojang.TexturesResponse{}, // Field must be initialized to avoid "null" after json encoding
 | 
				
			||||||
 | 
							CapeFile:        nil,
 | 
				
			||||||
 | 
							MojangTextures:  "",
 | 
				
			||||||
 | 
							MojangSignature: "",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if skin != nil {
 | 
				
			||||||
 | 
							profile.Id = strings.Replace(skin.Uuid, "-", "", -1)
 | 
				
			||||||
 | 
							profile.Username = skin.Username
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if skin != nil && skin.SkinId != 0 {
 | 
				
			||||||
 | 
							profile.Textures.Skin = &mojang.SkinTexturesResponse{
 | 
				
			||||||
 | 
								Url: skin.Url,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if skin.IsSlim {
 | 
				
			||||||
 | 
								profile.Textures.Skin.Metadata = &mojang.SkinTexturesMetadata{
 | 
				
			||||||
 | 
									Model: "slim",
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							cape, _ := ctx.CapesRepo.FindCapeByUsername(username)
 | 
				
			||||||
 | 
							if cape != nil {
 | 
				
			||||||
 | 
								profile.CapeFile = cape.File
 | 
				
			||||||
 | 
								profile.Textures.Cape = &mojang.CapeTexturesResponse{
 | 
				
			||||||
 | 
									// Use statically http since the application doesn't support TLS
 | 
				
			||||||
 | 
									Url: "http://" + request.Host + "/cloaks/" + username,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							profile.MojangTextures = skin.MojangTextures
 | 
				
			||||||
 | 
							profile.MojangSignature = skin.MojangSignature
 | 
				
			||||||
 | 
						} else if proxy {
 | 
				
			||||||
 | 
							mojangProfile, err := ctx.MojangTexturesProvider.GetForUsername(username)
 | 
				
			||||||
 | 
							// If we at least know something about a user,
 | 
				
			||||||
 | 
							// than we can ignore an error and return profile without textures
 | 
				
			||||||
 | 
							if err != nil && profile.Id != "" {
 | 
				
			||||||
 | 
								return profile, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err != nil || mojangProfile == nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							decodedTextures, err := mojangProfile.DecodeTextures()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// There might be no textures property
 | 
				
			||||||
 | 
							if decodedTextures != nil {
 | 
				
			||||||
 | 
								profile.Textures = decodedTextures.Textures
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var texturesProp *mojang.Property
 | 
				
			||||||
 | 
							for _, prop := range mojangProfile.Props {
 | 
				
			||||||
 | 
								if prop.Name == "textures" {
 | 
				
			||||||
 | 
									texturesProp = prop
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if texturesProp != nil {
 | 
				
			||||||
 | 
								profile.MojangTextures = texturesProp.Value
 | 
				
			||||||
 | 
								profile.MojangSignature = texturesProp.Signature
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If user id is unknown at this point, then use values from Mojang profile
 | 
				
			||||||
 | 
							if profile.Id == "" {
 | 
				
			||||||
 | 
								profile.Id = mojangProfile.Id
 | 
				
			||||||
 | 
								profile.Username = mojangProfile.Name
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return profile, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func parseUsername(username string) string {
 | 
					func parseUsername(username string) string {
 | 
				
			||||||
	return strings.TrimSuffix(username, ".png")
 | 
						return strings.TrimSuffix(username, ".png")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,10 @@ package http
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
 | 
						"crypto/rsa"
 | 
				
			||||||
 | 
						"crypto/x509"
 | 
				
			||||||
 | 
						"encoding/pem"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"image"
 | 
						"image"
 | 
				
			||||||
	"image/png"
 | 
						"image/png"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
@@ -89,6 +93,25 @@ func (m *mojangTexturesProviderMock) GetForUsername(username string) (*mojang.Si
 | 
				
			|||||||
	return result, args.Error(1)
 | 
						return result, args.Error(1)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type texturesSignerMock struct {
 | 
				
			||||||
 | 
						mock.Mock
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *texturesSignerMock) SignTextures(textures string) (string, error) {
 | 
				
			||||||
 | 
						args := m.Called(textures)
 | 
				
			||||||
 | 
						return args.String(0), args.Error(1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *texturesSignerMock) GetPublicKey() (*rsa.PublicKey, error) {
 | 
				
			||||||
 | 
						args := m.Called()
 | 
				
			||||||
 | 
						var publicKey *rsa.PublicKey
 | 
				
			||||||
 | 
						if casted, ok := args.Get(0).(*rsa.PublicKey); ok {
 | 
				
			||||||
 | 
							publicKey = casted
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return publicKey, args.Error(1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type skinsystemTestSuite struct {
 | 
					type skinsystemTestSuite struct {
 | 
				
			||||||
	suite.Suite
 | 
						suite.Suite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -97,6 +120,7 @@ type skinsystemTestSuite struct {
 | 
				
			|||||||
	SkinsRepository        *skinsRepositoryMock
 | 
						SkinsRepository        *skinsRepositoryMock
 | 
				
			||||||
	CapesRepository        *capesRepositoryMock
 | 
						CapesRepository        *capesRepositoryMock
 | 
				
			||||||
	MojangTexturesProvider *mojangTexturesProviderMock
 | 
						MojangTexturesProvider *mojangTexturesProviderMock
 | 
				
			||||||
 | 
						TexturesSigner         *texturesSignerMock
 | 
				
			||||||
	Emitter                *emitterMock
 | 
						Emitter                *emitterMock
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -105,15 +129,22 @@ type skinsystemTestSuite struct {
 | 
				
			|||||||
 ********************/
 | 
					 ********************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *skinsystemTestSuite) SetupTest() {
 | 
					func (suite *skinsystemTestSuite) SetupTest() {
 | 
				
			||||||
 | 
						timeNow = func() time.Time {
 | 
				
			||||||
 | 
							CET, _ := time.LoadLocation("CET")
 | 
				
			||||||
 | 
							return time.Date(2021, 02, 25, 01, 50, 23, 0, CET)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.SkinsRepository = &skinsRepositoryMock{}
 | 
						suite.SkinsRepository = &skinsRepositoryMock{}
 | 
				
			||||||
	suite.CapesRepository = &capesRepositoryMock{}
 | 
						suite.CapesRepository = &capesRepositoryMock{}
 | 
				
			||||||
	suite.MojangTexturesProvider = &mojangTexturesProviderMock{}
 | 
						suite.MojangTexturesProvider = &mojangTexturesProviderMock{}
 | 
				
			||||||
 | 
						suite.TexturesSigner = &texturesSignerMock{}
 | 
				
			||||||
	suite.Emitter = &emitterMock{}
 | 
						suite.Emitter = &emitterMock{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.App = &Skinsystem{
 | 
						suite.App = &Skinsystem{
 | 
				
			||||||
		SkinsRepo:               suite.SkinsRepository,
 | 
							SkinsRepo:               suite.SkinsRepository,
 | 
				
			||||||
		CapesRepo:               suite.CapesRepository,
 | 
							CapesRepo:               suite.CapesRepository,
 | 
				
			||||||
		MojangTexturesProvider:  suite.MojangTexturesProvider,
 | 
							MojangTexturesProvider:  suite.MojangTexturesProvider,
 | 
				
			||||||
 | 
							TexturesSigner:          suite.TexturesSigner,
 | 
				
			||||||
		Emitter:                 suite.Emitter,
 | 
							Emitter:                 suite.Emitter,
 | 
				
			||||||
		TexturesExtraParamName:  "texturesParamName",
 | 
							TexturesExtraParamName:  "texturesParamName",
 | 
				
			||||||
		TexturesExtraParamValue: "texturesParamValue",
 | 
							TexturesExtraParamValue: "texturesParamValue",
 | 
				
			||||||
@@ -124,6 +155,7 @@ func (suite *skinsystemTestSuite) TearDownTest() {
 | 
				
			|||||||
	suite.SkinsRepository.AssertExpectations(suite.T())
 | 
						suite.SkinsRepository.AssertExpectations(suite.T())
 | 
				
			||||||
	suite.CapesRepository.AssertExpectations(suite.T())
 | 
						suite.CapesRepository.AssertExpectations(suite.T())
 | 
				
			||||||
	suite.MojangTexturesProvider.AssertExpectations(suite.T())
 | 
						suite.MojangTexturesProvider.AssertExpectations(suite.T())
 | 
				
			||||||
 | 
						suite.TexturesSigner.AssertExpectations(suite.T())
 | 
				
			||||||
	suite.Emitter.AssertExpectations(suite.T())
 | 
						suite.Emitter.AssertExpectations(suite.T())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -144,6 +176,7 @@ func TestSkinsystem(t *testing.T) {
 | 
				
			|||||||
type skinsystemTestCase struct {
 | 
					type skinsystemTestCase struct {
 | 
				
			||||||
	Name       string
 | 
						Name       string
 | 
				
			||||||
	BeforeTest func(suite *skinsystemTestSuite)
 | 
						BeforeTest func(suite *skinsystemTestSuite)
 | 
				
			||||||
 | 
						PanicErr   string
 | 
				
			||||||
	AfterTest  func(suite *skinsystemTestSuite, response *http.Response)
 | 
						AfterTest  func(suite *skinsystemTestSuite, response *http.Response)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -156,6 +189,7 @@ var skinsTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
		Name: "Username exists in the local storage",
 | 
							Name: "Username exists in the local storage",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
 | 
				
			||||||
 | 
								suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
			suite.Equal(301, response.StatusCode)
 | 
								suite.Equal(301, response.StatusCode)
 | 
				
			||||||
@@ -203,6 +237,13 @@ var skinsTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
			suite.Equal(404, response.StatusCode)
 | 
								suite.Equal(404, response.StatusCode)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Receive an error from the SkinsRepository",
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							PanicErr: "skins repository error",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *skinsystemTestSuite) TestSkin() {
 | 
					func (suite *skinsystemTestSuite) TestSkin() {
 | 
				
			||||||
@@ -213,14 +254,20 @@ func (suite *skinsystemTestSuite) TestSkin() {
 | 
				
			|||||||
			req := httptest.NewRequest("GET", "http://chrly/skins/mock_username", nil)
 | 
								req := httptest.NewRequest("GET", "http://chrly/skins/mock_username", nil)
 | 
				
			||||||
			w := httptest.NewRecorder()
 | 
								w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			suite.App.Handler().ServeHTTP(w, req)
 | 
								if testCase.PanicErr != "" {
 | 
				
			||||||
 | 
									suite.PanicsWithError(testCase.PanicErr, func() {
 | 
				
			||||||
			testCase.AfterTest(suite, w.Result())
 | 
										suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									testCase.AfterTest(suite, w.Result())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.RunSubTest("Pass username with png extension", func() {
 | 
						suite.RunSubTest("Pass username with png extension", func() {
 | 
				
			||||||
		suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
 | 
							suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
 | 
				
			||||||
 | 
							suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil)
 | 
							req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil)
 | 
				
			||||||
		w := httptest.NewRecorder()
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
@@ -241,14 +288,18 @@ func (suite *skinsystemTestSuite) TestSkinGET() {
 | 
				
			|||||||
			req := httptest.NewRequest("GET", "http://chrly/skins?name=mock_username", nil)
 | 
								req := httptest.NewRequest("GET", "http://chrly/skins?name=mock_username", nil)
 | 
				
			||||||
			w := httptest.NewRecorder()
 | 
								w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			suite.App.Handler().ServeHTTP(w, req)
 | 
								if testCase.PanicErr != "" {
 | 
				
			||||||
 | 
									suite.PanicsWithError(testCase.PanicErr, func() {
 | 
				
			||||||
			testCase.AfterTest(suite, w.Result())
 | 
										suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									testCase.AfterTest(suite, w.Result())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.RunSubTest("Do not pass name param", func() {
 | 
						suite.RunSubTest("Do not pass name param", func() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
		req := httptest.NewRequest("GET", "http://chrly/skins", nil)
 | 
							req := httptest.NewRequest("GET", "http://chrly/skins", nil)
 | 
				
			||||||
		w := httptest.NewRecorder()
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -267,6 +318,7 @@ var capesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		Name: "Username exists in the local storage",
 | 
							Name: "Username exists in the local storage",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
 | 
				
			||||||
			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
 | 
								suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
@@ -279,7 +331,7 @@ var capesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
 | 
							Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(true, true), nil)
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(true, true), nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
@@ -290,7 +342,7 @@ var capesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		Name: "Username doesn't exists on the local storage, but exists on Mojang and has no cape texture",
 | 
							Name: "Username doesn't exists on the local storage, but exists on Mojang and has no cape texture",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(false, false), nil)
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(false, false), nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
@@ -300,7 +352,7 @@ var capesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		Name: "Username doesn't exists on the local storage, but exists on Mojang and has an empty properties",
 | 
							Name: "Username doesn't exists on the local storage, but exists on Mojang and has an empty properties",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createEmptyMojangResponse(), nil)
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createEmptyMojangResponse(), nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
@@ -310,13 +362,20 @@ var capesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
	{
 | 
						{
 | 
				
			||||||
		Name: "Username doesn't exists on the local storage and doesn't exists on Mojang",
 | 
							Name: "Username doesn't exists on the local storage and doesn't exists on Mojang",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
			suite.Equal(404, response.StatusCode)
 | 
								suite.Equal(404, response.StatusCode)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Receive an error from the SkinsRepository",
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							PanicErr: "skins repository error",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *skinsystemTestSuite) TestCape() {
 | 
					func (suite *skinsystemTestSuite) TestCape() {
 | 
				
			||||||
@@ -327,13 +386,19 @@ func (suite *skinsystemTestSuite) TestCape() {
 | 
				
			|||||||
			req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username", nil)
 | 
								req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username", nil)
 | 
				
			||||||
			w := httptest.NewRecorder()
 | 
								w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			suite.App.Handler().ServeHTTP(w, req)
 | 
								if testCase.PanicErr != "" {
 | 
				
			||||||
 | 
									suite.PanicsWithError(testCase.PanicErr, func() {
 | 
				
			||||||
			testCase.AfterTest(suite, w.Result())
 | 
										suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									testCase.AfterTest(suite, w.Result())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.RunSubTest("Pass username with png extension", func() {
 | 
						suite.RunSubTest("Pass username with png extension", func() {
 | 
				
			||||||
 | 
							suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
 | 
				
			||||||
		suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
 | 
							suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil)
 | 
							req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil)
 | 
				
			||||||
@@ -357,14 +422,18 @@ func (suite *skinsystemTestSuite) TestCapeGET() {
 | 
				
			|||||||
			req := httptest.NewRequest("GET", "http://chrly/cloaks?name=mock_username", nil)
 | 
								req := httptest.NewRequest("GET", "http://chrly/cloaks?name=mock_username", nil)
 | 
				
			||||||
			w := httptest.NewRecorder()
 | 
								w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			suite.App.Handler().ServeHTTP(w, req)
 | 
								if testCase.PanicErr != "" {
 | 
				
			||||||
 | 
									suite.PanicsWithError(testCase.PanicErr, func() {
 | 
				
			||||||
			testCase.AfterTest(suite, w.Result())
 | 
										suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									testCase.AfterTest(suite, w.Result())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.RunSubTest("Do not pass name param", func() {
 | 
						suite.RunSubTest("Do not pass name param", func() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
		req := httptest.NewRequest("GET", "http://chrly/cloaks", nil)
 | 
							req := httptest.NewRequest("GET", "http://chrly/cloaks", nil)
 | 
				
			||||||
		w := httptest.NewRecorder()
 | 
							w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -417,23 +486,9 @@ var texturesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
			}`, string(body))
 | 
								}`, string(body))
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	{
 | 
						// There is no case when the user has cape, but has no skin.
 | 
				
			||||||
		Name: "Username exists and has cape, no skin",
 | 
						// In v5 we will rework textures repositories to be more generic about source of textures,
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
						// but right now it's not possible to return profile entity with a cape only.
 | 
				
			||||||
			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
					 | 
				
			||||||
			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
					 | 
				
			||||||
			suite.Equal(200, response.StatusCode)
 | 
					 | 
				
			||||||
			suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
					 | 
				
			||||||
			body, _ := ioutil.ReadAll(response.Body)
 | 
					 | 
				
			||||||
			suite.JSONEq(`{
 | 
					 | 
				
			||||||
				"CAPE": {
 | 
					 | 
				
			||||||
					"url": "http://chrly/cloaks/mock_username"
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}`, string(body))
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		Name: "Username exists and has both skin and cape",
 | 
							Name: "Username exists and has both skin and cape",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
@@ -458,7 +513,6 @@ var texturesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
		Name: "Username not exists, but Mojang profile available",
 | 
							Name: "Username not exists, but Mojang profile available",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
					 | 
				
			||||||
			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
@@ -479,7 +533,6 @@ var texturesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
		Name: "Username not exists, but Mojang profile available, but there is an empty skin and cape textures",
 | 
							Name: "Username not exists, but Mojang profile available, but there is an empty skin and cape textures",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
					 | 
				
			||||||
			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(false, false), nil)
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(false, false), nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
@@ -490,7 +543,6 @@ var texturesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
		Name: "Username not exists, but Mojang profile available, but there is an empty properties",
 | 
							Name: "Username not exists, but Mojang profile available, but there is an empty properties",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
					 | 
				
			||||||
			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createEmptyMojangResponse(), nil)
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createEmptyMojangResponse(), nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
@@ -501,7 +553,6 @@ var texturesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
		Name: "Username not exists and Mojang profile unavailable",
 | 
							Name: "Username not exists and Mojang profile unavailable",
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
					 | 
				
			||||||
			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
@@ -510,6 +561,13 @@ var texturesTestsCases = []*skinsystemTestCase{
 | 
				
			|||||||
			suite.Equal("", string(body))
 | 
								suite.Equal("", string(body))
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Receive an error from the SkinsRepository",
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							PanicErr: "skins repository error",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *skinsystemTestSuite) TestTextures() {
 | 
					func (suite *skinsystemTestSuite) TestTextures() {
 | 
				
			||||||
@@ -520,9 +578,14 @@ func (suite *skinsystemTestSuite) TestTextures() {
 | 
				
			|||||||
			req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil)
 | 
								req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil)
 | 
				
			||||||
			w := httptest.NewRecorder()
 | 
								w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			suite.App.Handler().ServeHTTP(w, req)
 | 
								if testCase.PanicErr != "" {
 | 
				
			||||||
 | 
									suite.PanicsWithError(testCase.PanicErr, func() {
 | 
				
			||||||
			testCase.AfterTest(suite, w.Result())
 | 
										suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									testCase.AfterTest(suite, w.Result())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -535,6 +598,7 @@ type signedTexturesTestCase struct {
 | 
				
			|||||||
	Name       string
 | 
						Name       string
 | 
				
			||||||
	AllowProxy bool
 | 
						AllowProxy bool
 | 
				
			||||||
	BeforeTest func(suite *skinsystemTestSuite)
 | 
						BeforeTest func(suite *skinsystemTestSuite)
 | 
				
			||||||
 | 
						PanicErr   string
 | 
				
			||||||
	AfterTest  func(suite *skinsystemTestSuite, response *http.Response)
 | 
						AfterTest  func(suite *skinsystemTestSuite, response *http.Response)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -544,6 +608,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
 | 
				
			|||||||
		AllowProxy: false,
 | 
							AllowProxy: false,
 | 
				
			||||||
		BeforeTest: func(suite *skinsystemTestSuite) {
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil)
 | 
				
			||||||
 | 
								suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
			suite.Equal(200, response.StatusCode)
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
@@ -586,6 +651,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
 | 
				
			|||||||
			skinModel.MojangTextures = ""
 | 
								skinModel.MojangTextures = ""
 | 
				
			||||||
			skinModel.MojangSignature = ""
 | 
								skinModel.MojangSignature = ""
 | 
				
			||||||
			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skinModel, nil)
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skinModel, nil)
 | 
				
			||||||
 | 
								suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
			suite.Equal(204, response.StatusCode)
 | 
								suite.Equal(204, response.StatusCode)
 | 
				
			||||||
@@ -605,12 +671,13 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
 | 
				
			|||||||
			suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
								suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
				
			||||||
			body, _ := ioutil.ReadAll(response.Body)
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
			suite.JSONEq(`{
 | 
								suite.JSONEq(`{
 | 
				
			||||||
				"id": "00000000000000000000000000000000",
 | 
									"id": "292a1db7353d476ca99cab8f57mojang",
 | 
				
			||||||
				"name": "mock_username",
 | 
									"name": "mock_username",
 | 
				
			||||||
				"properties": [
 | 
									"properties": [
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						"name": "textures",
 | 
											"name": "textures",
 | 
				
			||||||
						"value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIsInByb2ZpbGVJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn19fQ=="
 | 
											"value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn19fQ==",
 | 
				
			||||||
 | 
											"signature": "mojang signature"
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						"name": "texturesParamName",
 | 
											"name": "texturesParamName",
 | 
				
			||||||
@@ -633,6 +700,13 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
 | 
				
			|||||||
			suite.Equal("", string(body))
 | 
								suite.Equal("", string(body))
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Receive an error from the SkinsRepository",
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							PanicErr: "skins repository error",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *skinsystemTestSuite) TestSignedTextures() {
 | 
					func (suite *skinsystemTestSuite) TestSignedTextures() {
 | 
				
			||||||
@@ -650,9 +724,406 @@ func (suite *skinsystemTestSuite) TestSignedTextures() {
 | 
				
			|||||||
			req := httptest.NewRequest("GET", target, nil)
 | 
								req := httptest.NewRequest("GET", target, nil)
 | 
				
			||||||
			w := httptest.NewRecorder()
 | 
								w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			suite.App.Handler().ServeHTTP(w, req)
 | 
								if testCase.PanicErr != "" {
 | 
				
			||||||
 | 
									suite.PanicsWithError(testCase.PanicErr, func() {
 | 
				
			||||||
 | 
										suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									testCase.AfterTest(suite, w.Result())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			testCase.AfterTest(suite, w.Result())
 | 
					/***************************
 | 
				
			||||||
 | 
					 * Get profile tests cases *
 | 
				
			||||||
 | 
					 ***************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type profileTestCase struct {
 | 
				
			||||||
 | 
						Name       string
 | 
				
			||||||
 | 
						Signed     bool
 | 
				
			||||||
 | 
						BeforeTest func(suite *skinsystemTestSuite)
 | 
				
			||||||
 | 
						PanicErr   string
 | 
				
			||||||
 | 
						AfterTest  func(suite *skinsystemTestSuite, response *http.Response)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var profileTestsCases = []*profileTestCase{
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Username exists and has both skin and cape, don't sign",
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
 | 
				
			||||||
 | 
								suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
 | 
								suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.JSONEq(`{
 | 
				
			||||||
 | 
									"id": "0f657aa8bfbe415db7005750090d3af3",
 | 
				
			||||||
 | 
									"name": "mock_username",
 | 
				
			||||||
 | 
									"properties": [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "textures",
 | 
				
			||||||
 | 
											"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifSwiQ0FQRSI6eyJ1cmwiOiJodHRwOi8vY2hybHkvY2xvYWtzL21vY2tfdXNlcm5hbWUifX19"
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "texturesParamName",
 | 
				
			||||||
 | 
											"value": "texturesParamValue"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									]
 | 
				
			||||||
 | 
								}`, string(body))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name:   "Username exists and has both skin and cape",
 | 
				
			||||||
 | 
							Signed: true,
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
 | 
				
			||||||
 | 
								suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("SignTextures", "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifSwiQ0FQRSI6eyJ1cmwiOiJodHRwOi8vY2hybHkvY2xvYWtzL21vY2tfdXNlcm5hbWUifX19").Return("textures signature", nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
 | 
								suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.JSONEq(`{
 | 
				
			||||||
 | 
									"id": "0f657aa8bfbe415db7005750090d3af3",
 | 
				
			||||||
 | 
									"name": "mock_username",
 | 
				
			||||||
 | 
									"properties": [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "textures",
 | 
				
			||||||
 | 
											"signature": "textures signature",
 | 
				
			||||||
 | 
											"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifSwiQ0FQRSI6eyJ1cmwiOiJodHRwOi8vY2hybHkvY2xvYWtzL21vY2tfdXNlcm5hbWUifX19"
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "texturesParamName",
 | 
				
			||||||
 | 
											"value": "texturesParamValue"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									]
 | 
				
			||||||
 | 
								}`, string(body))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name:   "Username exists and has skin, no cape",
 | 
				
			||||||
 | 
							Signed: true,
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
 | 
				
			||||||
 | 
								suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("SignTextures", mock.Anything).Return("textures signature", nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
 | 
								suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.JSONEq(`{
 | 
				
			||||||
 | 
									"id": "0f657aa8bfbe415db7005750090d3af3",
 | 
				
			||||||
 | 
									"name": "mock_username",
 | 
				
			||||||
 | 
									"properties": [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "textures",
 | 
				
			||||||
 | 
											"signature": "textures signature",
 | 
				
			||||||
 | 
											"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifX19"
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "texturesParamName",
 | 
				
			||||||
 | 
											"value": "texturesParamValue"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									]
 | 
				
			||||||
 | 
								}`, string(body))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name:   "Username exists and has slim skin, no cape",
 | 
				
			||||||
 | 
							Signed: true,
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil)
 | 
				
			||||||
 | 
								suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("SignTextures", mock.Anything).Return("textures signature", nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
 | 
								suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.JSONEq(`{
 | 
				
			||||||
 | 
									"id": "0f657aa8bfbe415db7005750090d3af3",
 | 
				
			||||||
 | 
									"name": "mock_username",
 | 
				
			||||||
 | 
									"properties": [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "textures",
 | 
				
			||||||
 | 
											"signature": "textures signature",
 | 
				
			||||||
 | 
											"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmciLCJtZXRhZGF0YSI6eyJtb2RlbCI6InNsaW0ifX19fQ=="
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "texturesParamName",
 | 
				
			||||||
 | 
											"value": "texturesParamValue"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									]
 | 
				
			||||||
 | 
								}`, string(body))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name:   "Username exists, but has no skin and Mojang profile with textures available",
 | 
				
			||||||
 | 
							Signed: true,
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								skin := createSkinModel("mock_username", false)
 | 
				
			||||||
 | 
								skin.SkinId = 0
 | 
				
			||||||
 | 
								skin.Url = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skin, nil)
 | 
				
			||||||
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
 | 
								suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.JSONEq(`{
 | 
				
			||||||
 | 
									"id": "0f657aa8bfbe415db7005750090d3af3",
 | 
				
			||||||
 | 
									"name": "mock_username",
 | 
				
			||||||
 | 
									"properties": [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "textures",
 | 
				
			||||||
 | 
											"signature": "chrly signature",
 | 
				
			||||||
 | 
											"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn0sIkNBUEUiOnsidXJsIjoiaHR0cDovL21vamFuZy9jYXBlLnBuZyJ9fX0="
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "texturesParamName",
 | 
				
			||||||
 | 
											"value": "texturesParamValue"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									]
 | 
				
			||||||
 | 
								}`, string(body))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name:   "Username exists, but has no skin and Mojang textures proxy returned an error",
 | 
				
			||||||
 | 
							Signed: true,
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								skin := createSkinModel("mock_username", false)
 | 
				
			||||||
 | 
								skin.SkinId = 0
 | 
				
			||||||
 | 
								skin.Url = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skin, nil)
 | 
				
			||||||
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, errors.New("shit happened"))
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
 | 
								suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.JSONEq(`{
 | 
				
			||||||
 | 
									"id": "0f657aa8bfbe415db7005750090d3af3",
 | 
				
			||||||
 | 
									"name": "mock_username",
 | 
				
			||||||
 | 
									"properties": [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "textures",
 | 
				
			||||||
 | 
											"signature": "chrly signature",
 | 
				
			||||||
 | 
											"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnt9fQ=="
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "texturesParamName",
 | 
				
			||||||
 | 
											"value": "texturesParamValue"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									]
 | 
				
			||||||
 | 
								}`, string(body))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name:   "Username not exists, but Mojang profile with textures available",
 | 
				
			||||||
 | 
							Signed: true,
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
 | 
								suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.JSONEq(`{
 | 
				
			||||||
 | 
									"id": "292a1db7353d476ca99cab8f57mojang",
 | 
				
			||||||
 | 
									"name": "mock_username",
 | 
				
			||||||
 | 
									"properties": [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "textures",
 | 
				
			||||||
 | 
											"signature": "chrly signature",
 | 
				
			||||||
 | 
											"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn0sIkNBUEUiOnsidXJsIjoiaHR0cDovL21vamFuZy9jYXBlLnBuZyJ9fX0="
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "texturesParamName",
 | 
				
			||||||
 | 
											"value": "texturesParamValue"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									]
 | 
				
			||||||
 | 
								}`, string(body))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name:   "Username not exists, but Mojang profile available, but there is an empty skin and cape textures",
 | 
				
			||||||
 | 
							Signed: true,
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(false, false), nil)
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
 | 
								suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.JSONEq(`{
 | 
				
			||||||
 | 
									"id": "292a1db7353d476ca99cab8f57mojang",
 | 
				
			||||||
 | 
									"name": "mock_username",
 | 
				
			||||||
 | 
									"properties": [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "textures",
 | 
				
			||||||
 | 
											"signature": "chrly signature",
 | 
				
			||||||
 | 
											"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnt9fQ=="
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "texturesParamName",
 | 
				
			||||||
 | 
											"value": "texturesParamValue"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									]
 | 
				
			||||||
 | 
								}`, string(body))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name:   "Username not exists, but Mojang profile available, but there is an empty properties",
 | 
				
			||||||
 | 
							Signed: true,
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createEmptyMojangResponse(), nil)
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
 | 
								suite.Equal("application/json", response.Header.Get("Content-Type"))
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.JSONEq(`{
 | 
				
			||||||
 | 
									"id": "292a1db7353d476ca99cab8f57mojang",
 | 
				
			||||||
 | 
									"name": "mock_username",
 | 
				
			||||||
 | 
									"properties": [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "textures",
 | 
				
			||||||
 | 
											"signature": "chrly signature",
 | 
				
			||||||
 | 
											"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnt9fQ=="
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											"name": "texturesParamName",
 | 
				
			||||||
 | 
											"value": "texturesParamValue"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									]
 | 
				
			||||||
 | 
								}`, string(body))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Username not exists and Mojang profile unavailable",
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(204, response.StatusCode)
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.Equal("", string(body))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Username not exists and Mojang textures proxy returned an error",
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
 | 
								suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, errors.New("mojang textures provider error"))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							PanicErr: "mojang textures provider error",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Receive an error from the SkinsRepository",
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							PanicErr: "skins repository error",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name:   "Receive an error from the TexturesSigner",
 | 
				
			||||||
 | 
							Signed: true,
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
 | 
				
			||||||
 | 
								suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("SignTextures", mock.Anything).Return("", errors.New("textures signer error"))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							PanicErr: "textures signer error",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *skinsystemTestSuite) TestProfile() {
 | 
				
			||||||
 | 
						for _, testCase := range profileTestsCases {
 | 
				
			||||||
 | 
							suite.RunSubTest(testCase.Name, func() {
 | 
				
			||||||
 | 
								testCase.BeforeTest(suite)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								url := "http://chrly/profile/mock_username"
 | 
				
			||||||
 | 
								if testCase.Signed {
 | 
				
			||||||
 | 
									url += "?unsigned=false"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := httptest.NewRequest("GET", url, nil)
 | 
				
			||||||
 | 
								w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if testCase.PanicErr != "" {
 | 
				
			||||||
 | 
									suite.PanicsWithError(testCase.PanicErr, func() {
 | 
				
			||||||
 | 
										suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									testCase.AfterTest(suite, w.Result())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/***************************
 | 
				
			||||||
 | 
					 * Get profile tests cases *
 | 
				
			||||||
 | 
					 ***************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var signingKeyTestsCases = []*skinsystemTestCase{
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Get public key",
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								pubPem, _ := pem.Decode([]byte("-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----"))
 | 
				
			||||||
 | 
								publicKey, _ := x509.ParsePKIXPublicKey(pubPem.Bytes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("GetPublicKey").Return(publicKey, nil)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
 | 
				
			||||||
 | 
								suite.Equal(200, response.StatusCode)
 | 
				
			||||||
 | 
								suite.Equal("application/octet-stream", response.Header.Get("Content-Type"))
 | 
				
			||||||
 | 
								body, _ := ioutil.ReadAll(response.Body)
 | 
				
			||||||
 | 
								suite.Equal([]byte{48, 92, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 75, 0, 48, 72, 2, 65, 0, 214, 212, 165, 80, 153, 144, 194, 169, 126, 246, 25, 211, 197, 183, 150, 233, 157, 1, 166, 49, 44, 25, 230, 80, 57, 115, 28, 20, 7, 220, 58, 88, 121, 254, 86, 8, 237, 246, 76, 53, 58, 125, 226, 9, 231, 192, 52, 148, 12, 176, 130, 214, 120, 195, 8, 182, 116, 97, 206, 207, 253, 97, 2, 247, 2, 3, 1, 0, 1}, body)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							Name: "Error while obtaining public key",
 | 
				
			||||||
 | 
							BeforeTest: func(suite *skinsystemTestSuite) {
 | 
				
			||||||
 | 
								suite.TexturesSigner.On("GetPublicKey").Return(nil, errors.New("textures signer error"))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							PanicErr: "textures signer error",
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *skinsystemTestSuite) TestSignatureVerificationKey() {
 | 
				
			||||||
 | 
						for _, testCase := range signingKeyTestsCases {
 | 
				
			||||||
 | 
							suite.RunSubTest(testCase.Name, func() {
 | 
				
			||||||
 | 
								testCase.BeforeTest(suite)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req := httptest.NewRequest("GET", "http://chrly/signature-verification-key", nil)
 | 
				
			||||||
 | 
								w := httptest.NewRecorder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if testCase.PanicErr != "" {
 | 
				
			||||||
 | 
									suite.PanicsWithError(testCase.PanicErr, func() {
 | 
				
			||||||
 | 
										suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									suite.App.Handler().ServeHTTP(w, req)
 | 
				
			||||||
 | 
									testCase.AfterTest(suite, w.Result())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -699,7 +1170,7 @@ func createCapeModel() *model.Cape {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func createEmptyMojangResponse() *mojang.SignedTexturesResponse {
 | 
					func createEmptyMojangResponse() *mojang.SignedTexturesResponse {
 | 
				
			||||||
	return &mojang.SignedTexturesResponse{
 | 
						return &mojang.SignedTexturesResponse{
 | 
				
			||||||
		Id:    "00000000000000000000000000000000",
 | 
							Id:    "292a1db7353d476ca99cab8f57mojang",
 | 
				
			||||||
		Name:  "mock_username",
 | 
							Name:  "mock_username",
 | 
				
			||||||
		Props: []*mojang.Property{},
 | 
							Props: []*mojang.Property{},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -708,8 +1179,8 @@ func createEmptyMojangResponse() *mojang.SignedTexturesResponse {
 | 
				
			|||||||
func createMojangResponseWithTextures(includeSkin bool, includeCape bool) *mojang.SignedTexturesResponse {
 | 
					func createMojangResponseWithTextures(includeSkin bool, includeCape bool) *mojang.SignedTexturesResponse {
 | 
				
			||||||
	timeZone, _ := time.LoadLocation("Europe/Minsk")
 | 
						timeZone, _ := time.LoadLocation("Europe/Minsk")
 | 
				
			||||||
	textures := &mojang.TexturesProp{
 | 
						textures := &mojang.TexturesProp{
 | 
				
			||||||
		Timestamp:   time.Date(2019, 4, 27, 23, 56, 12, 0, timeZone).Unix(),
 | 
							Timestamp:   time.Date(2019, 4, 27, 23, 56, 12, 0, timeZone).UnixNano() / int64(time.Millisecond),
 | 
				
			||||||
		ProfileID:   "00000000000000000000000000000000",
 | 
							ProfileID:   "292a1db7353d476ca99cab8f57mojang",
 | 
				
			||||||
		ProfileName: "mock_username",
 | 
							ProfileName: "mock_username",
 | 
				
			||||||
		Textures:    &mojang.TexturesResponse{},
 | 
							Textures:    &mojang.TexturesResponse{},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -728,8 +1199,9 @@ func createMojangResponseWithTextures(includeSkin bool, includeCape bool) *mojan
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	response := createEmptyMojangResponse()
 | 
						response := createEmptyMojangResponse()
 | 
				
			||||||
	response.Props = append(response.Props, &mojang.Property{
 | 
						response.Props = append(response.Props, &mojang.Property{
 | 
				
			||||||
		Name:  "textures",
 | 
							Name:      "textures",
 | 
				
			||||||
		Value: mojang.EncodeTextures(textures),
 | 
							Value:     mojang.EncodeTextures(textures),
 | 
				
			||||||
 | 
							Signature: "mojang signature",
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return response
 | 
						return response
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/elyby/chrly/api/mojang"
 | 
						"github.com/elyby/chrly/api/mojang"
 | 
				
			||||||
 | 
						"github.com/elyby/chrly/utils"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type inMemoryItem struct {
 | 
					type inMemoryItem struct {
 | 
				
			||||||
@@ -53,7 +54,7 @@ func (s *InMemoryTexturesStorage) StoreTextures(uuid string, textures *mojang.Si
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	s.data[uuid] = &inMemoryItem{
 | 
						s.data[uuid] = &inMemoryItem{
 | 
				
			||||||
		textures:  textures,
 | 
							textures:  textures,
 | 
				
			||||||
		timestamp: unixNanoToUnixMicro(time.Now().UnixNano()),
 | 
							timestamp: utils.UnixMillisecond(time.Now()),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -89,9 +90,5 @@ func (s *InMemoryTexturesStorage) gc() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *InMemoryTexturesStorage) getMinimalNotExpiredTimestamp() int64 {
 | 
					func (s *InMemoryTexturesStorage) getMinimalNotExpiredTimestamp() int64 {
 | 
				
			||||||
	return unixNanoToUnixMicro(time.Now().Add(s.Duration * time.Duration(-1)).UnixNano())
 | 
						return utils.UnixMillisecond(time.Now().Add(s.Duration * time.Duration(-1)))
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func unixNanoToUnixMicro(unixNano int64) int64 {
 | 
					 | 
				
			||||||
	return unixNano / 10e5
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										42
									
								
								signer/signer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								signer/signer.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					package signer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto"
 | 
				
			||||||
 | 
						"crypto/rand"
 | 
				
			||||||
 | 
						"crypto/rsa"
 | 
				
			||||||
 | 
						"crypto/sha1"
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var randomReader = rand.Reader
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Signer struct {
 | 
				
			||||||
 | 
						Key *rsa.PrivateKey
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Signer) SignTextures(textures string) (string, error) {
 | 
				
			||||||
 | 
						if s.Key == nil {
 | 
				
			||||||
 | 
							return "", errors.New("Key is empty")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						message := []byte(textures)
 | 
				
			||||||
 | 
						messageHash := sha1.New()
 | 
				
			||||||
 | 
						_, _ = messageHash.Write(message)
 | 
				
			||||||
 | 
						messageHashSum := messageHash.Sum(nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						signature, err := rsa.SignPKCS1v15(randomReader, s.Key, crypto.SHA1, messageHashSum)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return base64.StdEncoding.EncodeToString(signature), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Signer) GetPublicKey() (*rsa.PublicKey, error) {
 | 
				
			||||||
 | 
						if s.Key == nil {
 | 
				
			||||||
 | 
							return nil, errors.New("Key is empty")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &s.Key.PublicKey, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										64
									
								
								signer/signer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								signer/signer_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
				
			|||||||
 | 
					package signer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/rsa"
 | 
				
			||||||
 | 
						"crypto/x509"
 | 
				
			||||||
 | 
						"encoding/pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert "github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ConstantReader struct {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *ConstantReader) Read(p []byte) (int, error) {
 | 
				
			||||||
 | 
						return 1, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestSigner_SignTextures(t *testing.T) {
 | 
				
			||||||
 | 
						randomReader = &ConstantReader{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("sign textures", func(t *testing.T) {
 | 
				
			||||||
 | 
							rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n"))
 | 
				
			||||||
 | 
							key, _ := x509.ParsePKCS1PrivateKey(rawKey.Bytes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							signer := &Signer{key}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							signature, err := signer.SignTextures("eyJ0aW1lc3RhbXAiOjE2MTQzMDcxMzQsInByb2ZpbGVJZCI6ImZmYzhmZGM5NTgyNDUwOWU4YTU3Yzk5Yjk0MGZiOTk2IiwicHJvZmlsZU5hbWUiOiJFcmlja1NrcmF1Y2giLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly9lbHkuYnkvc3RvcmFnZS9za2lucy82OWM2NzQwZDI5OTNlNWQ2ZjZhN2ZjOTI0MjBlZmMyOS5wbmcifX0sImVseSI6dHJ1ZX0")
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.Equal(t, "IyHCxTP5ITquEXTHcwCtLd08jWWy16JwlQeWg8naxhoAVQecHGRdzHRscuxtdq/446kmeox7h4EfRN2A2ZLL+A==", signature)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("empty key", func(t *testing.T) {
 | 
				
			||||||
 | 
							signer := &Signer{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							signature, err := signer.SignTextures("hello world")
 | 
				
			||||||
 | 
							assert.Error(t, err, "Key is empty")
 | 
				
			||||||
 | 
							assert.Empty(t, signature)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestSigner_GetPublicKey(t *testing.T) {
 | 
				
			||||||
 | 
						randomReader = &ConstantReader{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("get public key", func(t *testing.T) {
 | 
				
			||||||
 | 
							rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n"))
 | 
				
			||||||
 | 
							key, _ := x509.ParsePKCS1PrivateKey(rawKey.Bytes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							signer := &Signer{key}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							publicKey, err := signer.GetPublicKey()
 | 
				
			||||||
 | 
							assert.NoError(t, err)
 | 
				
			||||||
 | 
							assert.IsType(t, &rsa.PublicKey{}, publicKey)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("empty key", func(t *testing.T) {
 | 
				
			||||||
 | 
							signer := &Signer{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							publicKey, err := signer.GetPublicKey()
 | 
				
			||||||
 | 
							assert.Error(t, err, "Key is empty")
 | 
				
			||||||
 | 
							assert.Nil(t, publicKey)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								utils/time.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								utils/time.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					package utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UnixMillisecond(t time.Time) int64 {
 | 
				
			||||||
 | 
						return t.UnixNano() / int64(time.Millisecond)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								utils/time_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								utils/time_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					package utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert "github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestUnixMillisecond(t *testing.T) {
 | 
				
			||||||
 | 
						loc, _ := time.LoadLocation("CET")
 | 
				
			||||||
 | 
						d := time.Date(2021, 02, 26, 00, 43, 57, 987654321, loc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, int64(1614296637987), UnixMillisecond(d))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user