Skip to content
Snippets Groups Projects
processor.go 17.2 KiB
Newer Older
Silvan's avatar
Silvan committed
package processor

import (
	"bufio"
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
Silvan's avatar
Silvan committed
	"github.com/cnsilvan/UnblockNeteaseMusic/common"
	"github.com/cnsilvan/UnblockNeteaseMusic/config"
Silvan's avatar
Silvan committed
	"github.com/cnsilvan/UnblockNeteaseMusic/network"
	"github.com/cnsilvan/UnblockNeteaseMusic/processor/crypto"
	"github.com/cnsilvan/UnblockNeteaseMusic/provider"
	"github.com/cnsilvan/UnblockNeteaseMusic/utils"
Silvan's avatar
Silvan committed
	"io"
	"io/ioutil"
	"net/http"
Silvan's avatar
Silvan committed
	"net/url"
Silvan's avatar
Silvan committed
	"regexp"
	"strings"
)

var (
	eApiKey     = "e82ckenh8dichen8"
	linuxApiKey = "rFgB&h#%2?^eDg:Q"
	///api/song/enhance/player/url
	///eapi/mlivestream/entrance/playlist/get
	Path = map[string]int{
		"/api/v3/playlist/detail":            1,
		"/api/v3/song/detail":                1,
		"/api/v6/playlist/detail":            1,
		"/api/album/play":                    1,
		"/api/artist/privilege":              1,
		"/api/album/privilege":               1,
		"/api/v1/artist":                     1,
		"/api/v1/artist/songs":               1,
		"/api/artist/top/song":               1,
		"/api/v1/album":                      1,
		"/api/album/v3/detail":               1,
		"/api/playlist/privilege":            1,
		"/api/song/enhance/player/url":       1,
		"/api/song/enhance/player/url/v1":    1,
		"/api/song/enhance/download/url":     1,
		"/batch":                             1,
		"/api/batch":                         1,
		"/api/v1/search/get":                 1,
Silvan's avatar
Silvan committed
		"/api/v1/search/song/get":            1,
		"/api/search/complex/get":            1,
Silvan's avatar
Silvan committed
		"/api/cloudsearch/pc":                1,
		"/api/v1/playlist/manipulate/tracks": 1,
		"/api/song/like":                     1,
		"/api/v1/play/record":                1,
		"/api/playlist/v4/detail":            1,
		"/api/v1/radio/get":                  1,
		"/api/v1/discovery/recommend/songs":  1,
		"/api/cloudsearch/get/web":           1,
		"/api/song/enhance/privilege":        1,
	}
Silvan's avatar
Silvan committed
	// "header key" : { "contain value":"protocol"}
Silvan's avatar
Silvan committed
	ReRulesUnderHeader = map[string]map[string]string{"os": {"pc": "http"}}
Silvan's avatar
Silvan committed
)

type Netease struct {
	Path      string
	Params    map[string]interface{}
	JsonBody  map[string]interface{}
	Web       bool
	Encrypted bool
Silvan's avatar
Silvan committed
	Forward   bool
Silvan's avatar
Silvan committed
}

func RequestBefore(request *http.Request) *Netease {
	netease := &Netease{Path: request.URL.Path}
	if request.Method == http.MethodPost && (strings.Contains(netease.Path, "/eapi/") || strings.Contains(netease.Path, "/api/linux/forward")) {
		request.Header.Del("x-napm-retry")
		request.Header.Set("X-Real-IP", "118.66.66.66")
		requestBody, _ := ioutil.ReadAll(request.Body)
		requestHold := ioutil.NopCloser(bytes.NewBuffer(requestBody))
		request.Body = requestHold
		pad := make([]byte, 0)
Silvan's avatar
Silvan committed
		reg := regexp.MustCompile(`%0+$`)
		if matched := reg.Find(requestBody); len(matched) > 0 {
Silvan's avatar
Silvan committed
			pad = requestBody
		}
		if netease.Path == "/api/linux/forward" {
Silvan's avatar
Silvan committed
			netease.Forward = true
Silvan's avatar
Silvan committed
			requestBodyH := make([]byte, len(requestBody))
			length, _ := hex.Decode(requestBodyH, requestBody[8:len(requestBody)-len(pad)])
			decryptECBBytes, _ := crypto.AesDecryptECB(requestBodyH[:length], []byte(linuxApiKey))
Silvan's avatar
Silvan committed
			var result common.MapType
Silvan's avatar
Silvan committed
			result = utils.ParseJson(decryptECBBytes)
Silvan's avatar
Silvan committed
			//fmt.Println(utils.ToJson(result))
			urlM, ok := result["url"].(string)
			if ok {
				netease.Path = urlM
Silvan's avatar
Silvan committed
			}
Silvan's avatar
Silvan committed
			params, ok := result["params"].(common.MapType)
			if ok {
				netease.Params = params
			}

			//fmt.Println("forward")
Silvan's avatar
Silvan committed
			//fmt.Printf("path:%s \nparams:%s\n", netease.Path, netease.Params)
		} else {
			requestBodyH := make([]byte, len(requestBody))
			length, _ := hex.Decode(requestBodyH, requestBody[7:len(requestBody)-len(pad)])
			decryptECBBytes, _ := crypto.AesDecryptECB(requestBodyH[:length], []byte(eApiKey))
			decryptString := string(decryptECBBytes)
			data := strings.Split(decryptString, "-36cd479b6b5-")
			netease.Path = data[0]
			netease.Params = utils.ParseJson(bytes.NewBufferString(data[1]).Bytes())
			//fmt.Printf("path:%s \nparams:%s\n", netease.Path, netease.Params)
		}
Silvan's avatar
Silvan committed
		netease.Path = strings.ReplaceAll(netease.Path, "https://music.163.com", "")
		netease.Path = strings.ReplaceAll(netease.Path, "http://music.163.com", "")
		netease.Path = utils.ReplaceAll(netease.Path, `\/\d*$`, "")
Silvan's avatar
Silvan committed
	} else if strings.Index(netease.Path, "/weapi/") == 0 || strings.Index(netease.Path, "/api/") == 0 {
		request.Header.Set("X-Real-IP", "118.66.66.66")
		netease.Web = true
		netease.Path = utils.ReplaceAll(netease.Path, `^\/weapi\/`, "/api/")
		netease.Path = utils.ReplaceAll(netease.Path, `\?.+$`, "")
		netease.Path = utils.ReplaceAll(netease.Path, `\/\d*$`, "")
	} else if strings.Contains(netease.Path, "package") {

	}
Silvan's avatar
Silvan committed
	//fmt.Println(utils.ToJson(netease))
Silvan's avatar
Silvan committed
	return netease
}
func Request(request *http.Request, remoteUrl string) (*http.Response, error) {
	clientRequest := network.ClientRequest{
		Method:    request.Method,
		RemoteUrl: remoteUrl,
		Host:      request.Host,
		Header:    request.Header,
		Body:      request.Body,
		Proxy:     true,
	}
	return network.Request(&clientRequest)
}
func RequestAfter(request *http.Request, response *http.Response, netease *Netease) {
	run := false
	if _, ok := Path[netease.Path]; ok {
		run = true
	}
	if run && response.StatusCode == 200 {
		encode := response.Header.Get("Content-Encoding")
		enableGzip := false
		if len(encode) > 0 && (strings.Contains(encode, "gzip") || strings.Contains(encode, "deflate")) {
			enableGzip = true
		}
		body, _ := ioutil.ReadAll(response.Body)
Silvan's avatar
Silvan committed
		response.Body.Close()
Silvan's avatar
Silvan committed
		tmpBody := make([]byte, len(body))
		copy(tmpBody, body)
		if len(body) > 0 {
			decryptECBBytes := body
			if enableGzip {
Silvan's avatar
Silvan committed
				decryptECBBytes, _ = utils.UnGzip(decryptECBBytes)
Silvan's avatar
Silvan committed
			}
			//fmt.Println(string(decryptECBBytes), netease)
Silvan's avatar
Silvan committed
			aeskey := eApiKey
			if netease.Forward {
				aeskey = linuxApiKey
			}
			decryptECBBytes, encrypted := crypto.AesDecryptECB(decryptECBBytes, []byte(aeskey))
Silvan's avatar
Silvan committed
			netease.Encrypted = encrypted
			result := utils.ParseJson(decryptECBBytes)
			netease.JsonBody = result
Silvan's avatar
Silvan committed

Silvan's avatar
Silvan committed
			//if strings.Contains(netease.Path,"batch"){
			// fmt.Println(utils.ToJson(netease))
			//}
Silvan's avatar
Silvan committed
			modified := false
Silvan's avatar
Silvan committed
			codeN, ok := netease.JsonBody["code"].(json.Number)
			code := "200"
			if ok {
				code = codeN.String()
			}
Silvan's avatar
Silvan committed
			if !netease.Web && (code == "401" || code == "512") && strings.Contains(netease.Path, "manipulate") {
				modified = tryCollect(netease, request)
			} else if !netease.Web && (code == "401" || code == "512") && strings.EqualFold(netease.Path, "/api/song/like") {
				modified = tryLike(netease, request)
			} else if strings.Contains(netease.Path, "url") {
				modified = tryMatch(netease)
			}
			if processMapJson(netease.JsonBody) || modified {
				response.Header.Del("transfer-encoding")
				response.Header.Del("content-encoding")
				response.Header.Del("content-length")
				//netease.JsonBody = netease.JsonBody
Silvan's avatar
Silvan committed
				modifiedJson, _ := utils.JSON.Marshal(netease.JsonBody)
				//fmt.Println(netease)
				//fmt.Println(string(modifiedJson))
				if netease.Encrypted {
Silvan's avatar
Silvan committed
					modifiedJson = crypto.AesEncryptECB(modifiedJson, []byte(aeskey))
Silvan's avatar
Silvan committed
				}
				response.Body = ioutil.NopCloser(bytes.NewBuffer(modifiedJson))
			} else {
Silvan's avatar
Silvan committed
				responseHold := ioutil.NopCloser(bytes.NewBuffer(tmpBody))
				response.Body = responseHold
			}
Silvan's avatar
Silvan committed
			//fmt.Println(utils.ToJson(netease.JsonBody))
Silvan's avatar
Silvan committed
		} else {
			responseHold := ioutil.NopCloser(bytes.NewBuffer(tmpBody))
			response.Body = responseHold
		}
	} else {
		//fmt.Println("Not Process")
	}
}
func tryCollect(netease *Netease, request *http.Request) bool {
	modified := false
	//fmt.Println(utils.ToJson(netease))
	if utils.Exist("trackIds", netease.Params) {
		trackId := ""
		switch netease.Params["trackIds"].(type) {
		case string:
Silvan's avatar
Silvan committed
			var result common.SliceType
Silvan's avatar
Silvan committed
			d := utils.JSON.NewDecoder(bytes.NewReader(bytes.NewBufferString(netease.Params["trackIds"].(string)).Bytes()))
			d.UseNumber()
			d.Decode(&result)
			trackId = result[0].(string)
Silvan's avatar
Silvan committed
		case common.SliceType:
			trackId = netease.Params["trackIds"].(common.SliceType)[0].(json.Number).String()
Silvan's avatar
Silvan committed
		}
		pid := netease.Params["pid"].(string)
		op := netease.Params["op"].(string)
Silvan's avatar
Silvan committed
		proxyRemoteHost := common.HostDomain["music.163.com"]
Silvan's avatar
Silvan committed
		clientRequest := network.ClientRequest{
			Method:    http.MethodPost,
			Host:      "music.163.com",
			RemoteUrl: "http://" + proxyRemoteHost + "/api/playlist/manipulate/tracks",
			Header:    request.Header,
			Body:      ioutil.NopCloser(bytes.NewBufferString("trackIds=[" + trackId + "," + trackId + "]&pid=" + pid + "&op=" + op)),
Silvan's avatar
Silvan committed
			Proxy:     true,
Silvan's avatar
Silvan committed
		}
		resp, err := network.Request(&clientRequest)
		if err != nil {
			return modified
		}
Silvan's avatar
Silvan committed
		defer resp.Body.Close()
		body, err := network.StealResponseBody(resp)
Silvan's avatar
Silvan committed
		if err != nil {
			return modified
		}
Silvan's avatar
Silvan committed
		netease.JsonBody = utils.ParseJsonV2(body)
Silvan's avatar
Silvan committed
		modified = true
	}
	return modified
}
func tryLike(netease *Netease, request *http.Request) bool {
	//fmt.Println("try like")
	modified := false
	if utils.Exist("trackId", netease.Params) {
		trackId := netease.Params["trackId"].(string)
Silvan's avatar
Silvan committed
		proxyRemoteHost := common.HostDomain["music.163.com"]
Silvan's avatar
Silvan committed
		clientRequest := network.ClientRequest{
			Method:    http.MethodGet,
			Host:      "music.163.com",
			RemoteUrl: "http://" + proxyRemoteHost + "/api/v1/user/info",
Silvan's avatar
Silvan committed
			Header:    request.Header,
			Proxy:     true,
		}
Silvan's avatar
Silvan committed
		resp, err := network.Request(&clientRequest)
		if err != nil {
			return modified
		}
Silvan's avatar
Silvan committed
		defer resp.Body.Close()
		body, err := network.StealResponseBody(resp)
Silvan's avatar
Silvan committed
		if err != nil {
			return modified
		}
Silvan's avatar
Silvan committed
		jsonBody := utils.ParseJsonV2(body)
Silvan's avatar
Silvan committed
		if utils.Exist("userPoint", jsonBody) && utils.Exist("userId", jsonBody["userPoint"].(common.MapType)) {
			userId := jsonBody["userPoint"].(common.MapType)["userId"].(json.Number).String()
Silvan's avatar
Silvan committed
			clientRequest.RemoteUrl = "http://" + proxyRemoteHost + "/api/user/playlist?uid=" + userId + "&limit=1"
			resp, err = network.Request(&clientRequest)
			if err != nil {
				return modified
			}
Silvan's avatar
Silvan committed
			defer resp.Body.Close()
			body, err = network.StealResponseBody(resp)
Silvan's avatar
Silvan committed
			if err != nil {
				return modified
			}
Silvan's avatar
Silvan committed
			jsonBody = utils.ParseJsonV2(body)
Silvan's avatar
Silvan committed
			if utils.Exist("playlist", jsonBody) {
Silvan's avatar
Silvan committed
				pid := jsonBody["playlist"].(common.SliceType)[0].(common.MapType)["id"].(json.Number).String()
Silvan's avatar
Silvan committed
				clientRequest.Method = http.MethodPost
				clientRequest.RemoteUrl = "http://" + proxyRemoteHost + "/api/playlist/manipulate/tracks"
				clientRequest.Body = ioutil.NopCloser(bytes.NewBufferString("trackIds=[" + trackId + "," + trackId + "]&pid=" + pid + "&op=add"))
				resp, err = network.Request(&clientRequest)
				if err != nil {
					return modified
				}
Silvan's avatar
Silvan committed
				defer resp.Body.Close()
				body, err = network.StealResponseBody(resp)
Silvan's avatar
Silvan committed
				if err != nil {
					return modified
				}
Silvan's avatar
Silvan committed
				jsonBody = utils.ParseJsonV2(body)
Silvan's avatar
Silvan committed
				code := jsonBody["code"].(json.Number).String()
				if code == "200" || code == "502" {
Silvan's avatar
Silvan committed
					netease.JsonBody = make(common.MapType)
Silvan's avatar
Silvan committed
					netease.JsonBody["code"] = 200
					netease.JsonBody["playlistId"] = pid
					modified = true
				}
			}
		}
	}

	return modified
}
func tryMatch(netease *Netease) bool {
	modified := false
	jsonBody := netease.JsonBody
	if value, ok := jsonBody["data"]; ok {
		switch value.(type) {
Silvan's avatar
Silvan committed
		case common.SliceType:
Silvan's avatar
Silvan committed
			if strings.Contains(netease.Path, "download") {
Silvan's avatar
Silvan committed
				for index, data := range value.(common.SliceType) {
Silvan's avatar
Silvan committed
					if index == 0 {
Silvan's avatar
Silvan committed
						modified = searchGreySong(data.(common.MapType), netease) || modified
Silvan's avatar
Silvan committed
						break
					}
				}
			} else {
Silvan's avatar
Silvan committed
				modified = searchGreySongs(value.(common.SliceType), netease) || modified
Silvan's avatar
Silvan committed
			}
Silvan's avatar
Silvan committed
		case common.MapType:
			modified = searchGreySong(value.(common.MapType), netease) || modified
Silvan's avatar
Silvan committed
		default:
		}
	}
	//modifiedJson, _ := json.Marshal(jsonBody)
	//fmt.Println(string(modifiedJson))
	return modified
}
Silvan's avatar
Silvan committed
func searchGreySongs(data common.SliceType, netease *Netease) bool {
Silvan's avatar
Silvan committed
	modified := false
	for _, value := range data {
		switch value.(type) {
Silvan's avatar
Silvan committed
		case common.MapType:
			modified = searchGreySong(value.(common.MapType), netease) || modified
Silvan's avatar
Silvan committed
		}
	}
	return modified
}
Silvan's avatar
Silvan committed
func searchGreySong(data common.MapType, netease *Netease) bool {
Silvan's avatar
Silvan committed
	modified := false
lphgor's avatar
lphgor committed
	if data["url"] == nil || data["freeTrialInfo"] != nil {
Silvan's avatar
Silvan committed
		data["flag"] = 0
		songId := data["id"].(json.Number).String()
		song := provider.Find(songId)
		haveSongMd5 := false
		if song.Size > 0 {
			modified = true
Silvan's avatar
Silvan committed
			if index := strings.LastIndex(song.Url, "."); index != -1 {
				songType := song.Url[index+1:]
				if songType == "mp3" || songType == "flac" || songType == "ape" || songType == "wav" || songType == "aac" || songType == "mp4" {
					data["type"] = songType
				} else {
					fmt.Println("unrecognized format:", songType)
					if song.Br > 320000 {
						data["type"] = "flac"
					} else {
						data["type"] = "mp3"
					}
				}
			} else if song.Br > 320000 {
Silvan's avatar
Silvan committed
				data["type"] = "flac"
			} else {
				data["type"] = "mp3"
			}
Silvan's avatar
Silvan committed
			if song.Br == 0 {
				if data["type"] == "flac" || data["type"] == "ape" || data["type"] == "wav" {
					song.Br = 999000
				} else {
					song.Br = 128000
				}
			}
Silvan's avatar
Silvan committed
			data["encodeType"] = data["type"] //web
			data["level"] = "standard"        //web
			data["fee"] = 8                   //web
Silvan's avatar
Silvan committed
			uri, err := url.Parse(song.Url)
			if err != nil {
				fmt.Println("url.Parse error:", song.Url)
				data["url"] = song.Url
			} else {
				//fmt.Println(uri.Path)
				//fmt.Println()
				//data["url"] = uri.Scheme + "://" + uri.Host + uri.EscapedPath()
				//data["url"] = uri.String()
Silvan's avatar
Silvan committed
				if *config.EndPoint {
Silvan's avatar
Silvan committed
					data["url"] = generateEndpoint(netease) + uri.String()
Silvan's avatar
Silvan committed
				} else {
					data["url"] = uri.String()
				}
				//fmt.Println(data["url"])
Silvan's avatar
Silvan committed
			}
Silvan's avatar
Silvan committed
			if len(song.Md5) > 0 {
				data["md5"] = song.Md5
				haveSongMd5 = true
			} else {
				h := md5.New()
				h.Write([]byte(song.Url))
				data["md5"] = hex.EncodeToString(h.Sum(nil))
				haveSongMd5 = false
			}
			if song.Br > 0 {
				data["br"] = song.Br
			} else {
				data["br"] = 128000
			}
			data["size"] = song.Size
			data["freeTrialInfo"] = nil
			data["code"] = 200
			if strings.Contains(netease.Path, "download") { //calculate the file md5
				if !haveSongMd5 {
					data["md5"] = calculateSongMd5(songId, song.Url)
				}
			} else if !haveSongMd5 {
				go calculateSongMd5(songId, song.Url)
			}
		}
	}
	return modified
}
func calculateSongMd5(songId string, songUrl string) string {
	songMd5 := ""
	clientRequest := network.ClientRequest{
		Method:    http.MethodGet,
		RemoteUrl: songUrl,
	}
	resp, err := network.Request(&clientRequest)
	if err != nil {
		fmt.Println(err)
		return songMd5
	}
Silvan's avatar
Silvan committed
	defer resp.Body.Close()
Silvan's avatar
Silvan committed
	r := bufio.NewReader(resp.Body)
	h := md5.New()
	_, err = io.Copy(h, r)
	if err != nil {
		fmt.Println(err)
		return songMd5
	}
	songMd5 = hex.EncodeToString(h.Sum(nil))
	provider.UpdateCacheMd5(songId, songMd5)
	//fmt.Println("calculateSongMd5 songId:", songId, ",songUrl:", songUrl, ",md5:", songMd5)
	return songMd5
}
Silvan's avatar
Silvan committed
func processSliceJson(jsonSlice common.SliceType) bool {
Silvan's avatar
Silvan committed
	needModify := false
	for _, value := range jsonSlice {
		switch value.(type) {
Silvan's avatar
Silvan committed
		case common.MapType:
			needModify = processMapJson(value.(common.MapType)) || needModify
Silvan's avatar
Silvan committed

Silvan's avatar
Silvan committed
		case common.SliceType:
			needModify = processSliceJson(value.(common.SliceType)) || needModify
Silvan's avatar
Silvan committed

		default:
			//fmt.Printf("index(%T):%v\n", index, index)
			//fmt.Printf("value(%T):%v\n", value, value)
		}
	}
	return needModify
}
Silvan's avatar
Silvan committed
func processMapJson(jsonMap common.MapType) bool {
Silvan's avatar
Silvan committed
	needModify := false
	if utils.Exists([]string{"st", "subp", "pl", "dl"}, jsonMap) {
		if v, _ := jsonMap["st"]; v.(json.Number).String() != "0" {
			//open grep song
			jsonMap["st"] = 0
			needModify = true
		}
		if v, _ := jsonMap["subp"]; v.(json.Number).String() != "1" {
			jsonMap["subp"] = 1
			needModify = true
		}
		if v, _ := jsonMap["pl"]; v.(json.Number).String() == "0" {
			jsonMap["pl"] = 320000
			needModify = true
		}
		if v, _ := jsonMap["dl"]; v.(json.Number).String() == "0" {
			jsonMap["dl"] = 320000
			needModify = true
		}
	}
	for _, value := range jsonMap {
		switch value.(type) {
Silvan's avatar
Silvan committed
		case common.MapType:
			needModify = processMapJson(value.(common.MapType)) || needModify
		case common.SliceType:
			needModify = processSliceJson(value.(common.SliceType)) || needModify
Silvan's avatar
Silvan committed
		default:
			//if key == "fee" {
			//	fee := "0"
			//	switch value.(type) {
			//	case int:
			//		fee = strconv.Itoa(value.(int))
			//	case json.Number:
			//		fee = value.(json.Number).String()
			//	case string:
			//		fee = value.(string)
			//	}
			//	if fee != "0" && fee != "8" {
			//		jsonMap[key] = 0
			//		needModify = true
			//	}
Silvan's avatar
Silvan committed
			//}
		}
	}
	return needModify
}
Silvan's avatar
Silvan committed

//if os is Windows, use http not https.
func generateEndpoint(netease *Netease) string {
	protocol := "https"
	endPoint := "://music.163.com/unblockmusic/"
Silvan's avatar
Silvan committed
	//fmt.Println(fmt.Sprintf("%+v\n", netease.Params))
Silvan's avatar
Silvan committed
	if headerIntf, ok := netease.Params["header"]; ok {
		header := make(map[string]interface{})
		if headerStr, ok := headerIntf.(string); ok {
			header = utils.ParseJson([]byte(headerStr))
		} else if header, ok = headerIntf.(map[string]interface{}); ok {

		}

		if len(header) > 0 {
			for headerKey, rules := range ReRulesUnderHeader {
				if valueI, ok := header[headerKey]; ok {
					if value, ok := valueI.(string); ok {
						for containValue, protocolValue := range rules {
							//fmt.Println("rules:",containValue,protocolValue)
							//fmt.Println("compare value:",strings.ToLower(value), strings.ToLower(containValue))
							if strings.Contains(strings.ToLower(value), strings.ToLower(containValue)) {
								protocol = protocolValue
							}
						}
					}
				}
			}
		}

	}
	return protocol + endPoint
}