├── .gitignore ├── .travis.yml ├── README.md ├── main.go └── util ├── helper.go └── helper_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - 1.10.x 6 | - 1.11.x 7 | - 1.12.x 8 | - 1.13.x 9 | - master 10 | 11 | before_install: 12 | - go get -t -v ./... 13 | 14 | script: 15 | - go test -race -coverprofile=coverage.txt -covermode=atomic 16 | 17 | after_success: 18 | - bash <(curl -s https://codecov.io/bash) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # license-proxy 2 | 3 | [![Build Status](https://travis-ci.org/easonlin404/license-proxy.svg)](https://travis-ci.org/easonlin404/license-proxy) 4 | [![codecov](https://codecov.io/gh/easonlin404/license-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/easonlin404/license-proxy) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/easonlin404/license-proxy)](https://goreportcard.com/report/github.com/easonlin404/license-proxy) 6 | [![GoDoc](https://godoc.org/github.com/easonlin404/license-proxy?status.svg)](https://godoc.org/github.com/easonlin404/license-proxy) 7 | 8 | Simplified Widevine license proxy server written in Go (Golang). 9 | 10 | ## Usage 11 | 12 | ### Start using it 13 | 14 | Download and install it: 15 | 16 | ```bash 17 | $ go get github.com/easonlin404/license-proxy 18 | ``` 19 | 20 | 21 | ## License 22 | license-proxy is MIT License. 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "strconv" 12 | 13 | "github.com/easonlin404/license-proxy/util" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | var provider = "widevine_test" 18 | var url = "https://license.uat.widevine.com/cenc/getlicense/" + provider 19 | var allowedTrackTypes = "SD_HD" 20 | var key = "1ae8ccd0e7985cc0b6203a55855a1034afc252980e970ca90e5202689f947ab9" 21 | var iv = "d58ce954203b7c9a9a9d467f59839249" 22 | 23 | type LicenseRequest struct { 24 | Request string `json:"request"` 25 | Signature string `json:"signature"` 26 | Signer string `json:"signer"` 27 | } 28 | 29 | type Request struct { 30 | Payload string `json:"payload"` 31 | ContentId string `json:"content_id"` 32 | Provider string `json:"provider"` 33 | AllowedTrackTypes string `json:"allowed_track_types"` 34 | } 35 | 36 | func main() { 37 | 38 | r := gin.Default() 39 | 40 | r.POST("/proxy", func(c *gin.Context) { 41 | body, _ := ioutil.ReadAll(c.Request.Body) 42 | 43 | // allow cross domain AJAX requests 44 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 45 | 46 | if len(body) == 0 { 47 | c.String(400, string("Empty Request")) 48 | return 49 | } 50 | 51 | fmt.Println("Requst POST body:") 52 | fmt.Println(string(body)) 53 | 54 | resBody, s := generateLicense(body) 55 | 56 | status, _ := strconv.Atoi(s) 57 | 58 | var licenseResponse map[string]interface{} 59 | json.Unmarshal([]byte(resBody), &licenseResponse) 60 | 61 | indentJson, _ := json.Marshal(licenseResponse) 62 | fmt.Println("response Body:", string(indentJson)) 63 | 64 | jsonStatus := licenseResponse["status"].(string) 65 | if jsonStatus == "OK" { 66 | license := licenseResponse["license"].(string) 67 | fmt.Println(license) 68 | 69 | licenseDecode, _ := base64.StdEncoding.DecodeString(license) 70 | c.String(status, string(licenseDecode)) 71 | } else { 72 | c.String(status, jsonStatus) 73 | } 74 | 75 | }) 76 | 77 | r.Run(":9000") // listen and serve on 0.0.0.0:9000 78 | 79 | } 80 | 81 | func buildMessage(body []byte) []byte { 82 | var request Request 83 | request.Payload = base64.StdEncoding.EncodeToString(body) 84 | 85 | //request.ContentId 86 | request.Provider = provider 87 | request.AllowedTrackTypes = allowedTrackTypes 88 | 89 | message, _ := json.Marshal(request) 90 | return message 91 | } 92 | 93 | func genrateLicenseRequest(body []byte) LicenseRequest { 94 | keyByteAry, _ := hex.DecodeString(key) 95 | ivByteAry, _ := hex.DecodeString(iv) 96 | 97 | message := buildMessage(body) 98 | 99 | var licenseRequest LicenseRequest 100 | licenseRequest.Request = base64.StdEncoding.EncodeToString(message) 101 | licenseRequest.Signature = util.GenerateSignature(keyByteAry, ivByteAry, message) 102 | licenseRequest.Signer = provider 103 | 104 | return licenseRequest 105 | } 106 | 107 | func generateLicense(body []byte) (string, string) { 108 | licenseRequest := genrateLicenseRequest(body) 109 | jsonStr, err := json.Marshal(licenseRequest) 110 | if err != nil { 111 | fmt.Println("json err:", err) 112 | } 113 | 114 | fmt.Println(string(jsonStr)) 115 | 116 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr)) 117 | 118 | req.Header.Set("Content-Type", "application/json") 119 | client := &http.Client{} 120 | resp, err := client.Do(req) 121 | if err != nil { 122 | panic(err) 123 | } 124 | defer resp.Body.Close() 125 | 126 | fmt.Println("response Status:", resp.Status) 127 | fmt.Println("response Headers:", resp.Header) 128 | resBody, _ := ioutil.ReadAll(resp.Body) 129 | //fmt.Println("response Body:", string(resBody)) 130 | 131 | return string(resBody), resp.Status 132 | } 133 | -------------------------------------------------------------------------------- /util/helper.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/sha1" 8 | "encoding/base64" 9 | "fmt" 10 | ) 11 | 12 | func makeSha1(s []byte) []byte { 13 | h := sha1.New() 14 | h.Write([]byte(s)) 15 | bs := h.Sum(nil) 16 | return bs 17 | } 18 | 19 | func GenerateSignature(key []byte, iv []byte, message []byte) string { 20 | sha1_message := makeSha1(message) 21 | 22 | block, err := aes.NewCipher(key) 23 | if err != nil { 24 | fmt.Println("key error1", err) 25 | } 26 | 27 | cbc := cipher.NewCBCEncrypter(block, iv) 28 | content := PKCS5Padding(sha1_message, block.BlockSize()) 29 | crypted := make([]byte, len(content)) 30 | cbc.CryptBlocks(crypted, content) 31 | return base64.StdEncoding.EncodeToString(crypted) 32 | } 33 | 34 | func PKCS5Padding(ciphertext []byte, blockSize int) []byte { 35 | padding := blockSize - len(ciphertext)%blockSize 36 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 37 | return append(ciphertext, padtext...) 38 | } 39 | -------------------------------------------------------------------------------- /util/helper_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | func TestGenerateSignature(*testing.T) { 11 | key, _ := hex.DecodeString("1ae8ccd0e7985cc0b6203a55855a1034afc252980e970ca90e5202689f947ab9") 12 | iv, _ := hex.DecodeString("d58ce954203b7c9a9a9d467f59839249") 13 | 14 | plaintextBase64 := "ew0KICAicGF5bG9hZCIgOiAiQ0FFU3NBd0toUXNJQVJMc0NRcXZBZ2dDRWhGRGFISnZiV1ZEUkUwdFRXRmpMWGc0TmhqMXFJNk9CU0tPQWpDQ0FRb0NnZ0VCQUxrdTJ0VGp4R2FLQVFkTmRZWmM0QUQvZjI2Y3RqRG9YcGI0NlNCdEhmL0h4NWRuL1FQSVhPd2pJQWEwWFd1dXFycTJ6Sm8rWWovVDl0WFJrdDBrZ3E0MVkyem1nMmFLd01SY2dIbkRjdUQ1RnZwQ0RBNHR6MDAvT0hXakJINkFjWjFPM3hEVnVHVDA5TDRITlI2NkFXcEQ2b0hRV1F5aEhzMkNRTy84TnhqcDh3OHdQaldFbzF2SER1K0gzU2NvV2huWUp0R3Z1Y1ZPdVo3ZitUVlNQWmViM25PMVBkNythMHJGU21VMGRPUUFNVUdNRHdzdUovQ3g3RTAzdWdWalpCanNndWx5cll1VmRwUXJITTBWWUh4dlB6Q201WjFGWklHQyt3eEx5RUI1WUtqL0hLajFxc3RuK1lyYmFIWmovK0R5RVkzRi9hSnJjT1B0ZnpuMVRGTUNBd0VBQVNqOUlSS0FBblA2dEJMbWxxWHN3T3drNTA2eCthNlVWSjhQZUY1TklxSGpaVUk1dmNlSXM1WndqSkY4Rkd0V0xtcFVwcmRiNEpWV2JWTXc4RFBQSkJNajhDWElkRmNlc1VSWHlYWWthMnNWcEpka3hxalY3WDcyZXBUNlRZS1QwUVB2Q0hjeTBpenRHNGNDU1dqTVZiMGlYeDlQRjB4djlySS9ORktBSTJFVExFcnB4dEl1TlUrazRIVDhFVHRMdlhkWGRWRGpjOG56cHkvMzh1empraE9DSzlNQXVQUXJ1eFNhbHlqUmplaThSaGpibG42YnJ4WnVvcmFEU0ZZdFZVbXBWcVE5bmxRdTJIZkdFOUtCeE9MNjQ5TVZOMzBMeDhJbVpmSTdOUFFLdjlpZG1jQnhsK1A1aVBra2VJNk9nQ3JUL1JiTXFYcTIzSnh6Q0dJczFsaU5VNnhPa1JBYXRBVUtyZ0lJQVJJUVN1aGlBQlA3aHBsbldIQ2pYelMyT0JqL3BJNk9CU0tPQWpDQ0FRb0NnZ0VCQUs2ckN5YzBaZGMycmRxTHR1dFNySm1XcENGa2Q4RmZCSWV4VFdxVmQ3M0ZURHBtSVdLelZJQmtsamE0WmZZdkFsN29qbWg5ZnVOTmFGQTVkVlpCY0o5dTRmelFBeFBQQmE3OEVOT0VLcTZxOG9NNkF1WS8wSHRBNFNmVmJ4bGdtMVJjMlVUTFFiWWZVSTBXcnBTanZBQ2JYUlhiSEtWd2VqalVMVzhZazVJYzJrbUNweXZuRkZoTmxtdFBpdEtyNHQzeEI3ZkxhbWJLQVhvVE5qMGJ2SHBnRk0wajZBOFh6VkdCTHpCbWtnalhpKy90MXVnT0pzZjhPTFI4M1NZcy9sbFFRWFdFV3drQ1FpcFpleGEyNU0zS3M4Y293dFdLSXZmN0hIeWpYamRSODZOK1ZtRFhsLy80SXZlYnl2MUpka1lXcUlvdStqRFJGQzVrZWlHemZZVUNBd0VBQVNqOUlSS0FBemdNSzkzajlacGJOTjQxSGF0WEozTFdnTFJheGNFMFVMZjZTSlRGbTVSbkQrSFJpQzJYWUNqaFVxZmpvclViWUo0OG02enNPN3VVSUsvUkI3ZzFUUTNWTWhTNGFVV09aUUlYUkFMWDN0NmNITnAzZHUrZDJTbFkxRWdnL0xDb2d1b1FGNUFYSHdYY24rVWxKQVlINTlIQ21pc1JyVElqdktva3VCMTd6VHVtK1FrWHJmazF5dy81UnpUYU9kb0hkQzMxYTNUL3YwNU5hbmNZSTR3WWRkdXhPOGJuYVhpWDdTK1k2Zk1KTldKT0NMNEdwYWpReGlEdkc4L2I3OExlYS8yNGprVTJNbmZtRkRsZ0F1VkM0dGxEZ2I3TVVaRUVSeFJmR0V6NTlEdURIdW01SnJzTmZTVWkrRnpxZjFyU211YXRDS2J0NDFaT3RCcy9zNDBPaTNBYW1rT05odXpFZ1hwZXdSVyttaHQ4ckdNLzFaSy9SbjR2c3B6Sm5qaVpKOEtoQmY4ckQ0RER2UWNSWFB0TmZ3TzZsWWIyL2t0bkhGOWtTRXcyNWF4OVZkYVFsTWhYUDRJRnNwVGx6ZVM0MnVsL3dFbi9wam1ST0hKYWVVSEwrZXMvZnlYSXVHclM0R1F2S2FldG9hNVE2UmR6dUF1U3NOc0Y3VHBkV0RZQzhSb2JDaEZoY21Ob2FYUmxZM1IxY21WZmJtRnRaUklHZURnMkxUWTBHaFlLREdOdmJYQmhibmxmYm1GdFpSSUdSMjl2WjJ4bEdoY0tDbTF2WkdWc1gyNWhiV1VTQ1VOb2NtOXRaVU5FVFJvWENnMXdiR0YwWm05eWJWOXVZVzFsRWdaTllXTlBVMWdhSVFvVWQybGtaWFpwYm1WZlkyUnRYM1psY25OcGIyNFNDVEV1TkM0NExqazJNaklJQ0FBUUFCZ0JJQUFTbXdFS21BRUtnUUVTRU1BVDJkWithMVlTamxKdy9ES2xaalVTRUlRdXJmZVA5MUhSbGV4bFBOY3Q5ZWtTRVBvVGQ1ZlBUMXQ4cjZsYmF4NVF0UzBTRUZlNlFLdnFTRnpndmxoVWEzcVJkY2dTRUl3dS8xSWZTbDVEaGEwZllpd3FHWWthRFhkcFpHVjJhVzVsWDNSbGMzUWlFSys3WVJGT2lJL1pKRE1haElJN2xuNUk0OXlWbXdZUUFSb1FQdTNya21NOERBSE9nbEZxSzE3QmpCZ0JJSURnMmNZRk1CVWFnQUpJRTQ2dzEyUkM2MjlpTng1cGZJVGZzY1BLVmtnbzV5MnU3dzBORnl1OGJqVkxSWE50MW9CeVJ0VitDL3l4V0JuallRaTVaWGNqTHFVcUNVT3M1MEEreHlxNXkxNytrRGxDQkdTOXNoUDJCa052UzBwSGU5OWdCcnh3cTlNSWxTZ2Y4ZUxrUWQ0RFcyVEc1SjRFTmpIZjQvOVlDNkhCT1EvamhNdlJyYk51VHRsZzNnR2pQcHAvUTJWZzhtODBJTlpYbER2SklqUnZyVksrbXdhNVNDdHNoU2dlQkl0SFY1YmNOSy8vK1l5UElEZFhwMVgrT204Z2tGQ1ZqS2E3QjdyN3hCQlNEblpjYzhyaWExWWFRdUo1Zk45UU8yMmZYUGhMRkpacTRzK0VVM3RsTUFFa1hwaGRhUWVBUHJZR04vcUtydFU1TktWZGFEdFd3eEtYdnAxVyIsDQogICJwcm92aWRlciIgOiAid2lkZXZpbmVfdGVzdCIsDQogICJjb250ZW50X2lkIjogInI3dGhFVTZJajlra014cUVnanVXZmc9PSIsDQogICJjb250ZW50X2tleV9zcGVjcyI6IFsNCiAgICB7ICJ0cmFja190eXBlIjogIlNEX0hEIiB9DQogIF0NCn0NCg==" 15 | message, _ := base64.StdEncoding.DecodeString(plaintextBase64) 16 | fmt.Println("Message:") 17 | fmt.Println(string(message)) 18 | 19 | signature := GenerateSignature(key, iv, message) 20 | 21 | fmt.Println("signature:" + signature) 22 | 23 | } 24 | --------------------------------------------------------------------------------