diff --git a/.gitignore b/.gitignore index a09c56d..08c26dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +# IDEA /.idea diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2c08e63 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.7 + +RUN mkdir -p /go/src/app +WORKDIR /go/src/app + +COPY ./src /go/src/app + +RUN go-wrapper download +RUN go-wrapper install + +EXPOSE 80 + +VOLUME ["/go/src/app"] + +CMD ["go-wrapper", "run"] diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0e31b24 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: '2' +services: + app: + build: . + links: + - redis + + web: + build: ./docker/nginx + ports: + - "80:80" + links: + - app + volumes_from: + - app + + redis: + image: redis:3.0 + volumes: + - ./data/:/data diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 0000000..5440e75 --- /dev/null +++ b/docker/nginx/Dockerfile @@ -0,0 +1,8 @@ +FROM nginx:1.11 + +COPY nginx.conf /etc/nginx/nginx.conf +COPY skinsystem.ely.by.conf /etc/nginx/conf.d/default.conf + +RUN mkdir -p /data/cache \ + /data/logs \ + && chown nginx:nginx -R /data diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 0000000..dd75a2d --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,31 @@ +user nginx; +worker_processes auto; + +events { + worker_connections 4048; + multi_accept on; + use epoll; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 15; + types_hash_max_size 2048; + server_tokens off; + + access_log /data/logs/access.log combined buffer=32k; + error_log /data/logs/error.log crit; + + gzip off; + + client_max_body_size 1M; + + proxy_cache_path /data/cache levels=1:2 keys_zone=cache:30m max_size=1G; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/docker/nginx/skinsystem.ely.by.conf b/docker/nginx/skinsystem.ely.by.conf new file mode 100644 index 0000000..829ceef --- /dev/null +++ b/docker/nginx/skinsystem.ely.by.conf @@ -0,0 +1,63 @@ +server { + listen 80; + + charset utf-8; + set $root_path '/go/src/app'; + root $root_path; + + proxy_cache_use_stale error timeout invalid_header http_502; + proxy_cache_min_uses 1; + + location / { + proxy_pass http://app; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + + proxy_cache cache; + proxy_cache_valid 30m; + } + + location /minecraft.php { + if ($arg_name = "") { + return 400; + } + + if ($arg_type = "cloack") { + rewrite .* http://skins.minecraft.net/MinecraftCloaks/$arg_name.png? permanent; + break; + } + + if ($arg_type = "skin") { + rewrite .* /skins/$arg_name last; + break; + } + + return 404; + } + + location /cloaks/ { + try_files $uri $uri.png @cloaks; + } + + location @cloaks { + rewrite ^/cloaks/(.+?)(\.[^.]*$|$)$ http://skins.minecraft.net/MinecraftCloaks/$1.png? permanent; + } + + location ~* ^/skins/$ { + if ($arg_name = "") { + return 400; + } + + rewrite .* /skins/$arg_name last; + } + + location ~* ^/cloaks/$ { + if ($arg_name = "") { + return 400; + } + + rewrite .* /cloaks/$arg_name last; + } +} diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index 2073d57..0000000 --- a/nginx.conf +++ /dev/null @@ -1,40 +0,0 @@ -location /minecraft.php { - if ($arg_name = "") { - return 400; - } - - if ($arg_type = "cloack") { - rewrite .* http://skins.minecraft.net/MinecraftCloaks/$arg_name.png? permanent; - break; - } - - if ($arg_type = "skin") { - rewrite .* /skins/$arg_name last; - } - - return 404; -} - -location /cloaks/ { - try_files $uri $uri.png @cloaks; -} - -location @cloaks { - rewrite ^/cloaks/(.+?)(\.[^.]*$|$)$ http://skins.minecraft.net/MinecraftCloaks/$1.png? permanent; -} - -location ~* ^/skins/$ { - if ($arg_name = "") { - return 400; - } - - rewrite .* /skins/$arg_name permanent; -} - -location ~* ^/cloaks/$ { - if ($arg_name = "") { - return 400; - } - - rewrite .* /cloaks/$arg_name permanent; -} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..1e1406b --- /dev/null +++ b/src/main.go @@ -0,0 +1,133 @@ +package main + +import ( + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/mediocregopher/radix.v2/redis" + "encoding/json" + "strings" + "time" + "strconv" + "crypto/md5" + "encoding/hex" +) + +var client, redisErr = redis.Dial("tcp", "redis:6379") + +func main() { + if redisErr != nil { + log.Fatal("Redis unavailable") + } + + router := mux.NewRouter().StrictSlash(true) + router.HandleFunc("/skins/{username}", GetSkin) + router.HandleFunc("/textures/{username}", GetTextures) + + log.Fatal(http.ListenAndServe(":80", router)) +} + +func GetSkin(w http.ResponseWriter, r *http.Request) { + username := ParseUsername(mux.Vars(r)["username"]) + log.Println("request skin for username " + username); + rec, err := FindRecord(username) + if (err != nil) { + http.Redirect(w, r, "http://skins.minecraft.net/MinecraftSkins/" + username + ".png", 301) + log.Println("Cannot get skin for username " + username) + return + } + + http.Redirect(w, r, rec.Url, 301); +} + +func GetTextures(w http.ResponseWriter, r *http.Request) { + username := ParseUsername(mux.Vars(r)["username"]) + log.Println("request textures for username " + username) + + rec, err := FindRecord(username) + if (err != nil || rec.SkinId == 0) { + rec.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png" + rec.Hash = string(BuildNonElyTexturesHash(username)) + } + + textures := TexturesResponse{ + Skin: &Skin{ + Url: rec.Url, + Hash: rec.Hash, + }, + } + + if (rec.IsSlim) { + textures.Skin.Metadata = &SkinMetadata{ + Model: "slim", + } + } + + response,_ := json.Marshal(textures) + w.Header().Set("Content-Type", "application/json") + w.Write(response) +} + +// STRUCTURES + +type SkinItem struct { + UserId int `json:"userId"` + Nickname string `json:"nickname"` + SkinId int `json:"skinId"` + Url string `json:"url"` + Is1_8 bool `json:"is1_8"` + IsSlim bool `json:"isSlim"` + Hash string `json:"hash"` +} + +type TexturesResponse struct { + Skin *Skin `json:"SKIN"` +} + +type Skin struct { + Url string `json:"url"` + Hash string `json:"hash"` + Metadata *SkinMetadata `json:"metadata,omitempty"` +} + +type SkinMetadata struct { + Model string `json:"model"` +} + +// TOOLS + +func ParseUsername(username string) string { + const suffix = ".png" + if strings.HasSuffix(username, suffix) { + username = strings.TrimSuffix(username, suffix) + } + + return username +} + +func BuildNonElyTexturesHash(username string) string { + n := time.Now() + hour := time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), 0, 0, 0, time.UTC).Unix() + hasher := md5.New() + hasher.Write([]byte("non-ely-" + strconv.FormatInt(hour, 10) + "-" + username)) + + return hex.EncodeToString(hasher.Sum(nil)) +} + +func FindRecord(username string) (SkinItem, error) { + var record SkinItem; + result, err := client.Cmd("GET", BuildKey(username)).Str(); + if (err == nil) { + decodeErr := json.Unmarshal([]byte(result), &record) + if (decodeErr != nil) { + log.Println("Cannot decode record data") + } + } + + return record, err +} + +func BuildKey(username string) string { + return "username:" + strings.ToLower(username) +}