├── .gitignore ├── README.md ├── WhatsAppDotNet.sln ├── WhatsAppDotNet ├── Program.cs └── WhatsAppDotNet.csproj └── WhatsAppLib ├── Logger ├── ConsoleLogger.cs └── ILogger.cs ├── Messages ├── ImageMessage.cs ├── MessageBase.cs └── TextMessage.cs ├── Models ├── MediaConnResponse.cs ├── MediaKeys.cs ├── Node.cs ├── ReceiveModel.cs ├── SessionInfo.cs ├── UploadResponse.cs └── WriteBinaryType.cs ├── Proto └── Def.cs ├── Serialization ├── BinaryDecoder.cs └── BinaryEncoder.cs ├── Utils ├── Extend.cs └── LogUtil.cs ├── WhatsApp.cs ├── WhatsAppLib.csproj ├── WhatsAppLib.csproj.user └── Yove.Proxy ├── ProxyClient.cs └── ProxyType.cs /.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | obj 3 | bin 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WhatsAppDotNet 2 | ## WhatsApp协议的Net版 3 | 4 | ### 改写于 [go](https://github.com/Rhymen/go-whatsapp) 5 | ``` 6 | 1.0 完成登录,重新登录,发送文本消息,发送图片消息,接收消息 7 | 1.1 添加日记和Scoket代理,多谢[Yove.Proxy](https://github.com/TheSuunny/Yove.Proxy) 8 | ``` 9 | -------------------------------------------------------------------------------- /WhatsAppDotNet.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30517.126 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WhatsAppDotNet", "WhatsAppDotNet\WhatsAppDotNet.csproj", "{B1C10D36-3A50-43AE-B3A7-6159CEE870E2}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WhatsAppLib", "WhatsAppLib\WhatsAppLib.csproj", "{283C0154-F70B-42CE-804E-C2DEE96E7CF7}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {B1C10D36-3A50-43AE-B3A7-6159CEE870E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {B1C10D36-3A50-43AE-B3A7-6159CEE870E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {B1C10D36-3A50-43AE-B3A7-6159CEE870E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {B1C10D36-3A50-43AE-B3A7-6159CEE870E2}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {283C0154-F70B-42CE-804E-C2DEE96E7CF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {283C0154-F70B-42CE-804E-C2DEE96E7CF7}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {283C0154-F70B-42CE-804E-C2DEE96E7CF7}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {283C0154-F70B-42CE-804E-C2DEE96E7CF7}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {10072B6B-FBF1-45FF-A50D-0A2BFD84ABF1} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /WhatsAppDotNet/Program.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.IO; 4 | using WhatsAppLib; 5 | using WhatsAppLib.Messages; 6 | using WhatsAppLib.Models; 7 | 8 | namespace WhatsAppDotNet 9 | { 10 | class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | //Http抓包 15 | WhatsApp whatsApp = new WhatsApp(new Yove.Proxy.ProxyClient("127.0.0.1", 8888,Yove.Proxy.ProxyType.Http)); 16 | //Http代理 17 | //WhatsApp whatsApp = new WhatsApp(new Yove.Proxy.ProxyClient("127.0.0.1", 1081, Yove.Proxy.ProxyType.Http)); 18 | //Socks5代理 19 | //WhatsApp whatsApp = new WhatsApp(new Yove.Proxy.ProxyClient("127.0.0.1", 1080, Yove.Proxy.ProxyType.Socks5)); 20 | if (File.Exists("Session.ini")) 21 | { 22 | whatsApp.Session = JsonConvert.DeserializeObject(File.ReadAllText("Session.ini")); 23 | } 24 | whatsApp.LoginScanCodeEvent += WhatsApp_LoginScanCodeEvent; 25 | whatsApp.LoginSuccessEvent += WhatsApp_LoginSuccessEvent; 26 | whatsApp.ReceiveImageMessageEvent += WhatsApp_ReceiveImageMessageEvent; 27 | whatsApp.ReceiveTextMessageEvent += WhatsApp_ReceiveTextMessageEvent; 28 | whatsApp.ReceiveRemainingMessagesEvent += WhatsApp_ReceiveRemainingMessagesEvent; 29 | whatsApp.Login(); 30 | Console.ReadLine(); 31 | } 32 | 33 | private static void WhatsApp_ReceiveTextMessageEvent(TextMessage obj) 34 | { 35 | Console.WriteLine($"{obj.RemoteJid}-{obj.MessageTimestamp}-{obj.Text}"); 36 | } 37 | 38 | private static void WhatsApp_ReceiveRemainingMessagesEvent(ReceiveModel obj) 39 | { 40 | Console.WriteLine($"{obj.StringData}"); 41 | } 42 | 43 | private static void WhatsApp_ReceiveImageMessageEvent(ImageMessage obj) 44 | { 45 | Console.WriteLine($"{obj.RemoteJid}-{obj.MessageTimestamp}-{obj.Text}-{obj.ImageData?.Length}"); 46 | } 47 | 48 | private static void WhatsApp_LoginSuccessEvent(SessionInfo obj) 49 | { 50 | File.WriteAllText("Session.ini", JsonConvert.SerializeObject(obj)); 51 | } 52 | 53 | private static void WhatsApp_LoginScanCodeEvent(string obj) 54 | { 55 | Console.WriteLine($"请使用手机WhatsApp扫描该二维码登录(Please use your mobile WhatsApp to scan the QR code to log in):{obj}"); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /WhatsAppDotNet/WhatsAppDotNet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /WhatsAppLib/Logger/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace WhatsAppLib.Logger 6 | { 7 | public class ConsoleLogger : ILogger 8 | { 9 | public void Debug(string log) 10 | { 11 | Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}-DEBUG--{log}"); 12 | } 13 | 14 | public void Error(string log) 15 | { 16 | Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}-ERROR--{log}"); 17 | } 18 | 19 | public void Fatal(string log) 20 | { 21 | Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}-FATAL--{log}"); 22 | } 23 | 24 | public void Info(string log) 25 | { 26 | Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}-INFO--{log}"); 27 | } 28 | 29 | public void Trace(string log) 30 | { 31 | Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}-TRACE--{log}"); 32 | } 33 | 34 | public void Warn(string log) 35 | { 36 | Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}-WARN--{log}"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /WhatsAppLib/Logger/ILogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace WhatsAppLib.Logger 6 | { 7 | public interface ILogger 8 | { 9 | void Trace(string log); 10 | void Debug(string log); 11 | void Info(string log); 12 | void Warn(string log); 13 | void Error(string log); 14 | void Fatal(string log); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /WhatsAppLib/Messages/ImageMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace WhatsAppLib.Messages 6 | { 7 | public class ImageMessage:MessageBase 8 | { 9 | public byte[] ImageData { set; get; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WhatsAppLib/Messages/MessageBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace WhatsAppLib.Messages 6 | { 7 | public abstract class MessageBase 8 | { 9 | /// 10 | /// 消息生成时间 11 | /// 12 | public ulong MessageTimestamp { set; get; } 13 | /// 14 | /// 对方Id 15 | /// 16 | public string RemoteJid { set; get; } 17 | /// 18 | /// 消息文字 19 | /// 20 | public string Text { set; get; } 21 | /// 22 | /// 消息ID 23 | /// 24 | public string MsgId { set; get; } 25 | /// 26 | /// 是否是本人发送消息 27 | /// 28 | public bool FromMe { set; get; } 29 | public int Status { set; get; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WhatsAppLib/Messages/TextMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace WhatsAppLib.Messages 6 | { 7 | public class TextMessage:MessageBase 8 | { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WhatsAppLib/Models/MediaConnResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace WhatsAppLib.Models 9 | { 10 | internal class MediaConnResponse 11 | { 12 | [JsonProperty("status")] 13 | public int Status { get; set; } 14 | [JsonProperty("media_conn")] 15 | public MediaConnModel MediaConn { get; set; } 16 | 17 | public class HostsModel 18 | { 19 | [JsonProperty("hostname")] 20 | public string Hostname { get; set; } 21 | } 22 | 23 | public class MediaConnModel 24 | { 25 | [JsonProperty("auth")] 26 | public string Auth { get; set; } 27 | [JsonProperty("ttl")] 28 | public int Ttl { get; set; } 29 | [JsonProperty("hosts")] 30 | public List Hosts { get; set; } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /WhatsAppLib/Models/MediaKeys.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WhatsAppLib.Models 8 | { 9 | internal class MediaKeys 10 | { 11 | public byte[] Iv { set; get; } 12 | public byte[] CipherKey { set; get; } 13 | public byte[] MacKey { set; get; } 14 | public MediaKeys(byte[] data) 15 | { 16 | Iv = data.Take(16).ToArray(); 17 | CipherKey = data.Skip(16).Take(32).ToArray(); 18 | MacKey = data.Skip(48).Take(32).ToArray(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /WhatsAppLib/Models/Node.cs: -------------------------------------------------------------------------------- 1 | using Proto; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using WhatsAppLib.Serialization; 5 | 6 | namespace WhatsAppLib.Models 7 | { 8 | internal class Node 9 | { 10 | public string Description { set; get; } 11 | public Dictionary Attributes { set; get; } 12 | public object Content { set; get; } 13 | public byte[] Marshal() 14 | { 15 | if (Attributes != null && Attributes.Count > 0 && Content != null&& Content is List wms) 16 | { 17 | var nl = new List(); 18 | foreach (var wm in wms) 19 | { 20 | var bs = new byte[4096]; 21 | var me = new Google.Protobuf.CodedOutputStream(bs); 22 | wm.WriteTo(me); 23 | nl.Add( new Node 24 | { 25 | Content = bs.Take((int)me.Position).ToArray(), 26 | Description = "message" 27 | }); 28 | } 29 | Content = nl; 30 | } 31 | BinaryEncoder binaryEncoder = new BinaryEncoder(); 32 | return binaryEncoder.WriteNode(this); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /WhatsAppLib/Models/ReceiveModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.WebSockets; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace WhatsAppLib.Models 9 | { 10 | public class ReceiveModel 11 | { 12 | public byte[] ByteData { private set; get; } 13 | public string StringData { private set; get; } 14 | public string Tag { private set; get; } 15 | public string Body { private set; get; } 16 | public WebSocketMessageType MessageType { set; get; } 17 | internal Node Nodes { set; get; } 18 | internal ArraySegment ReceiveData { set; get; } 19 | private List _bs = new List(); 20 | private ReceiveModel(int length) 21 | { 22 | ReceiveData = new ArraySegment(new byte[length]); 23 | } 24 | private ReceiveModel(byte[] bs) 25 | { 26 | ByteData = bs; 27 | if (bs != null) 28 | { 29 | StringData = Encoding.UTF8.GetString(ByteData); 30 | } 31 | } 32 | internal static ReceiveModel GetReceiveModel(int length = 1024) 33 | { 34 | return new ReceiveModel(length); 35 | } 36 | internal static ReceiveModel GetReceiveModel(byte[] bs) 37 | { 38 | return new ReceiveModel(bs); 39 | } 40 | internal void Continue(int count) 41 | { 42 | _bs.AddRange(ReceiveData.Take(count)); 43 | } 44 | internal void End(int count, WebSocketMessageType messageType) 45 | { 46 | _bs.AddRange(ReceiveData.Take(count)); 47 | ByteData = _bs.ToArray(); 48 | StringData = Encoding.UTF8.GetString(ByteData); 49 | var index = StringData.IndexOf(","); 50 | if (index >= 0 && index < StringData.Length) 51 | { 52 | Tag = StringData.Substring(0, index); 53 | Body = StringData.Substring(index + 1); 54 | } 55 | MessageType = messageType; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /WhatsAppLib/Models/SessionInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WhatsAppLib.Models 8 | { 9 | public class SessionInfo 10 | { 11 | /// 12 | /// 客户端Id 13 | /// 14 | public string ClientId { set; get; } 15 | /// 16 | /// 客户端Token 17 | /// 18 | public string ClientToken { set; get; } 19 | /// 20 | /// 服务端Token 21 | /// 22 | public string ServerToken { set; get; } 23 | /// 24 | /// 通信加密密钥 Communication encryption key 25 | /// 26 | public byte[] EncKey { set; get; } 27 | /// 28 | /// 签名加密密钥 Signature encryption key 29 | /// 30 | public byte[] MacKey { set; get; } 31 | /// 32 | /// 当前登录Id Current login ID 33 | /// 34 | public string Wid { set; get; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /WhatsAppLib/Models/UploadResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WhatsAppLib.Models 8 | { 9 | internal class UploadResponse 10 | { 11 | public string DownloadUrl { set; get; } 12 | public byte[] MediaKey { set; get; } 13 | public byte[] FileSha256 { set; get; } 14 | public byte[] FileEncSha256 { set; get; } 15 | public ulong FileLength { set; get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WhatsAppLib/Models/WriteBinaryType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace WhatsAppLib.Models 6 | { 7 | public enum WriteBinaryType 8 | { 9 | DebugLog = 1, 10 | QueryResume, 11 | QueryReceipt, 12 | QueryMedia, 13 | QueryChat, 14 | QueryContacts, 15 | QueryMessages, 16 | Presence, 17 | PresenceSubscribe, 18 | Group, 19 | Read, 20 | Chat, 21 | Received, 22 | Pic, 23 | Status, 24 | Message, 25 | QueryActions, 26 | Block, 27 | QueryGroup, 28 | QueryPreview, 29 | QueryEmoji, 30 | QueryMessageInfo, 31 | Spam, 32 | QuerySearch, 33 | QueryIdentity, 34 | QueryUrl, 35 | Profile, 36 | Contact, 37 | QueryVcard, 38 | QueryStatus, 39 | QueryStatusUpdate, 40 | PrivacyStatus, 41 | QueryLiveLocations, 42 | LiveLocation, 43 | QueryVname, 44 | QueryLabels, 45 | Call, 46 | QueryCall, 47 | QueryQuickReplies 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /WhatsAppLib/Serialization/BinaryDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using WhatsAppLib.Models; 6 | using WhatsAppLib.Utils; 7 | 8 | namespace WhatsAppLib.Serialization 9 | { 10 | internal class BinaryDecoder 11 | { 12 | private byte[] _data; 13 | private int _index; 14 | public static List SingleByteTokens = new List {"", "", "", "200", "400", "404", "500", "501", "502", "action", "add", 15 | "after", "archive", "author", "available", "battery", "before", "body", 16 | "broadcast", "chat", "clear", "code", "composing", "contacts", "count", 17 | "create", "debug", "delete", "demote", "duplicate", "encoding", "error", 18 | "false", "filehash", "from", "g.us", "group", "groups_v2", "height", "id", 19 | "image", "in", "index", "invis", "item", "jid", "kind", "last", "leave", 20 | "live", "log", "media", "message", "mimetype", "missing", "modify", "name", 21 | "notification", "notify", "out", "owner", "participant", "paused", 22 | "picture", "played", "presence", "preview", "promote", "query", "raw", 23 | "read", "receipt", "received", "recipient", "recording", "relay", 24 | "remove", "response", "resume", "retry", "s.whatsapp.net", "seconds", 25 | "set", "size", "status", "subject", "subscribe", "t", "text", "to", "true", 26 | "type", "unarchive", "unavailable", "url", "user", "value", "web", "width", 27 | "mute", "read_only", "admin", "creator", "short", "update", "powersave", 28 | "checksum", "epoch", "block", "previous", "409", "replaced", "reason", 29 | "spam", "modify_tag", "message_info", "delivery", "emoji", "title", 30 | "description", "canonical-url", "matched-text", "star", "unstar", 31 | "media_key", "filename", "identity", "unread", "page", "page_count", 32 | "search", "media_message", "security", "call_log", "profile", "ciphertext", 33 | "invite", "gif", "vcard", "frequent", "privacy", "blacklist", "whitelist", 34 | "verify", "location", "document", "elapsed", "revoke_invite", "expiration", 35 | "unsubscribe", "disable", "vname", "old_jid", "new_jid", "announcement", 36 | "locked", "prop", "label", "color", "call", "offer", "call-id","quick_reply","sticker","pay_t","accept","reject","sticker_pack","invalid","canceled","missed","connected","result","audio","video","recent" }; 37 | 38 | public BinaryDecoder(byte[] bs) 39 | { 40 | _data = bs; 41 | } 42 | public byte ReadByte() 43 | { 44 | return _data[_index++]; 45 | } 46 | public byte[] ReadBytes(int n) 47 | { 48 | var ret = _data.Skip(_index).Take(n).ToArray(); 49 | _index += n; 50 | return ret; 51 | } 52 | public int ReadInt8(bool littleEndian = false) 53 | { 54 | return ReadIntN(1, littleEndian); 55 | } 56 | public int ReadInt16(bool littleEndian = false) 57 | { 58 | return ReadIntN(2, littleEndian); 59 | } 60 | public int ReadInt20() 61 | { 62 | var ret = (((_data[_index]) & 15) << 16 )+ ((_data[_index + 1]) << 8 )+ ((_data[_index + 2])); 63 | _index += 3; 64 | return ret; 65 | } 66 | public int ReadInt32(bool littleEndian = false) 67 | { 68 | return ReadIntN(4, littleEndian); 69 | } 70 | public long ReadInt64(bool littleEndian = false) 71 | { 72 | return ReadIntN(8, littleEndian); 73 | } 74 | private int ReadIntN(int n, bool littleEndian) 75 | { 76 | int ret = 0; 77 | for (int i = 0; i < n; i++) 78 | { 79 | int curShift = i; 80 | if (!littleEndian) 81 | { 82 | curShift = n - i - 1; 83 | } 84 | ret |= _data[_index + i] << (curShift * 8); 85 | } 86 | _index += n; 87 | return ret; 88 | } 89 | public int ReadListSize(int size) 90 | { 91 | switch (size) 92 | { 93 | case 248: 94 | return ReadInt8(); 95 | case 0: 96 | return 0; 97 | case 249: 98 | return ReadInt16(); 99 | default: 100 | LogUtil.Warn("readListSize with unknown tag"); 101 | return 0; 102 | } 103 | } 104 | public Dictionary ReadAttributes(int n) 105 | { 106 | var ret = new Dictionary(); 107 | for (int i = 0; i < n; i++) 108 | { 109 | var idx = ReadInt8(); 110 | var index = ReadString(idx); 111 | idx = ReadInt8(); 112 | ret.Add(index, ReadString(idx)); 113 | } 114 | return ret; 115 | } 116 | public string ReadStringFromChars(int length) 117 | { 118 | var str = Encoding.UTF8.GetString(_data, _index, length); 119 | _index += length; 120 | return str; 121 | } 122 | public string ReadString(int tag) 123 | { 124 | if (tag >= 3 && tag <= SingleByteTokens.Count) 125 | { 126 | var tok = SingleByteTokens[tag]; 127 | if (tok == "s.whatsapp.net") 128 | { 129 | tok = "c.us"; 130 | } 131 | return tok; 132 | } 133 | else if (tag >= 236 && tag <= 239) 134 | { 135 | var i = ReadInt8(); 136 | return null; 137 | } 138 | else if (tag == (int)ReadStringTag.LIST_EMPTY) 139 | { 140 | return ""; 141 | } 142 | else if (tag == (int)ReadStringTag.BINARY_8) 143 | { 144 | var length = ReadInt8(); 145 | return ReadStringFromChars(length); 146 | } 147 | else if (tag == (int)ReadStringTag.BINARY_20) 148 | { 149 | var length = ReadInt20(); 150 | return ReadStringFromChars(length); 151 | } 152 | else if (tag == (int)ReadStringTag.BINARY_32) 153 | { 154 | var length = ReadInt32(); 155 | return ReadStringFromChars(length); 156 | } 157 | else if (tag == (int)ReadStringTag.JID_PAIR) 158 | { 159 | var b = ReadByte(); 160 | var i = ReadString(b); 161 | b = ReadByte(); 162 | var j = ReadString(b); 163 | return $"{i}@{j}"; 164 | } 165 | else if (tag == (int)ReadStringTag.NIBBLE_8 || tag == (int)ReadStringTag.HEX_8) 166 | { 167 | return ReadPacked8(tag); 168 | } 169 | LogUtil.Warn("invalid string with tag" + tag); 170 | return ""; 171 | } 172 | public string ReadPacked8(int tag) 173 | { 174 | var startByte = ReadByte(); 175 | var ret = string.Empty; 176 | for (int i = 0; i < (startByte & 127); i++) 177 | { 178 | var currByte = ReadByte(); 179 | var lower = UnpackByte(tag, currByte & 0xF0 >> 4); 180 | var upper = UnpackByte(tag, currByte & 0x0F); 181 | ret += lower + upper; 182 | } 183 | if (startByte >> 7 != 0) 184 | { 185 | ret = ret.Substring(0, ret.Length - 1); 186 | } 187 | return ret; 188 | } 189 | public string UnpackByte(int tag, int value) 190 | { 191 | switch (tag) 192 | { 193 | case (int)ReadStringTag.NIBBLE_8: 194 | return UnpackNibble(value); 195 | case (int)ReadStringTag.HEX_8: 196 | return UnpackHex(value); 197 | } 198 | LogUtil.Warn("UnpackByte Fail"); 199 | return ""; 200 | } 201 | public string UnpackNibble(int value) 202 | { 203 | if (value < 0 || value > 15) 204 | { 205 | LogUtil.Warn("unpackNibble with value" + value); 206 | return ""; 207 | } 208 | else if (value == 10) 209 | { 210 | return "-"; 211 | } 212 | else if (value == 11) 213 | { 214 | return ","; 215 | } 216 | else if (value == 15) 217 | { 218 | return "\x00"; 219 | } 220 | else 221 | { 222 | return value.ToString(); 223 | } 224 | } 225 | public string UnpackHex(int value) 226 | { 227 | if (value < 0 || value > 15) 228 | { 229 | LogUtil.Warn("unpackHex with value" + value); 230 | return ""; 231 | } 232 | else if (value < 10) 233 | { 234 | return value.ToString(); 235 | } 236 | else 237 | { 238 | return ((char)('A' + 14 - 10)).ToString(); 239 | } 240 | } 241 | public Node ReadNode() 242 | { 243 | var ret = new Node(); 244 | var size = ReadInt8(); 245 | var listSize = ReadListSize(size); 246 | var descrTag = ReadInt8(); 247 | if (descrTag == (int)ReadStringTag.STREAM_END) 248 | { 249 | LogUtil.Warn("unexpected stream end"); 250 | return null; 251 | } 252 | ret.Description = ReadString(descrTag); 253 | if (listSize == 0 || string.IsNullOrWhiteSpace(ret.Description)) 254 | { 255 | LogUtil.Warn("invalid Node"); 256 | return null; 257 | } 258 | ret.Attributes = ReadAttributes((listSize - 1) >> 1); 259 | if (listSize % 2 == 1) 260 | { 261 | return ret; 262 | } 263 | var tag = ReadInt8(); 264 | ret.Content=ReadContent(tag); 265 | return ret; 266 | } 267 | public List ReadList(int tag) 268 | { 269 | var size = ReadListSize(tag); 270 | var ret = new List(); 271 | for (int i = 0; i < size; i++) 272 | { 273 | ret.Add(ReadNode()); 274 | } 275 | return ret; 276 | } 277 | public object ReadContent(int tag) 278 | { 279 | switch ((ReadStringTag)tag) 280 | { 281 | case ReadStringTag.LIST_EMPTY: 282 | case ReadStringTag.LIST_8: 283 | case ReadStringTag.LIST_16: 284 | return ReadList(tag); 285 | case ReadStringTag.BINARY_8: 286 | { 287 | var size = ReadInt8(); 288 | return ReadBytes(size); 289 | } 290 | case ReadStringTag.BINARY_20: 291 | { 292 | var size = ReadInt20(); 293 | return ReadBytes(size); 294 | } 295 | case ReadStringTag.BINARY_32: 296 | { 297 | var size = ReadInt32(); 298 | return ReadBytes(size); 299 | } 300 | default: 301 | return ReadString(tag); 302 | } 303 | } 304 | public enum ReadStringTag 305 | { 306 | LIST_EMPTY = 0, 307 | STREAM_END = 2, 308 | DICTIONARY_0 = 236, 309 | DICTIONARY_1 = 237, 310 | DICTIONARY_2 = 238, 311 | DICTIONARY_3 = 239, 312 | LIST_8 = 248, 313 | LIST_16 = 249, 314 | JID_PAIR = 250, 315 | HEX_8 = 251, 316 | BINARY_8 = 252, 317 | BINARY_20 = 253, 318 | BINARY_32 = 254, 319 | NIBBLE_8 = 255, 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /WhatsAppLib/Serialization/BinaryEncoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using WhatsAppLib.Models; 5 | using WhatsAppLib.Utils; 6 | using static WhatsAppLib.Serialization.BinaryDecoder; 7 | 8 | namespace WhatsAppLib.Serialization 9 | { 10 | internal class BinaryEncoder 11 | { 12 | private List _data = new List(); 13 | public byte[] WriteNode(Node node) 14 | { 15 | var numAttributes = 0; 16 | 17 | if (node.Attributes != null && node.Attributes.Count > 0) 18 | { 19 | numAttributes = node.Attributes.Count; 20 | } 21 | var hasContent = 0; 22 | if (node.Content != null) 23 | { 24 | hasContent = 1; 25 | } 26 | WriteListStart(2 * numAttributes + 1 + hasContent); 27 | WriteString(node.Description); 28 | WriteAttributes(node.Attributes); 29 | WriteChildren(node.Content); 30 | return _data.ToArray(); 31 | } 32 | private void WriteChildren(object obj) 33 | { 34 | switch (obj) 35 | { 36 | case string str: 37 | WriteString(str,true); 38 | break; 39 | case byte[] bs: 40 | WriteByteLength(bs.Length); 41 | PushBytes(bs); 42 | break; 43 | case List ns: 44 | WriteListStart(ns.Count); 45 | foreach (var item in ns) 46 | { 47 | WriteNode(item); 48 | } 49 | break; 50 | default: 51 | LogUtil.Warn("cannot write child of type:"+obj); 52 | break; 53 | } 54 | } 55 | private void WriteAttributes(Dictionary attributes) 56 | { 57 | if (attributes == null) 58 | { 59 | return; 60 | } 61 | foreach (var item in attributes) 62 | { 63 | if (string.IsNullOrWhiteSpace(item.Value)) 64 | { 65 | continue; 66 | } 67 | WriteString(item.Key); 68 | WriteString(item.Value); 69 | } 70 | } 71 | private void WriteString(string str, bool i=false) 72 | { 73 | if (!i && str == "c.us") 74 | { 75 | WriteToken(SingleByteTokens.IndexOf("s.whatsapp.net")); 76 | return; 77 | } 78 | var tokenIndex = SingleByteTokens.IndexOf(str); 79 | if (tokenIndex == -1) 80 | { 81 | var jidSepIndex = str.IndexOf("@"); 82 | if (jidSepIndex < 1) 83 | { 84 | WriteStringRaw(str); 85 | } 86 | else 87 | { 88 | WriteJid(str.Substring(0, jidSepIndex), str.Substring(jidSepIndex + 1)); 89 | } 90 | } 91 | else 92 | { 93 | if (tokenIndex < 256) 94 | { 95 | WriteToken(tokenIndex); 96 | } 97 | else 98 | { 99 | var singleByteOverflow = tokenIndex - 256; 100 | var dictionaryIndex = singleByteOverflow >> 8; 101 | if (dictionaryIndex < 0 || dictionaryIndex > 3) 102 | { 103 | LogUtil.Warn("double byte dictionary token out of range:"+str); 104 | return; 105 | } 106 | WriteToken((int)ReadStringTag.DICTIONARY_0 + dictionaryIndex); 107 | WriteToken(singleByteOverflow % 256); 108 | } 109 | } 110 | } 111 | private void WriteJid(string jidLeft, string jidRight) 112 | { 113 | PushByte((byte)ReadStringTag.JID_PAIR); 114 | if (!string.IsNullOrWhiteSpace(jidLeft)) 115 | { 116 | WritePackedBytes(jidLeft); 117 | } 118 | else 119 | { 120 | WriteToken((int)ReadStringTag.LIST_EMPTY); 121 | } 122 | WriteString(jidRight); 123 | } 124 | private void WritePackedBytes(string str) 125 | { 126 | if (!WritePackedBytesImpl(str, (int)ReadStringTag.NIBBLE_8)) 127 | { 128 | if (!WritePackedBytesImpl(str, (int)ReadStringTag.HEX_8)) 129 | { 130 | LogUtil.Warn("WritePackedBytes fail"); 131 | } 132 | } 133 | } 134 | private bool WritePackedBytesImpl(string str, int dataType) 135 | { 136 | var numBytes = str.Length; 137 | if (numBytes > 254) 138 | { 139 | LogUtil.Warn("too many bytes to pack:" + numBytes); 140 | return false; 141 | } 142 | PushByte(dataType); 143 | int x = 0; 144 | if (numBytes % 2 != 0) 145 | { 146 | x = 128; 147 | } 148 | PushByte(x | (int)(Math.Ceiling(numBytes / 2.0))); 149 | for (int i = 0; i < numBytes / 2; i++) 150 | { 151 | var b = PackBytePair(dataType, str.Substring(2 * i, 1), str.Substring(2 * i + 1, 1)); 152 | if (b < 0) 153 | { 154 | return false; 155 | } 156 | PushByte(b); 157 | } 158 | if (numBytes % 2 != 0) 159 | { 160 | var b = PackBytePair(dataType, str.Substring(numBytes - 1), "\x00"); 161 | if (b < 0) 162 | { 163 | return false; 164 | } 165 | PushByte(b); 166 | } 167 | return true; 168 | } 169 | private int PackBytePair(int packType, string part1, string part2) 170 | { 171 | if (packType == (int)ReadStringTag.NIBBLE_8) 172 | { 173 | var n1 = PackNibble(part1); 174 | if (n1 < 0) 175 | { 176 | return -1; 177 | } 178 | var n2 = PackNibble(part2); 179 | if (n2 < 0) 180 | { 181 | return -1; 182 | } 183 | return (n1 << 4) | n2; 184 | } 185 | else if (packType == (int)ReadStringTag.HEX_8) 186 | { 187 | var n1 = PackHex(part1); 188 | if (n1 < 0) 189 | { 190 | return -1; 191 | } 192 | var n2 = PackHex(part2); 193 | if (n2 < 0) 194 | { 195 | return -1; 196 | } 197 | return (n1 << 4) | n2; 198 | } 199 | else 200 | { 201 | LogUtil.Warn($"invalid pack type {packType} for byte pair:{part1} / {part2}"); 202 | return -1; 203 | } 204 | } 205 | private int PackNibble(string str) 206 | { 207 | if (str.Length > 1) 208 | { 209 | LogUtil.Warn("PackNibble str length:" + str.Length); 210 | return -1; 211 | } 212 | else if (str[0] >= '0' && str[0] <= '9') 213 | { 214 | return Convert.ToInt32(str); 215 | } 216 | else if (str == "-") 217 | { 218 | return 10; 219 | } 220 | else if (str == ".") 221 | { 222 | return 11; 223 | } 224 | else if (str == "\x00") 225 | { 226 | return 15; 227 | } 228 | LogUtil.Warn("invalid string to pack as nibble:" + str); 229 | return -1; 230 | } 231 | private int PackHex(string str) 232 | { 233 | if (str.Length > 1) 234 | { 235 | LogUtil.Warn("PackHex str length:" + str.Length); 236 | return -1; 237 | } 238 | var value = str[0]; 239 | if ((value >= '0' && value <= '9') || (value >= 'A' && value <= 'F') || (value >= 'a' && value <= 'f')) 240 | { 241 | var d = Convert.ToInt32(str, 16); 242 | return d; 243 | } 244 | else if (str == "\x00") 245 | { 246 | return 15; 247 | } 248 | LogUtil.Warn("invalid string to pack as hex: "+ str); 249 | return -1; 250 | } 251 | private void WriteStringRaw(string str) 252 | { 253 | WriteByteLength(str.Length); 254 | PushString(str); 255 | } 256 | private void WriteByteLength(int length) 257 | { 258 | if (length > int.MaxValue) 259 | { 260 | LogUtil.Error("length is too large:" + length); 261 | } 262 | else if (length >= (1 << 20)) 263 | { 264 | PushByte((byte)ReadStringTag.BINARY_32); 265 | PushInt32(length); 266 | } 267 | else if (length >=256) 268 | { 269 | PushByte((byte)ReadStringTag.BINARY_20); 270 | PushInt20(length); 271 | } 272 | else 273 | { 274 | PushByte((byte)ReadStringTag.BINARY_8); 275 | PushInt8(length); 276 | } 277 | } 278 | private void WriteToken(int token) 279 | { 280 | if (token < SingleByteTokens.Count) 281 | { 282 | PushByte((byte)token); 283 | } 284 | else if (token <= 500) 285 | { 286 | LogUtil.Error("invalid token: " + token); 287 | } 288 | } 289 | private void WriteListStart(int listSize) 290 | { 291 | if (listSize == 0) 292 | { 293 | PushByte((byte)ReadStringTag.LIST_EMPTY); 294 | } 295 | else if (listSize < 256) 296 | { 297 | PushByte((byte)ReadStringTag.LIST_8); 298 | PushInt8(listSize); 299 | } 300 | else 301 | { 302 | PushByte((byte)ReadStringTag.LIST_16); 303 | PushInt16(listSize); 304 | } 305 | } 306 | private void PushString(string str) 307 | { 308 | PushBytes(Encoding.UTF8.GetBytes(str)); 309 | } 310 | private void PushIntN(int value, int n, bool littleEndian = false) 311 | { 312 | for (int i = 0; i < n; i++) 313 | { 314 | int curShift; 315 | if (littleEndian) 316 | { 317 | curShift = i; 318 | } 319 | else 320 | { 321 | curShift = n - i - 1; 322 | } 323 | PushByte((byte)((value >> (curShift * 8)) & 0xFF)); 324 | } 325 | } 326 | private void PushInt8(int value) 327 | { 328 | PushIntN(value, 1); 329 | } 330 | private void PushInt16(int value) 331 | { 332 | PushIntN(value, 2); 333 | } 334 | private void PushInt20(int value) 335 | { 336 | PushIntN(value, 3); 337 | } 338 | private void PushInt32(int value) 339 | { 340 | PushIntN(value, 4); 341 | } 342 | private void PushByte(byte b) 343 | { 344 | _data.Add(b); 345 | } 346 | private void PushByte(int b) 347 | { 348 | _data.Add((byte)b); 349 | } 350 | private void PushBytes(IEnumerable bs) 351 | { 352 | _data.AddRange(bs); 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /WhatsAppLib/Utils/Extend.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Security.Cryptography; 8 | using System.Text; 9 | using System.Text.RegularExpressions; 10 | using System.Threading.Tasks; 11 | using Yove.Proxy; 12 | 13 | namespace WhatsAppLib.Utils 14 | { 15 | internal static class Extend 16 | { 17 | /// 18 | /// 正则获取匹配的字符串 Regularly get the matched string 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | public static string RegexGetString(this string str, string pattern, int retuenIndex = 1) 25 | { 26 | Regex r = new Regex(pattern, RegexOptions.None); 27 | return r.Match(str).Groups[retuenIndex].Value; 28 | } 29 | 30 | public static string UrlEncode(this string str) 31 | { 32 | return System.Web.HttpUtility.UrlEncode(str); 33 | } 34 | /// 35 | /// 获取时间戳 36 | /// 37 | /// 38 | /// 39 | public static long GetTimeStampLong(this DateTime dateTime) 40 | { 41 | return (dateTime.ToUniversalTime().Ticks - 621355968000000000) / 10000; 42 | } 43 | /// 44 | /// 获取时间戳 45 | /// 46 | /// 47 | /// 48 | public static long GetTimeStampInt(this DateTime dateTime) 49 | { 50 | return GetTimeStampLong(dateTime) / 1000; 51 | } 52 | /// 53 | /// 时间戳转时间 54 | /// 55 | /// 56 | /// 57 | public static DateTime GetDateTime(this string timeStamp) 58 | { 59 | if (string.IsNullOrWhiteSpace(timeStamp)) 60 | { 61 | return DateTime.MinValue; 62 | } 63 | var num = long.Parse(timeStamp); 64 | DateTime dtStart = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1), TimeZoneInfo.Local); 65 | if (num > 9466560000) 66 | { 67 | TimeSpan toNow = new TimeSpan(num * 10000); 68 | return dtStart.Add(toNow); 69 | } 70 | else 71 | { 72 | TimeSpan toNow = new TimeSpan(num * 1000 * 10000); 73 | return dtStart.Add(toNow); 74 | } 75 | } 76 | 77 | public static bool IsNullOrWhiteSpace(this string str) 78 | { 79 | return string.IsNullOrWhiteSpace(str); 80 | } 81 | /// 82 | /// 使用HMACSHA256进行加密 83 | /// 84 | /// 85 | /// 86 | /// 87 | public static byte[] HMACSHA256_Encrypt(this byte[] bs, byte[] key) 88 | { 89 | using (HMACSHA256 hmac = new HMACSHA256(key)) 90 | { 91 | byte[] computedHash = hmac.ComputeHash(bs); 92 | return computedHash; 93 | } 94 | } 95 | /// 96 | /// SHA256加密 97 | /// 98 | /// 99 | /// 100 | public static byte[] SHA256_Encrypt(this byte[] bs) 101 | { 102 | HashAlgorithm iSha = new SHA256CryptoServiceProvider(); 103 | return iSha.ComputeHash(bs); 104 | } 105 | /// 106 | /// 判断是否相同 107 | /// 108 | /// 109 | /// 110 | /// 111 | public static bool ValueEquals(this byte[] bs, byte[] bs2) 112 | { 113 | if (bs.Length != bs.Length) 114 | { 115 | return false; 116 | } 117 | for (int i = 0; i < bs.Length; i++) 118 | { 119 | if (bs[i] != bs2[i]) 120 | { 121 | return false; 122 | } 123 | } 124 | return true; 125 | } 126 | /// 127 | /// 字节转字节字符串 128 | /// 129 | /// 130 | /// 131 | public static string ToHexString(this byte[] bytes) 132 | { 133 | string hexString = string.Empty; 134 | if (bytes != null) 135 | { 136 | StringBuilder strB = new StringBuilder(); 137 | for (int i = 0; i < bytes.Length; i++) 138 | { 139 | strB.Append(bytes[i].ToString("X2")); 140 | } 141 | hexString = strB.ToString(); 142 | } 143 | return hexString; 144 | } 145 | public static byte[] AesCbcDecrypt(this byte[] data, byte[] key, byte[] iv) 146 | { 147 | var rijndaelCipher = new RijndaelManaged 148 | { 149 | Mode = CipherMode.CBC, 150 | Padding = PaddingMode.PKCS7, 151 | KeySize = key.Length * 8, 152 | BlockSize = iv.Length * 8 153 | }; 154 | rijndaelCipher.Key = key; 155 | rijndaelCipher.IV = iv; 156 | var transform = rijndaelCipher.CreateDecryptor(); 157 | var plainText = transform.TransformFinalBlock(data, 0, data.Length); 158 | return plainText; 159 | } 160 | public static byte[] AesCbcEncrypt(this byte[] data, byte[] key, byte[] iv) 161 | { 162 | var rijndaelCipher = new RijndaelManaged 163 | { 164 | Mode = CipherMode.CBC, 165 | Padding = PaddingMode.PKCS7, 166 | KeySize = key.Length * 8, 167 | BlockSize = iv.Length * 8 168 | }; 169 | rijndaelCipher.Key = key; 170 | rijndaelCipher.IV = iv; 171 | var transform = rijndaelCipher.CreateEncryptor(); 172 | var plainText = transform.TransformFinalBlock(data, 0, data.Length); 173 | return plainText; 174 | } 175 | public static byte[] AesCbcDecrypt(this byte[] data, byte[] key) 176 | { 177 | return AesCbcDecrypt(data.Skip(16).ToArray(), key, data.Take(16).ToArray()); 178 | } 179 | 180 | public static async Task GetStream(this string url, ProxyClient webProxy =null) 181 | { 182 | MemoryStream memory = new MemoryStream(); 183 | HttpClientHandler Handler = new HttpClientHandler { Proxy = webProxy }; 184 | using (var client = new HttpClient(Handler)) 185 | { 186 | var message = new HttpRequestMessage(HttpMethod.Get, url) 187 | { 188 | Version = HttpVersion.Version20, 189 | }; 190 | message.Headers.Add("user-agent", "Mozilla/5.0 (MSIE 10.0; Windows NT 6.1; Trident/5.0)"); 191 | var response = await client.SendAsync(message); 192 | if (response.IsSuccessStatusCode) 193 | { 194 | await response.Content.CopyToAsync(memory); 195 | } 196 | } 197 | return memory; 198 | } 199 | public static async Task Post(this string url,byte[] data, ProxyClient webProxy = null,Dictionary head=null) 200 | { 201 | MemoryStream memory = new MemoryStream(); 202 | HttpClientHandler Handler = new HttpClientHandler { Proxy = webProxy }; 203 | using (var client = new HttpClient(Handler)) 204 | { 205 | var message = new HttpRequestMessage(HttpMethod.Post, url) 206 | { 207 | Version = HttpVersion.Version20, 208 | }; 209 | message.Content = new ByteArrayContent(data); 210 | message.Headers.Add("user-agent", "Mozilla/5.0 (MSIE 10.0; Windows NT 6.1; Trident/5.0)"); 211 | if (head != null) 212 | { 213 | foreach (var item in head) 214 | { 215 | message.Headers.Add(item.Key, item.Value); 216 | } 217 | } 218 | var response = await client.SendAsync(message); 219 | if (response.IsSuccessStatusCode) 220 | { 221 | await response.Content.CopyToAsync(memory); 222 | } 223 | } 224 | return memory; 225 | } 226 | public static async Task PostHtml(this string url, byte[] data, ProxyClient webProxy = null, Dictionary head = null,Encoding encoding=null) 227 | { 228 | var memory =await Post(url,data,webProxy,head); 229 | if (encoding == null) 230 | { 231 | return Encoding.UTF8.GetString(memory.ToArray()); 232 | } 233 | else 234 | { 235 | return encoding.GetString(memory.ToArray()); 236 | } 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /WhatsAppLib/Utils/LogUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using WhatsAppLib.Logger; 5 | 6 | namespace WhatsAppLib.Utils 7 | { 8 | public class LogUtil 9 | { 10 | public static ILogger Logger = new ConsoleLogger(); 11 | public static void Trace(string log) 12 | { 13 | Logger.Trace(log); 14 | } 15 | public static void Debug(string log) 16 | { 17 | Logger.Debug(log); 18 | } 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static void Info(string log) 24 | { 25 | Logger.Info(log); 26 | } 27 | public static void Warn(string log) 28 | { 29 | Logger.Warn(log); 30 | } 31 | public static void Error(string log) 32 | { 33 | Logger.Error(log); 34 | } 35 | 36 | public static void Fatal(string log) 37 | { 38 | Logger.Fatal(log); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /WhatsAppLib/WhatsApp.cs: -------------------------------------------------------------------------------- 1 | using AronParker.Hkdf; 2 | using Elliptic; 3 | using Google.Protobuf; 4 | using Newtonsoft.Json; 5 | using Proto; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Net; 11 | using System.Net.WebSockets; 12 | using System.Security.Cryptography; 13 | using System.Text; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | using WhatsAppLib.Messages; 17 | using WhatsAppLib.Models; 18 | using WhatsAppLib.Serialization; 19 | using WhatsAppLib.Utils; 20 | using Yove.Proxy; 21 | using ImageMessage = Proto.ImageMessage; 22 | 23 | namespace WhatsAppLib 24 | { 25 | public class WhatsApp : IDisposable 26 | { 27 | #region 公有事件 Event 28 | /// 29 | /// 需要扫码登录 Scan code to log in 30 | /// 31 | public event Action LoginScanCodeEvent; 32 | /// 33 | /// 登录成功后返回Session信息 Return Session information after successful login 34 | /// 35 | public event Action LoginSuccessEvent; 36 | /// 37 | /// 接收其他未处理的消息 Receive Remaining messages 38 | /// 39 | public event Action ReceiveRemainingMessagesEvent; 40 | /// 41 | /// 接受到的文字消息 Receive Text messages 42 | /// 43 | public event Action ReceiveTextMessageEvent; 44 | /// 45 | /// 接受到的图片消息 Receive Image messages 46 | /// 47 | public event Action ReceiveImageMessageEvent; 48 | /// 49 | /// 掉线事件 50 | /// 51 | public event Action AccountDroppedEvent; 52 | #endregion 53 | #region 公有属性 public property 54 | /// 55 | /// Http代理 Http Proxy 56 | /// 57 | public ProxyClient WebProxy 58 | { 59 | set 60 | { 61 | _webSocket.Options.Proxy = value; 62 | _webProxy = value; 63 | } 64 | get => _webProxy; 65 | } 66 | /// 67 | /// 登录Session Login Session 68 | /// 69 | public SessionInfo Session { set; get; } 70 | #endregion 71 | #region 私有成员 private member 72 | private ClientWebSocket _webSocket; 73 | private ProxyClient _webProxy; 74 | //private object _sendObj = new object(); 75 | private int _msgCount; 76 | private bool _loginSuccess; 77 | private object _snapReceiveLock = new object(); 78 | private Dictionary> _snapReceiveDictionary = new Dictionary>(); 79 | private Dictionary _snapReceiveRemoveCountDictionary = new Dictionary(); 80 | private const string MediaImage = "WhatsApp Image Keys"; 81 | private const string MediaVideo = "WhatsApp Video Keys"; 82 | private const string MediaAudio = "WhatsApp Audio Keys"; 83 | private const string MediaDocument = "WhatsApp Document Keys"; 84 | private static Dictionary MediaTypeMap = new Dictionary{ 85 | { MediaImage,"/mms/image" }, 86 | { MediaVideo,"/mms/video" }, 87 | { MediaAudio,"/mms/document" }, 88 | { MediaDocument,"/mms/audio" }, 89 | }; 90 | #endregion 91 | #region 构造函数 Constructor 92 | static WhatsApp() 93 | { 94 | ServicePointManager.DefaultConnectionLimit = int.MaxValue; 95 | } 96 | public WhatsApp() 97 | { 98 | _webSocket = new ClientWebSocket(); 99 | _webSocket.Options.SetRequestHeader("Origin", "https://web.whatsapp.com"); 100 | } 101 | public WhatsApp(ProxyClient webProxy) : this() 102 | { 103 | _webProxy = webProxy; 104 | _webSocket.Options.Proxy = webProxy; 105 | } 106 | public WhatsApp(SessionInfo session) : this() 107 | { 108 | Session = session; 109 | } 110 | public WhatsApp(SessionInfo session, ProxyClient webProxy) : this(webProxy) 111 | { 112 | Session = session; 113 | } 114 | #endregion 115 | #region 公有方法 public method 116 | public async Task Connect() 117 | { 118 | if (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.Connecting) 119 | { 120 | await _webSocket.CloseAsync(WebSocketCloseStatus.Empty, "Close", CancellationToken.None); 121 | } 122 | await _webSocket.ConnectAsync(new Uri("wss://web.whatsapp.com/ws"), CancellationToken.None); 123 | Receive(ReceiveModel.GetReceiveModel()); 124 | Send("?,,"); 125 | return true; 126 | } 127 | /// 128 | /// 登录,需要监听LoginScanCodeEvent,和LoginSuccessEvent事件 To log in, you need to monitor the LoginScanCodeEvent and LoginSuccessEvent events 129 | /// 130 | public async void Login() 131 | { 132 | _snapReceiveDictionary.Clear(); 133 | if(! await Connect()) 134 | { 135 | throw new Exception("Connect Error"); 136 | } 137 | if (Session == null) 138 | { 139 | Session = new SessionInfo(); 140 | } 141 | if (string.IsNullOrEmpty(Session.ClientToken)) 142 | { 143 | WhatsAppLogin(); 144 | } 145 | else 146 | { 147 | ReLogin(); 148 | } 149 | } 150 | /// 151 | /// 发送图片消息 152 | /// 153 | /// 发送给谁 154 | /// 图片字节码 155 | /// 消息名称 156 | /// 157 | public async Task SendImage(string remoteJid, byte[] data, string caption = null, Action act = null) 158 | { 159 | var uploadResponse = await Upload(data, MediaImage); 160 | if (uploadResponse == null) 161 | { 162 | return null; 163 | } 164 | return SendProto(new WebMessageInfo() 165 | { 166 | Key = new MessageKey 167 | { 168 | RemoteJid = remoteJid 169 | }, 170 | Message = new Message 171 | { 172 | ImageMessage = new ImageMessage 173 | { 174 | Url = uploadResponse.DownloadUrl, 175 | Caption = caption, 176 | Mimetype = "image/jpeg", 177 | MediaKey = ByteString.CopyFrom(uploadResponse.MediaKey), 178 | FileEncSha256 = ByteString.CopyFrom(uploadResponse.FileEncSha256), 179 | FileSha256 = ByteString.CopyFrom(uploadResponse.FileSha256), 180 | FileLength = uploadResponse.FileLength 181 | } 182 | } 183 | }, act); 184 | } 185 | public string SendText(string remoteJid, string text, Action act = null) 186 | { 187 | return SendProto(new WebMessageInfo() 188 | { 189 | Key = new MessageKey 190 | { 191 | RemoteJid = remoteJid 192 | }, 193 | Message = new Message 194 | { 195 | Conversation = text 196 | } 197 | }, act); 198 | } 199 | public string LoadMediaInfo(string jid, string messageId, string owner, Action act = null) 200 | { 201 | return SendQuery("media", jid, messageId, "", owner, "", 0, 0, act); 202 | } 203 | public string CreateGroup(string subject, string[] participants, Action act = null) 204 | { 205 | return SendGroup("create", "", subject, participants, act); 206 | } 207 | public string GetFullChatHistory(string jid, int count = 300) 208 | { 209 | if (string.IsNullOrWhiteSpace(jid)) 210 | { 211 | return string.Empty; 212 | } 213 | var beforeMsg = ""; 214 | var beforeMsgIsOwner = true; 215 | //while (true) 216 | { 217 | SendQuery("message", jid, beforeMsg, "before", beforeMsgIsOwner ? "true" : "false", "", count, 0, async rm => 218 | { 219 | var node = await GetDecryptNode(rm); 220 | if (node != null) 221 | { 222 | if (node.Content is List nodeList) 223 | { 224 | foreach (var item in nodeList) 225 | { 226 | if (item.Description == "message") 227 | { 228 | var messageData = item.Content as byte[]; 229 | var ms = WebMessageInfo.Parser.ParseFrom(messageData); 230 | if (ms.Message != null) 231 | { 232 | } 233 | } 234 | } 235 | } 236 | } 237 | }, 2); 238 | } 239 | return string.Empty; 240 | } 241 | public void Dispose() 242 | { 243 | _webSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); 244 | } 245 | #endregion 246 | #region 私有方法 private method 247 | private async Task DownloadImage(string url, byte[] mediaKey) 248 | { 249 | return await Download(url, mediaKey, MediaImage); 250 | } 251 | private async Task Download(string url, byte[] mediaKey, string info) 252 | { 253 | var memory = await url.GetStream(WebProxy); 254 | var mk = GetMediaKeys(mediaKey, info); 255 | var data = memory.ToArray(); 256 | var file = data.Take(data.Length - 10).ToArray(); 257 | var mac = data.Skip(file.Length).ToArray(); 258 | var sign = (mk.Iv.Concat(file).ToArray()).HMACSHA256_Encrypt(mk.MacKey); 259 | if (!sign.Take(10).ToArray().ValueEquals(mac)) 260 | { 261 | LogUtil.Error("invalid media hmac"); 262 | return null; 263 | } 264 | var fileData = file.AesCbcDecrypt(mk.CipherKey, mk.Iv); 265 | return fileData; 266 | 267 | } 268 | private MediaKeys GetMediaKeys(byte[] mediaKey, string info) 269 | { 270 | var sharedSecretExtract = new Hkdf(HashAlgorithmName.SHA256).Extract(mediaKey); 271 | var sharedSecretExpand = new Hkdf(HashAlgorithmName.SHA256).Expand(sharedSecretExtract, 112, Encoding.UTF8.GetBytes(info)); 272 | return new MediaKeys(sharedSecretExpand); 273 | } 274 | private async Task Upload(byte[] data, string info) 275 | { 276 | return await await Task.Factory.StartNew(async () => 277 | { 278 | var uploadResponse = new UploadResponse(); 279 | uploadResponse.FileLength = (ulong)data.Length; 280 | uploadResponse.MediaKey = GetRandom(32); 281 | var mk = GetMediaKeys(uploadResponse.MediaKey, MediaImage); 282 | var enc = data.AesCbcEncrypt(mk.CipherKey, mk.Iv); 283 | var mac = (mk.Iv.Concat(enc).ToArray()).HMACSHA256_Encrypt(mk.MacKey).Take(10); 284 | uploadResponse.FileSha256 = data.SHA256_Encrypt(); 285 | var joinData = enc.Concat(mac).ToArray(); 286 | uploadResponse.FileEncSha256 = joinData.SHA256_Encrypt(); 287 | var mediaConnResponse = await QueryMediaConn(); 288 | if (mediaConnResponse == null) 289 | { 290 | return null; 291 | } 292 | var token = Convert.ToBase64String(uploadResponse.FileEncSha256).Replace("+", "-").Replace("/", "_"); 293 | var url = $"https://{mediaConnResponse.MediaConn.Hosts[0].Hostname}{MediaTypeMap[info]}/{token}?auth={mediaConnResponse.MediaConn.Auth}&token={token}"; 294 | var response = await url.PostHtml(joinData, WebProxy, new Dictionary { 295 | { "Origin","https://web.whatsapp.com" }, 296 | { "Referer","https://web.whatsapp.com/"} 297 | }); 298 | uploadResponse.DownloadUrl = response.RegexGetString("url\":\"([^\"]*)\""); 299 | return uploadResponse; 300 | }).ConfigureAwait(false); 301 | 302 | } 303 | private async Task QueryMediaConn() 304 | { 305 | MediaConnResponse connResponse = null; 306 | SendJson("[\"query\",\"mediaConn\"]", rm => connResponse = JsonConvert.DeserializeObject(rm.Body)); 307 | await await Task.Factory.StartNew(async () => 308 | { 309 | for (int i = 0; i < 100; i++) 310 | { 311 | if (connResponse != null) 312 | { 313 | return; 314 | } 315 | await Task.Delay(100); 316 | } 317 | }).ConfigureAwait(false); 318 | return connResponse; 319 | } 320 | private void AddCallback(string tag, Action act, int count = 0) 321 | { 322 | if (act != null) 323 | { 324 | AddSnapReceive(tag, rm => 325 | { 326 | act(rm); 327 | return true; 328 | }, count); 329 | } 330 | } 331 | private string SendProto(WebMessageInfo webMessage, Action act = null) 332 | { 333 | if (webMessage.Key.Id.IsNullOrWhiteSpace()) 334 | { 335 | webMessage.Key.Id = GetRandom(10).ToHexString().ToUpper(); 336 | } 337 | if (webMessage.MessageTimestamp == 0) 338 | { 339 | webMessage.MessageTimestamp = (ulong)DateTime.Now.GetTimeStampInt(); 340 | } 341 | webMessage.Key.FromMe = true; 342 | webMessage.Status = WebMessageInfo.Types.WEB_MESSAGE_INFO_STATUS.Error; 343 | var n = new Node 344 | { 345 | Description = "action", 346 | Attributes = new Dictionary { 347 | { "type", "relay" }, 348 | {"epoch",( Interlocked.Increment(ref _msgCount) - 1).ToString() }//"5" }// 349 | }, 350 | Content = new List { webMessage } 351 | }; 352 | AddCallback(webMessage.Key.Id, act); 353 | SendBinary(n, WriteBinaryType.Message, webMessage.Key.Id); 354 | return webMessage.Key.Id; 355 | } 356 | private void SendBinary(Node node, WriteBinaryType binaryType, string messageTag) 357 | { 358 | var data = EncryptBinaryMessage(node); 359 | var bs = new List(Encoding.UTF8.GetBytes($"{messageTag},")); 360 | bs.Add((byte)binaryType); 361 | bs.Add(128); 362 | bs.AddRange(data); 363 | _webSocket.SendAsync(new ArraySegment(bs.ToArray()), WebSocketMessageType.Binary, true, CancellationToken.None); 364 | } 365 | private string SendQuery(string t, string jid, string messageId, string kind, string owner, string search, int count, int page, Action act = null, int removeCount = 0) 366 | { 367 | var msgCount = Interlocked.Increment(ref _msgCount) - 1; 368 | var tag = $"{DateTime.Now.GetTimeStampInt()}.--{msgCount}"; 369 | AddCallback(tag, act, removeCount); 370 | var n = new Node 371 | { 372 | Description = "query", 373 | Attributes = new Dictionary { 374 | { "type", t }, 375 | {"epoch",msgCount.ToString() }//"5" }// 376 | }, 377 | }; 378 | if (!jid.IsNullOrWhiteSpace()) 379 | { 380 | n.Attributes.Add("jid", jid); 381 | } 382 | if (!messageId.IsNullOrWhiteSpace()) 383 | { 384 | n.Attributes.Add("index", messageId); 385 | } 386 | if (!kind.IsNullOrWhiteSpace()) 387 | { 388 | n.Attributes.Add("kind", kind); 389 | } 390 | if (!owner.IsNullOrWhiteSpace()) 391 | { 392 | n.Attributes.Add("owner", owner); 393 | } 394 | if (!search.IsNullOrWhiteSpace()) 395 | { 396 | n.Attributes.Add("search", search); 397 | } 398 | if (count > 0) 399 | { 400 | n.Attributes.Add("count", count.ToString()); 401 | } 402 | if (page > 0) 403 | { 404 | n.Attributes.Add("page", page.ToString()); 405 | } 406 | var msgType = WriteBinaryType.Group; 407 | if (t == "media") 408 | { 409 | msgType = WriteBinaryType.QueryMedia; 410 | } 411 | SendBinary(n, msgType, tag); 412 | return tag; 413 | } 414 | private string SendGroup(string t, string jid, string subject, string[] participants, Action act = null) 415 | { 416 | var msgCount = Interlocked.Increment(ref _msgCount) - 1; 417 | var tag = $"{DateTime.Now.GetTimeStampInt()}.--{msgCount}"; 418 | AddCallback(tag, act); 419 | var g = new Node 420 | { 421 | Description = "group", 422 | Attributes = new Dictionary { 423 | { "author", Session.Wid }, 424 | { "id", tag }, 425 | { "type", t } 426 | } 427 | }; 428 | if (participants != null && participants.Length > 0) 429 | { 430 | var ns = new List(); 431 | foreach (var participant in participants) 432 | { 433 | ns.Add(new Node 434 | { 435 | Description = "participant", 436 | Attributes = new Dictionary { { "jid", participant } } 437 | }); 438 | } 439 | g.Content = ns; 440 | } 441 | if (!jid.IsNullOrWhiteSpace()) 442 | { 443 | g.Attributes.Add("jid", jid); 444 | } 445 | 446 | if (!subject.IsNullOrWhiteSpace()) 447 | { 448 | g.Attributes.Add("subject", subject); 449 | } 450 | SendBinary(new Node 451 | { 452 | Description = "action", 453 | Attributes = new Dictionary { 454 | { "type", "set" }, 455 | {"epoch",msgCount.ToString() } 456 | }, 457 | Content = new List { g } 458 | }, WriteBinaryType.Group, tag); 459 | return tag; 460 | } 461 | private string SendJson(string str, Action act = null) 462 | { 463 | var tag = $"{DateTime.Now.GetTimeStampInt()}.--{Interlocked.Increment(ref _msgCount) - 1}"; 464 | AddCallback(tag, act); 465 | Send($"{tag},{str}"); 466 | return tag; 467 | } 468 | private void Send(string str) 469 | { 470 | Send(Encoding.UTF8.GetBytes(str)); 471 | } 472 | private void Send(byte[] bs) 473 | { 474 | //lock (_sendObj) 475 | //{ 476 | _webSocket.SendAsync(new ArraySegment(bs, 0, bs.Length), WebSocketMessageType.Text, true, CancellationToken.None); 477 | //} 478 | } 479 | private void Receive(ReceiveModel receiveModel) 480 | { 481 | Task.Factory.StartNew(async () => 482 | { 483 | var receiveResult = await _webSocket.ReceiveAsync(receiveModel.ReceiveData, CancellationToken.None); 484 | try 485 | { 486 | if (receiveResult.EndOfMessage) 487 | { 488 | Receive(ReceiveModel.GetReceiveModel()); 489 | receiveModel.End(receiveResult.Count, receiveResult.MessageType); 490 | await ReceiveHandle(receiveModel); 491 | } 492 | else 493 | { 494 | receiveModel.Continue(receiveResult.Count); 495 | Receive(receiveModel); 496 | } 497 | } 498 | catch 499 | { 500 | LogUtil.Warn("连接断开"); 501 | _webSocket.Dispose(); 502 | _ = Task.Factory.StartNew(() => AccountDroppedEvent?.Invoke()); 503 | } 504 | }); 505 | 506 | } 507 | private byte[] EncryptBinaryMessage(Node node) 508 | { 509 | var b = node.Marshal(); 510 | var iv = Convert.FromBase64String("aKs1sBxLFMBHVkUQwS/YEg=="); //GetRandom(16); 511 | var cipher = b.AesCbcEncrypt(Session.EncKey, iv); 512 | var cipherIv = iv.Concat(cipher).ToArray(); 513 | var hash = cipherIv.HMACSHA256_Encrypt(Session.MacKey); 514 | var data = new byte[cipherIv.Length + 32]; 515 | Array.Copy(hash, data, 32); 516 | Array.Copy(cipherIv, 0, data, 32, cipherIv.Length); 517 | return data; 518 | } 519 | private bool LoginResponseHandle(ReceiveModel receive) 520 | { 521 | var challenge = receive.Body.RegexGetString("\"challenge\":\"([^\"]*)\""); 522 | if (challenge.IsNullOrWhiteSpace()) 523 | { 524 | var jsData = JsonConvert.DeserializeObject(receive.Body); 525 | Session.ClientToken = jsData[1]["clientToken"]; 526 | Session.ServerToken = jsData[1]["serverToken"]; 527 | Session.Wid = jsData[1]["wid"]; 528 | _ = Task.Factory.StartNew(() => LoginSuccessEvent?.Invoke(Session)); 529 | _loginSuccess = true; 530 | } 531 | else 532 | { 533 | AddSnapReceive("s2", LoginResponseHandle); 534 | ResolveChallenge(challenge); 535 | } 536 | return true; 537 | } 538 | private void ReLogin() 539 | { 540 | AddSnapReceive("s1", LoginResponseHandle); 541 | SendJson($"[\"admin\",\"init\",[2,2033,7],[\"Windows\",\"Chrome\",\"10\"],\"{Session.ClientId}\",true]"); 542 | Task.Delay(5000).ContinueWith(t => 543 | { 544 | SendJson($"[\"admin\",\"login\",\"{Session.ClientToken}\",\"{Session.ServerToken}\",\"{Session.ClientId}\",\"takeover\"]"); 545 | }); 546 | 547 | } 548 | private void ResolveChallenge(string challenge) 549 | { 550 | var decoded = Convert.FromBase64String(challenge); 551 | var loginChallenge = decoded.HMACSHA256_Encrypt(Session.MacKey); 552 | SendJson($"[\"admin\",\"challenge\",\"{Convert.ToBase64String(loginChallenge)}\",\"{Session.ServerToken}\",\"{Session.ClientId}\"]"); 553 | } 554 | private void WhatsAppLogin() 555 | { 556 | Task.Factory.StartNew(async () => 557 | { 558 | var clientId = GetRandom(16); 559 | Session.ClientId = Convert.ToBase64String(clientId); 560 | var tag = SendJson($"[\"admin\",\"init\",[2,2033,7],[\"Windows\",\"Chrome\",\"10\"],\"{Session.ClientId}\",true]"); 561 | string refUrl = null; 562 | AddSnapReceive(tag, rm => 563 | { 564 | if (rm.Body.Contains("\"ref\":\"")) 565 | { 566 | refUrl = rm.Body.RegexGetString("\"ref\":\"([^\"]*)\""); 567 | return true; 568 | } 569 | return false; 570 | }); 571 | var privateKey = Curve25519.CreateRandomPrivateKey(); 572 | var publicKey = Curve25519.GetPublicKey(privateKey); 573 | AddSnapReceive("s1", rm => 574 | { 575 | var jsData = JsonConvert.DeserializeObject(rm.Body); 576 | Session.ClientToken = jsData[1]["clientToken"]; 577 | Session.ServerToken = jsData[1]["serverToken"]; 578 | Session.Wid = jsData[1]["wid"]; 579 | string secret = jsData[1]["secret"]; 580 | var decodedSecret = Convert.FromBase64String(secret); 581 | var pubKey = decodedSecret.Take(32).ToArray(); 582 | var sharedSecret = Curve25519.GetSharedSecret(privateKey, pubKey); 583 | var data = sharedSecret.HMACSHA256_Encrypt(new byte[32]); 584 | var sharedSecretExtended = new Hkdf(HashAlgorithmName.SHA256).Expand(data, 80); 585 | var checkSecret = new byte[112]; 586 | Array.Copy(decodedSecret, checkSecret, 32); 587 | Array.Copy(decodedSecret, 64, checkSecret, 32, 80); 588 | var sign = checkSecret.HMACSHA256_Encrypt(sharedSecretExtended.Skip(32).Take(32).ToArray()); 589 | if (!sign.ValueEquals(decodedSecret.Skip(32).Take(32).ToArray())) 590 | { 591 | LogUtil.Error("签名校验错误"); 592 | return true; 593 | } 594 | var keysEncrypted = new byte[96]; 595 | Array.Copy(sharedSecretExtended, 64, keysEncrypted, 0, 16); 596 | Array.Copy(decodedSecret, 64, keysEncrypted, 16, 80); 597 | var keyDecrypted = decodedSecret.Skip(64).ToArray().AesCbcDecrypt(sharedSecretExtended.Take(32).ToArray(), sharedSecretExtended.Skip(64).ToArray()); 598 | Session.EncKey = keyDecrypted.Take(32).ToArray(); 599 | Session.MacKey = keyDecrypted.Skip(32).ToArray(); 600 | _ = Task.Factory.StartNew(() => LoginSuccessEvent?.Invoke(Session)); 601 | _loginSuccess = true; 602 | return true; 603 | }); 604 | while (refUrl.IsNullOrWhiteSpace()) 605 | { 606 | await Task.Delay(100); 607 | } 608 | var loginUrl = $"{refUrl},{Convert.ToBase64String(publicKey)},{Session.ClientId}"; 609 | _ = Task.Factory.StartNew(() => LoginScanCodeEvent?.Invoke(loginUrl)); 610 | 611 | }); 612 | 613 | } 614 | private async Task GetDecryptNode(ReceiveModel rm) 615 | { 616 | if (rm.Nodes != null) 617 | { 618 | return rm.Nodes; 619 | } 620 | if (rm.MessageType == WebSocketMessageType.Binary && rm.ByteData.Length >= 33) 621 | { 622 | while (!_loginSuccess) 623 | { 624 | await Task.Delay(100); 625 | } 626 | var tindex = Array.IndexOf(rm.ByteData, (byte)44, 0, rm.ByteData.Length); 627 | var wd = rm.ByteData.Skip(tindex + 1).ToArray(); 628 | var data = wd.Skip(32).ToArray(); 629 | if (!wd.Take(32).ToArray().ValueEquals(data.HMACSHA256_Encrypt(Session.MacKey))) 630 | { 631 | return null; 632 | } 633 | var decryptData = data.AesCbcDecrypt(Session.EncKey); 634 | var bd = new BinaryDecoder(decryptData); 635 | var node = bd.ReadNode(); 636 | rm.Nodes = node; 637 | return rm.Nodes; 638 | } 639 | return null; 640 | } 641 | private async Task ReceiveHandle(ReceiveModel rm) 642 | { 643 | var node = await GetDecryptNode(rm); 644 | if (rm.Tag != null && _snapReceiveDictionary.ContainsKey(rm.Tag)) 645 | { 646 | var result = await Task.Factory.StartNew(() => _snapReceiveDictionary[rm.Tag](rm)); 647 | if (result) 648 | { 649 | lock (_snapReceiveLock) 650 | { 651 | if (_snapReceiveRemoveCountDictionary.ContainsKey(rm.Tag)) 652 | { 653 | if (_snapReceiveRemoveCountDictionary[rm.Tag] <= 1) 654 | { 655 | _snapReceiveRemoveCountDictionary.Remove(rm.Tag); 656 | } 657 | else 658 | { 659 | _snapReceiveRemoveCountDictionary[rm.Tag] = _snapReceiveRemoveCountDictionary[rm.Tag] - 1; 660 | return; 661 | } 662 | } 663 | _snapReceiveDictionary.Remove(rm.Tag); 664 | } 665 | return; 666 | } 667 | } 668 | if (node != null) 669 | { 670 | if (node.Content is List nodeList) 671 | { 672 | foreach (var item in nodeList) 673 | { 674 | if (item.Description == "message") 675 | { 676 | var messageData = item.Content as byte[]; 677 | var ms = WebMessageInfo.Parser.ParseFrom(messageData); 678 | if (ms.Message != null) 679 | { 680 | if (ms.Message.ImageMessage != null && ReceiveImageMessageEvent != null) 681 | { 682 | _ = Task.Factory.StartNew(async () => 683 | { 684 | try 685 | { 686 | 687 | var fileData = await DownloadImage(ms.Message.ImageMessage.Url, ms.Message.ImageMessage.MediaKey.ToArray()); 688 | ReceiveImageMessageEvent.Invoke(new Messages.ImageMessage 689 | { 690 | MessageTimestamp = ms.MessageTimestamp, 691 | RemoteJid = ms.Key.RemoteJid, 692 | Text = ms.Message.ImageMessage.Caption, 693 | ImageData = fileData, 694 | MsgId = ms.Key.Id, 695 | FromMe = ms.Key.FromMe, 696 | Status = (int)ms.Status, 697 | }); 698 | } 699 | catch (Exception ex) 700 | { 701 | LoadMediaInfo(ms.Key.RemoteJid, ms.Key.Id, ms.Key.FromMe ? "true" : "false", async _ => 702 | { 703 | try 704 | { 705 | var fileData = await DownloadImage(ms.Message.ImageMessage.Url, ms.Message.ImageMessage.MediaKey.ToArray()); 706 | var ignore = Task.Factory.StartNew(() => ReceiveImageMessageEvent.Invoke(new Messages.ImageMessage 707 | { 708 | MessageTimestamp = ms.MessageTimestamp, 709 | RemoteJid = ms.Key.RemoteJid, 710 | Text = ms.Message.ImageMessage.Caption, 711 | ImageData = fileData 712 | })); 713 | } 714 | catch 715 | { 716 | LogUtil.Error($"图片下载失败"); 717 | return; 718 | } 719 | }); 720 | } 721 | }); 722 | } 723 | else if (ms.Message.HasConversation && ReceiveTextMessageEvent != null) 724 | { 725 | _ = Task.Factory.StartNew(() => ReceiveTextMessageEvent?.Invoke(new TextMessage 726 | { 727 | MessageTimestamp = ms.MessageTimestamp, 728 | RemoteJid = ms.Key.RemoteJid, 729 | Text = ms.Message.Conversation, 730 | MsgId = ms.Key.Id, 731 | FromMe = ms.Key.FromMe, 732 | Status = (int)ms.Status, 733 | })); 734 | } 735 | else 736 | { 737 | InvokeReceiveRemainingMessagesEvent(messageData); 738 | } 739 | } 740 | else 741 | { 742 | InvokeReceiveRemainingMessagesEvent(messageData); 743 | } 744 | } 745 | else if (item.Content is byte[] bs) 746 | { 747 | InvokeReceiveRemainingMessagesEvent(bs); 748 | } 749 | } 750 | } 751 | else 752 | { 753 | InvokeReceiveRemainingMessagesEvent(rm); 754 | } 755 | } 756 | else 757 | { 758 | InvokeReceiveRemainingMessagesEvent(rm); 759 | } 760 | } 761 | private byte[] GetRandom(int length) 762 | { 763 | var random = new Random(); 764 | byte[] bs = new byte[length]; 765 | for (int i = 0; i < length; i++) 766 | { 767 | bs[i] = (byte)random.Next(0, 255); 768 | } 769 | return bs; 770 | } 771 | private void InvokeReceiveRemainingMessagesEvent(ReceiveModel receiveModel) 772 | { 773 | Task.Factory.StartNew(() => ReceiveRemainingMessagesEvent?.Invoke(receiveModel)); 774 | } 775 | private void InvokeReceiveRemainingMessagesEvent(byte[] data) 776 | { 777 | InvokeReceiveRemainingMessagesEvent(ReceiveModel.GetReceiveModel(data)); 778 | } 779 | private void AddSnapReceive(string tag, Func func, int count = 0) 780 | { 781 | if (count != 0) 782 | { 783 | _snapReceiveRemoveCountDictionary.Add(tag, count); 784 | } 785 | _snapReceiveDictionary.Add(tag, func); 786 | } 787 | 788 | #endregion 789 | } 790 | } 791 | -------------------------------------------------------------------------------- /WhatsAppLib/WhatsAppLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /WhatsAppLib/WhatsAppLib.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /WhatsAppLib/Yove.Proxy/ProxyClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Text; 4 | using System.Net.Sockets; 5 | using System.Threading.Tasks; 6 | using System.Security.Authentication; 7 | 8 | namespace Yove.Proxy 9 | { 10 | public class ProxyClient : IDisposable, IWebProxy 11 | { 12 | #region IWebProxy 13 | 14 | public ICredentials Credentials { get; set; } 15 | 16 | public int ReadWriteTimeOut { get; set; } = 30000; 17 | 18 | public Uri GetProxy(Uri destination) => InternalUri; 19 | public bool IsBypassed(Uri host) => false; 20 | 21 | #endregion 22 | 23 | #region Internal Server 24 | 25 | private Uri InternalUri { get; set; } 26 | private Socket InternalServer { get; set; } 27 | private int InternalPort { get; set; } 28 | 29 | #endregion 30 | 31 | #region ProxyClient 32 | 33 | private IPAddress Host { get; set; } 34 | private int Port { get; set; } 35 | private string Username { get; set; } 36 | private string Password { get; set; } 37 | private ProxyType Type { get; set; } 38 | private int SocksVersion { get; set; } 39 | 40 | public bool IsDisposed { get; set; } 41 | 42 | #endregion 43 | 44 | private const byte AddressTypeIPV4 = 0x01; 45 | private const byte AddressTypeIPV6 = 0x04; 46 | private const byte AddressTypeDomainName = 0x03; 47 | 48 | public ProxyClient(string Proxy, ProxyType Type) 49 | : this(Proxy, null, null, null, Type) { } 50 | 51 | public ProxyClient(string Proxy, string Username, ProxyType Type) 52 | : this(Proxy, null, Username, null, Type) { } 53 | 54 | public ProxyClient(string Proxy, string Username, string Password, ProxyType Type) 55 | : this(Proxy, null, Username, Password, Type) { } 56 | 57 | public ProxyClient(string Host, int Port, ProxyType Type) 58 | : this(Host, Port, null, null, Type) { } 59 | 60 | public ProxyClient(string Host, int Port, string Username, ProxyType Type) 61 | : this(Host, Port, Username, null, Type) { } 62 | 63 | public ProxyClient(string Host, int? Port, string Username, string Password, ProxyType Type) 64 | { 65 | if (Type == ProxyType.Http) 66 | { 67 | InternalUri = new Uri($"http://{Host}:{Port}"); 68 | return; 69 | } 70 | 71 | if (string.IsNullOrEmpty(Host)) 72 | throw new ArgumentNullException("Host null or empty"); 73 | 74 | if (Port == null && Host.Contains(":")) 75 | { 76 | Port = Convert.ToInt32(Host.Split(':')[1].Trim()); 77 | Host = Host.Split(':')[0]; 78 | 79 | if (Port < 0 || Port > 65535) 80 | throw new ArgumentOutOfRangeException("Port goes beyond"); 81 | } 82 | else if (Port == null && !Host.Contains(":")) 83 | { 84 | throw new ArgumentNullException("Incorrect host"); 85 | } 86 | 87 | if (!string.IsNullOrEmpty(Username)) 88 | { 89 | if (Username.Length > 255) 90 | throw new ArgumentNullException("Username null or long"); 91 | 92 | this.Username = Username; 93 | } 94 | 95 | if (!string.IsNullOrEmpty(Password)) 96 | { 97 | if (Password.Length > 255) 98 | throw new ArgumentNullException("Password null or long"); 99 | 100 | this.Password = Password; 101 | } 102 | 103 | this.Host = GetHost(Host); 104 | this.Port = Port.Value; 105 | this.Type = Type; 106 | this.SocksVersion = (Type == ProxyType.Socks4) ? 4 : 5; 107 | 108 | CreateInternalServer(); 109 | } 110 | 111 | private async void CreateInternalServer() 112 | { 113 | InternalServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) 114 | { 115 | ReceiveTimeout = ReadWriteTimeOut, 116 | SendTimeout = ReadWriteTimeOut, 117 | ExclusiveAddressUse = true 118 | }; 119 | 120 | InternalServer.Bind(new IPEndPoint(IPAddress.Loopback, 0)); 121 | 122 | InternalPort = ((IPEndPoint)(InternalServer.LocalEndPoint)).Port; 123 | InternalUri = new Uri($"http://127.0.0.1:{InternalPort}"); 124 | 125 | InternalServer.Listen(512); 126 | 127 | while (!IsDisposed) 128 | { 129 | try 130 | { 131 | using (Socket InternalClient = await InternalServer?.AcceptAsync()) 132 | { 133 | if (InternalClient != null) 134 | await HandleClient(InternalClient); 135 | } 136 | } 137 | catch 138 | { 139 | //? Ignore dispose intrnal server 140 | } 141 | } 142 | } 143 | 144 | private async Task HandleClient(Socket InternalClient) 145 | { 146 | if (IsDisposed) 147 | return; 148 | 149 | byte[] HeaderBuffer = new byte[8192]; 150 | 151 | InternalClient.Receive(HeaderBuffer, HeaderBuffer.Length, 0); 152 | 153 | string Header = Encoding.ASCII.GetString(HeaderBuffer); 154 | 155 | string HttpVersion = Header.Split(' ')[2].Split('\r')[0]?.Trim(); 156 | string TargetURL = Header.Split(' ')[1]?.Trim(); 157 | 158 | if (string.IsNullOrEmpty(HttpVersion) || string.IsNullOrEmpty(TargetURL)) 159 | return; 160 | 161 | string TargetHostname = string.Empty; 162 | int TargetPort = 0; 163 | 164 | if (TargetURL.Contains(":") && !TargetURL.Contains("http://")) 165 | { 166 | TargetHostname = TargetURL.Split(':')[0]; 167 | TargetPort = int.Parse(TargetURL.Split(':')[1]); 168 | } 169 | else 170 | { 171 | Uri URL = new Uri(TargetURL); 172 | 173 | TargetHostname = URL.Host; 174 | TargetPort = URL.Port; 175 | } 176 | 177 | using (Socket TargetClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) 178 | { 179 | ReceiveTimeout = ReadWriteTimeOut, 180 | SendTimeout = ReadWriteTimeOut, 181 | ExclusiveAddressUse = true 182 | }) 183 | { 184 | try 185 | { 186 | if (!TargetClient.ConnectAsync(Host, Port).Wait(ReadWriteTimeOut) || !TargetClient.Connected) 187 | { 188 | SendMessage(InternalClient, $"{HttpVersion} 408 Request Timeout\r\n\r\n"); 189 | return; 190 | } 191 | 192 | SocketError Connection = Type == ProxyType.Socks4 ? 193 | await SendSocks4(TargetClient, TargetHostname, TargetPort) : 194 | await SendSocks5(TargetClient, TargetHostname, TargetPort); 195 | 196 | if (Connection != SocketError.Success) 197 | { 198 | if (Connection == SocketError.HostUnreachable || Connection == SocketError.ConnectionRefused || Connection == SocketError.ConnectionReset) 199 | SendMessage(InternalClient, $"{HttpVersion} 502 Bad Gateway\r\n\r\n"); 200 | else if (Connection == SocketError.AccessDenied) 201 | SendMessage(InternalClient, $"{HttpVersion} 401 Unauthorized\r\n\r\n"); 202 | else 203 | SendMessage(InternalClient, $"{HttpVersion} 500 Internal Server Error\r\nX-Proxy-Error-Type: {Connection}\r\n\r\n"); 204 | } 205 | else 206 | { 207 | SendMessage(InternalClient, $"{HttpVersion} 200 Connection established\r\n\r\n"); 208 | 209 | Relay(InternalClient, TargetClient, false); 210 | } 211 | } 212 | catch (AuthenticationException) 213 | { 214 | SendMessage(InternalClient, $"{HttpVersion} 511 Network Authentication Required\r\n\r\n"); 215 | } 216 | catch 217 | { 218 | SendMessage(InternalClient, $"{HttpVersion} 408 Request Timeout\r\n\r\n"); 219 | } 220 | } 221 | } 222 | 223 | private void Relay(Socket Source, Socket Target, bool IsTarget) 224 | { 225 | try 226 | { 227 | if (!IsTarget) 228 | Task.Run(() => Relay(Target, Source, true)); 229 | 230 | while (true) 231 | { 232 | byte[] Buffer = new byte[8192]; 233 | 234 | int Read = Source.Receive(Buffer, 0, Buffer.Length, SocketFlags.None); 235 | 236 | if (Read == 0) 237 | break; 238 | 239 | Target.Send(Buffer, 0, Read, SocketFlags.None); 240 | } 241 | } 242 | catch 243 | { 244 | //? Ignore timeout exception 245 | } 246 | } 247 | 248 | private async Task SendSocks4(Socket Socket, string DestinationHost, int DestinationPort) 249 | { 250 | byte AddressType = GetAddressType(DestinationHost); 251 | 252 | if (AddressType == AddressTypeDomainName) 253 | DestinationHost = GetHost(DestinationHost).ToString(); 254 | 255 | byte[] Address = GetIPAddressBytes(DestinationHost); 256 | byte[] Port = GetPortBytes(DestinationPort); 257 | byte[] UserId = string.IsNullOrEmpty(Username) ? new byte[0] : Encoding.ASCII.GetBytes(Username); 258 | 259 | byte[] Request = new byte[9 + UserId.Length]; 260 | 261 | Request[0] = (byte)SocksVersion; 262 | Request[1] = 0x01; 263 | Address.CopyTo(Request, 4); 264 | Port.CopyTo(Request, 2); 265 | UserId.CopyTo(Request, 8); 266 | Request[8 + UserId.Length] = 0x00; 267 | 268 | byte[] Response = new byte[8]; 269 | 270 | Socket.Send(Request); 271 | 272 | await WaitStream(Socket); 273 | 274 | Socket.Receive(Response); 275 | 276 | if (Response[1] != 0x5a) 277 | return SocketError.ConnectionRefused; 278 | 279 | return SocketError.Success; 280 | } 281 | 282 | private async Task SendSocks5(Socket Socket, string DestinationHost, int DestinationPort) 283 | { 284 | byte[] Response = new byte[255]; 285 | 286 | byte[] Auth = new byte[3]; 287 | Auth[0] = (byte)SocksVersion; 288 | Auth[1] = (byte)1; 289 | 290 | if (!string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password)) 291 | Auth[2] = 0x02; 292 | else 293 | Auth[2] = (byte)0; 294 | 295 | Socket.Send(Auth); 296 | 297 | await WaitStream(Socket); 298 | 299 | Socket.Receive(Response); 300 | 301 | if (Response[1] == 0x02) 302 | await SendAuth(Socket); 303 | else if (Response[1] != 0x00) 304 | return SocketError.ConnectionRefused; 305 | 306 | byte AddressType = GetAddressType(DestinationHost); 307 | 308 | if (AddressType == AddressTypeDomainName) 309 | DestinationHost = GetHost(DestinationHost).ToString(); 310 | 311 | byte[] Address = GetAddressBytes(AddressType, DestinationHost); 312 | byte[] Port = GetPortBytes(DestinationPort); 313 | 314 | byte[] Request = new byte[4 + Address.Length + 2]; 315 | 316 | Request[0] = (byte)SocksVersion; 317 | Request[1] = 0x01; 318 | Request[2] = 0x00; 319 | Request[3] = AddressType; 320 | 321 | Address.CopyTo(Request, 4); 322 | Port.CopyTo(Request, 4 + Address.Length); 323 | 324 | Socket.Send(Request); 325 | 326 | await WaitStream(Socket); 327 | 328 | Socket.Receive(Response); 329 | 330 | if (Response[1] != 0x00) 331 | return SocketError.ConnectionRefused; 332 | 333 | return SocketError.Success; 334 | } 335 | 336 | private async Task SendAuth(Socket Socket) 337 | { 338 | byte[] Uname = Encoding.ASCII.GetBytes(Username); 339 | byte[] Passwd = Encoding.ASCII.GetBytes(Password); 340 | 341 | byte[] Request = new byte[Uname.Length + Passwd.Length + 3]; 342 | 343 | Request[0] = 1; 344 | Request[1] = (byte)Uname.Length; 345 | Uname.CopyTo(Request, 2); 346 | Request[2 + Uname.Length] = (byte)Passwd.Length; 347 | Passwd.CopyTo(Request, 3 + Uname.Length); 348 | 349 | Socket.Send(Request); 350 | 351 | byte[] Response = new byte[2]; 352 | 353 | await WaitStream(Socket); 354 | 355 | Socket.Receive(Response); 356 | 357 | if (Response[1] != 0x00) 358 | throw new AuthenticationException(); 359 | } 360 | 361 | private async Task WaitStream(Socket Socket) 362 | { 363 | int Sleep = 0; 364 | int Delay = (Socket.ReceiveTimeout < 10) ? 10 : Socket.ReceiveTimeout; 365 | 366 | while (Socket.Available == 0) 367 | { 368 | if (Sleep < Delay) 369 | { 370 | Sleep += 10; 371 | await Task.Delay(10); 372 | 373 | continue; 374 | } 375 | 376 | throw new TimeoutException(); 377 | } 378 | } 379 | 380 | private void SendMessage(Socket Client, string Message) 381 | { 382 | Client.Send(Encoding.UTF8.GetBytes(Message)); 383 | } 384 | 385 | private IPAddress GetHost(string Host) 386 | { 387 | if (IPAddress.TryParse(Host, out IPAddress Ip)) 388 | return Ip; 389 | 390 | return Dns.GetHostAddresses(Host)[0]; 391 | } 392 | 393 | private byte[] GetAddressBytes(byte AddressType, string Host) 394 | { 395 | switch (AddressType) 396 | { 397 | case AddressTypeIPV4: 398 | case AddressTypeIPV6: 399 | return IPAddress.Parse(Host).GetAddressBytes(); 400 | case AddressTypeDomainName: 401 | byte[] Bytes = new byte[Host.Length + 1]; 402 | 403 | Bytes[0] = (byte)Host.Length; 404 | Encoding.ASCII.GetBytes(Host).CopyTo(Bytes, 1); 405 | 406 | return Bytes; 407 | default: 408 | return null; 409 | } 410 | } 411 | 412 | private byte GetAddressType(string Host) 413 | { 414 | if (IPAddress.TryParse(Host, out IPAddress Ip)) 415 | { 416 | if (Ip.AddressFamily == AddressFamily.InterNetwork) 417 | return AddressTypeIPV4; 418 | 419 | return AddressTypeIPV6; 420 | } 421 | 422 | return AddressTypeDomainName; 423 | } 424 | 425 | private byte[] GetIPAddressBytes(string DestinationHost) 426 | { 427 | IPAddress Address = null; 428 | 429 | if (!IPAddress.TryParse(DestinationHost, out Address)) 430 | { 431 | IPAddress[] IPs = Dns.GetHostAddresses(DestinationHost); 432 | 433 | if (IPs.Length > 0) 434 | Address = IPs[0]; 435 | } 436 | 437 | return Address.GetAddressBytes(); 438 | } 439 | 440 | private byte[] GetPortBytes(int Port) 441 | { 442 | byte[] ArrayBytes = new byte[2]; 443 | 444 | ArrayBytes[0] = (byte)(Port / 256); 445 | ArrayBytes[1] = (byte)(Port % 256); 446 | 447 | return ArrayBytes; 448 | } 449 | 450 | public void Dispose() 451 | { 452 | if (!IsDisposed) 453 | { 454 | IsDisposed = true; 455 | 456 | if (InternalServer != null && InternalServer.Connected) 457 | InternalServer.Disconnect(false); 458 | 459 | InternalServer?.Dispose(); 460 | 461 | InternalServer = null; 462 | } 463 | } 464 | 465 | ~ProxyClient() 466 | { 467 | Dispose(); 468 | 469 | GC.SuppressFinalize(this); 470 | } 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /WhatsAppLib/Yove.Proxy/ProxyType.cs: -------------------------------------------------------------------------------- 1 | namespace Yove.Proxy 2 | { 3 | public enum ProxyType 4 | { 5 | Http, 6 | Socks4, 7 | Socks5 8 | } 9 | } --------------------------------------------------------------------------------