前些天一位高人發的貼子被刪了,好不容易找回來,放在這裏慢慢學習html
如下爲轉載內容:web
優酷視頻的算法在2015年11月24日起至今連續更改了好幾個版本,以前發的這篇臨時解決方案獲得不少響應,很是感謝!如今對這篇文章從新修改,全面規整完整的破解思路(含破解方法)!算法
對了,這篇文章只是針對m3u8格式的視頻。json
所謂工欲善其事必先利其器,作好破解的準備工做會令你事半功倍。api
1.首先準備一個Http抓包工具,PC上推薦Fiddler或者Postman,iOS上推薦Surge數組
2.手備一臺iOS測試設備(由於在Safari裏優酷視頻是肯定使用m3u8進行播放的)cookie
以Fiddler爲例子,先配置Fiddler的設置和iOS設備,讓Fiddler能夠抓取iOS設備的請求數據,這一步若是不會請先自行摸索。dom
打開Safari,輸入優酷網址,進入一個視頻查看Fiddler窗口,須要關注的一些重要信息:工具
前兩步都好說,可第三部的m3u8這麼一個複雜的串,格式如http://pl.youku.com/playlist/m3u8?vid=XMTQzNzIwODQ3Ng==&type=flv&ts=1452839810&keyframe=0&ep=ciaRGEGOX8YB5SPYjD8bNC6xJnIGXJZ3kn7P%2F5gbR8RQKevBzjPcqJ21TPs%3D&sid=045283981007412c2cb59&token=0524&ctype=12&ev=1&oip=2093868719,是怎麼獲得的?回頭看一下抓到的一堆js代碼,你會發現精髓就在這裏——http://player.youku.com/embed/unifull/unifull_.js,這裏面就是所有優酷真實地址的解析算法,源碼很是長,若是你有興趣,能夠對文件格式化一下後慢慢閱讀。下一節就具體來說講這裏邊的算法是怎樣的。學習
在上一節各個步驟標註的重要信息中,get.json這個接口返回的securiy節點就對算法起着很重要的做用(其實從名字就能看出它的特別意義嘛~)
security節點有兩個值,一個是encryp_string,一個是ip。
在http://player.youku.com/embed/unifull/unifull_.js裏,隨處能夠找到YK.m3u8src這個function,其中傳入兩個參數,一個是視頻的vid,另外一個是視頻片斷的格式(如"mp4")。
總結一下這個方法作的事情(這個算法須要計算3個值:sid, token和ep):
1). 對encrypt_string進行Decode64,即對其進行base64解碼,獲得一個byte數組decoded_ep
2).生成一個祕鑰key_a(其值實際上是固定的),利用這個祕鑰與decoded_ep做Rc4算法加密,獲得一個字符串temp
3).temp的結果由「xxxx_xxxx」組成,切割‘_’,前者爲sid,後者爲token
4).生成一個祕鑰key_b(其值也是固定的),利用這個祕鑰與字符串{sid}_{vid}_{token}的ASCII碼字節數組whole記性Rc4算法加密,獲得一個字符串temp2
5).對temp2進行base64轉換,而後再作url encode,獲得ep
至此sid, token和ep就獲得了!
關於Rc4加解密,能夠參考https://zh.wikipedia.org/wiki/RC4
上代碼:
/// <Summary> 2 /// 計算sid, token和ep 3 /// </Summary> 4 public static void GetParameters(string vid, string encryptString, ref string sid, ref string token, ref string ep) 5 { 6 string keyA= "becaf9be"; 7 byte[] decodeEp= Decode64(encryptString); // 對encryptString做base64解碼 8 string temp = Rc4(keyA, decodeEp, false); // 用祕鑰keyA之做Rc4加密,不作base64編碼 9 string[] part = temp.Split('_'); 10 sid = part[0]; 11 token = part[1]; 12 13 string keyB= "bf7e5f01"; 14 byte[] whole= Encoding.ASCII.GetBytes(string.Format("{0}_{1}_{2}", sid, vid, token)); // 組合字符串的ASCII字節數組 15 ep= WebUtility.UrlEncode(Rc4(keyB, whole, true)); // 用祕鑰keyB與之做Rc4加密,且結果進行base64編碼,以後再作url encode 16 } 17 18 private static byte[] Decode64(string a) 19 { 20 if (string.IsNullOrEmpty(a)) 21 { 22 return null; 23 } 24 int f; 25 int g; 26 string h; 27 List<byte> l = new List<byte>(); 28 int[] i = 29 { 30 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 31 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 32 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 33 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 34 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, 35 -1, -1 36 }; 37 for (g = a.Length, f = 0, h = ""; g > f;) 38 { 39 int b; 40 do 41 { 42 b = i[255 & a[f++]]; 43 } while (g > f && -1 == b); 44 45 if (-1 == b) break; 46 47 int c; 48 do 49 { 50 c = i[255 & a[f++]]; 51 } while (g > f && -1 == c); 52 53 if (-1 == c) break; 54 55 byte[] bytes0 = { (byte)(b << 2 | (48 & c) >> 4) }; 56 h += Encoding.ASCII.GetString(bytes0); 57 l.Add(bytes0[0]); 58 int d; 59 do 60 { 61 d = 255 & a[f++]; 62 if (61 == d) return l.ToArray(); 63 d = i[d]; 64 } while (g > f && -1 == d); 65 66 if (-1 == d) break; 67 byte[] bytes1 = { (byte)((15 & c) << 4 | (60 & d) >> 2) }; 68 h += Encoding.ASCII.GetString(bytes1); 69 l.Add(bytes1[0]); 70 int e; 71 do 72 { 73 e = 255 & a[f++]; 74 if (61 == e) return l.ToArray(); 75 e = i[e]; 76 } while (g > f && -1 == e); 77 78 if (-1 == e) break; 79 byte[] bytes2 = { (byte)((3 & d) << 6 | e) }; 80 h += Encoding.ASCII.GetString(bytes2); 81 l.Add(bytes2[0]); 82 } 83 return l.ToArray(); 84 } 85 86 private static string Rc4(string a, byte[] c, bool isToBase64) 87 { 88 // rc4加密算法 89 int f = 0, h = 0, q; 90 int[] b = new int[256]; 91 for (int i = 0; i < 256; i++) 92 { 93 b[i] = i; 94 } 95 while (h < 256) 96 { 97 f = (f + b[h] + a[h % a.Length]) % 256; 98 int temp = b[h]; 99 b[h] = b[f]; 100 b[f] = temp; 101 h++; 102 } 103 104 f = 0; h = 0; q = 0; 105 string result = ""; 106 List<byte> bytesR = new List<byte>(); 107 while (q < c.Length) 108 { 109 h = (h + 1) % 256; 110 f = (f + b[h]) % 256; 111 int temp = b[h]; 112 b[h] = b[f]; 113 b[f] = temp; 114 byte[] bytes = { (byte)(c[q] ^ b[(b[h] + b[f]) % 256]) }; 115 bytesR.Add(bytes[0]); 116 result += Encoding.ASCII.GetString(bytes); 117 q++; 118 } 119 120 if (isToBase64) 121 { 122 var byteR = bytesR.ToArray(); 123 result = Convert.ToBase64String(byteR); 124 //result = Encode64(result); 125 } 126 127 return result; 128 }
若是對祕鑰key_a和祕鑰key_b怎麼獲得有興趣,請繼續細度下面的內容,不感興趣能夠跳過這一段。
在http://player.youku.com/embed/unifull/unifull_.js裏,祕鑰由方法translate(a, b)生成,其中a爲一個組合字符串,b是一個固定的目標數組。
translate方法的做用是利用字符的ASCII碼進行變形,對於組合字符串中的每一個字符,若是是小寫字母a-z,則取其ASCII碼,不然將其ASCII碼+26;接下來若是能在目標數組裏找到這個數碼,若是能找到,且碼的值 > 25,則取碼-26,不然取碼+97對應的字符;最後組合成新的位數相等的字符串,這就是祕鑰。上代碼:
private static string Translate(string a, int[] b) 2 { 3 List<string> c = new List<string>(); 4 foreach (char t in a) 5 { 6 int e; 7 e = t >= 'a' && t <= 'z' ? t - 'a' : t - '0' + 26; 8 for (int j = 0; j < 36; j++) 9 { 10 if (b[j] == e) 11 { 12 e = j; 13 break; 14 } 15 } 16 if (e > 25) 17 { 18 c.Add((e - 26).ToString()); 19 } 20 else 21 { 22 var bytes = new[] { (byte)(e + 97) }; 23 c.Add(Encoding.ASCII.GetString(bytes)); 24 } 25 } 26 string result = c.Aggregate(string.Empty, (current, cc) => current + cc.ToString()); 27 return result; 28 }
兩個祕鑰的原始字符串分別爲b4eto0b4和boa4poz1
目標數組:[19, 1, 4, 7, 30, 14, 28, 8, 24, 17, 6, 35, 34, 16, 9, 10, 13, 22, 32, 29, 31, 21, 18, 3, 2, 23, 25, 27, 11, 20, 5, 15, 12, 0, 33, 26]
(不要問我怎麼知道,看js源碼本身組一下,都是固定的)
接下來就是按照m3u8指定的格式將數值填入便可,格式以下:
http://pl.youku.com/playlist/m3u8?vid={vid}&type={type}&ts={ts}&keyframe=1&ep={ep}&sid={sid}&token={token}&ctype=12&ev=1&oip={oip}
在這個串中還有其餘一些變量須要填寫,其中
type指m3u8的內容中視頻片斷的格式,可取值標清(flv, 3pgphd)、高清(mp4, flvhd)、超清(hd2)、1080P(hd3)
ts指當前時間的Unix時間戳(精確到秒)
oip指security節點中的ip
組合完成後就去請求m3u8地址吧,會獲得一系列的視頻片斷。
算法邏輯清楚了,還須要有寫注意點,一旦忽略就會致使最終即便拼出m3u8地址,也請求不到視頻內容。
回顧一下第二節的3個步驟,每一個步驟都有注意點:
第一個步驟,請求v.youku.com/v_show/id_{vid}.html ,返回值會帶有名字爲ykss的Cookie,這個很重要,不能丟
第二個步驟,請求get.json時,第一步的Cookie:ykss也要發送,而且還要增長一個名爲__ysuid的Cookie,__ysuid=getPvid(6),算法請看以往的更新記錄;還有一點,Referer要填視頻的url地址,絕對不能丟,不然即便get.json能正常返回數據,也能拿到security節點,可是拼出來的m3u8不管你怎麼請求都沒法獲得視頻內容,切記Referer不要丟!
第三個步驟,請求m3u8地址時,就目前看已經不須要再傳名爲r的Cookie,可是以優酷的尿性,保不許之後還會再加上。
若是有什麼疑問,歡迎留言。(如下內容是以往的一些變動記錄,已做廢)
————————————————————————————————————————————————————————————————
11-30更新:
今日又發現了一個問題,這裏補充一下。get.json這個接口返回的時候,Response帶有Cookie,若是你是本身提供web api,這個Cookie記得也要返回去給應用,不然應用即便拼出m3u8的地址,也沒法獲取到完整的視頻內容。
————————————————————————————————————————————————————————————————
12-8更新:
感謝@kkia 發現,
請求http://play.youku.com/play/get.json?vid={vid}&ct=12這個api的時候,http請求須要帶上Referer:{url},url爲須要獲取的視頻的頁面連接,否則會出現非主站請求的錯誤。
另外,cookie校驗機制又啓動了。
因此,若是本身封裝web api,建議每次請求,都把m3u8文件的內容下載下來,本身生成並存儲臨時的m3u8文件,再把這個文件的連接返回給客戶端,再定時去清理這些臨時文件。
————————————————————————————————————————————————————————————————
12-29更新:
感謝@kkia 發現,name=r的Cookie獲取途徑再次發生了變化,步驟更新以下:
1.經過完整的視頻地址url(http://v.youku.com/v_show/id_{vid}.html)中拿到一堆Cookie,只提取其中的ykss=132b825604a2f7516fd91fc0; path=/; domain=.youku.com; 這條Cookie
2.請求get.json時帶上以上名爲ykss的Cookie,且加上Referer=視頻url,請求結果中就會出現名爲r的Cookie了
3.最後請求m3u8地址的時候帶上名爲r的Cookie,Referer=視頻url,就能獲得結果了。
————————————————————————————————————————————————————————————————
12-30更新:
優酷平常做死,又更新了Cookie算法,看來這是持久戰,你們作好心理準備,相信這麼折騰你們都明白名爲r的Cookie的重要性了吧。
目前能夠這樣獲取名爲r的Cookie:
直接請求get.json這個API,頭部須要有以下格式的信息:
Cookie: __ysuid={16-length-string};
Referer: http://v.youku.com/v_show/id_{vid}.html
其中__ysuid的算法以下(感謝@kkia)
public string getPvid(int len) 2 { 3 string[] randchar = new string[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", 4 "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", 5 "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" 6 }; 7 var i = 0; 8 var r = ""; 9 TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); 10 Int64 seconds = Convert.ToInt64(ts.TotalMilliseconds); 11 12 for (i = 0; i < len; i++) 13 { 14 var index = System.Convert.ToInt32(new Random().Next() * Math.Pow(10, 6) % randchar.Length); 15 r += randchar[index]; 16 } 17 return seconds + r; 18 }
由13位長的UNIX時間戳拼接3位長的隨機字符串組成__ysuid。
事實上,只要在請求m3u8連接的時候Cookie裏含有r(不用管value是啥,至少現階段一直是這樣的),就能請求成功。
附上WPF的demo
本文僅供學習參考