写一个Drcom第三方客户端
2018年3月1日大约 7 分钟
前一阵子弄了一个 Drcom 登陆客户端,用于登陆校园网,具体可以看中南大学联通校园网络第三方客户端,这次简单说一下这个东西是怎么做出来的。
这也是我第二次写 WPF 程序,很多地方都是一边学习一边瞎搞的。
项目地址:https://github.com/MonoLogueChi/Drcom
原理分析
首先是自己抓吧,还有参考 GitHub 上已有的项目,得出了登陆方式,然后写了一个简单的网页版,测试了一下。
就是利用 POST 提交表单登陆,用 Get 方式注销,表单为:
Name | Value |
---|---|
DDDDD | 账号 |
upass | 密码 |
0MKKey | |
Submit | %E7%99%BB+%E5%BD%95 (转义过来也就是登 陆 ) |
前面都是很常规的东西,我个人觉得最有意思的就是后面获取登陆 IP 和登陆提示那一部分了。
界面和交互
设计界面
界面设计部分 xaml 没啥意思,就不放了,最后大概就是下面这张图这样
交互逻辑
其中包括功能:
- IP 输入和自动获取 IP;
- 账号输入和记住账号;
- 密码输入和记住密码;
- 登陆和注销按钮;
- 检查更新和关于软件;
using System.Diagnostics;
using System.Windows;
using Drcom.net;
namespace Drcom
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Settings();
}
//初始化配置
private void Settings()
{
if (Setting.GetSetting("nip") == "" || Setting.GetSetting("nip") == null)
{
GetIp.IsChecked = true;
}
else
{
TextIp.Text = Setting.GetSetting("nip");
}
if (Setting.GetSetting("uid") != "")
{
SaveUid.IsChecked = true;
TextUid.Text = Setting.GetSetting("uid");
}
if (Setting.GetSetting("pwd") != "")
{
SavePwd.IsChecked = true;
TextPwd.Password = Setting.GetSetting("pwd");
}
}
//登陆按钮
private void LoginNet(object sender, RoutedEventArgs e)
{
//获取账号密码ip
string nip = TextIp.Text;
string uid = TextUid.Text;
string pwd = TextPwd.Password;
//获取登陆IP
if (GetIp.IsChecked == true)
{
TextIp.Text = CsuNet.LoginIP();
nip = TextIp.Text;
}
//保存IP,账号和密码
if (TextIp.Text != null & TextIp.Text != "")
{
Setting.UpdateSetting("nip", nip);
}
if (SaveUid.IsChecked == true)
{
Setting.UpdateSetting("uid", uid);
}
if (SavePwd.IsChecked == true)
{
Setting.UpdateSetting("pwd", pwd);
}
var relust = CsuNet.LoginCsuNet(nip, uid, pwd);
MessageBox.Show(relust);
}
//注销按钮
private void LogoutNet(object sender, RoutedEventArgs e)
{
string nip = TextIp.Text;
var relust = CsuNet.LogoutCsuNet(nip);
MessageBox.Show(relust);
}
//关于软件
private void About(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
Process.Start("http://url.xxwhite.com?id=5a884de19f5454543ef4201e");
}
//检查更新
private void FindNew(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
string[] version = NewApp.IsNew();
MessageBox.Show("当前版本为:" + version[0] + "\r\n" +
"最新版本为:" + version[1] + "\r\n" +
"最后更新时间为:" + version[2]);
if (version[0] != version[1])
{
Process.Start("https://gitee.com/monologuechi/Drcom/releases");
}
}
}
}
接下来就是各项功能的实现了
核心功能
我们的核心功能便是登陆和注销功能,对于这两部分,其实很好理解,就是 POST 和 GET。这两部分的代码我都写在了Drcom.net.CsuNet.cs
中了
登陆
public static string LoginCsuNet(string nip, string uid, string pwd)
{
if (nip == "" || uid == "" || pwd == "")
{
return "请检查登陆ip,账号和密码是否输入正确";
}
else
{
try
{
string content = "DDDDD=" + uid + "@zndx&upass=" + pwd + "&0MKKey= +Login";
string posthost = "http://" + nip + "/";
string result = null;
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(posthost);
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";
byte[] data = Encoding.UTF8.GetBytes(content);
req.ContentLength = data.Length;
using (Stream reqStream = req.GetRequestStream())
{
reqStream.Write(data, 0, data.Length);
reqStream.Close();
}
HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
Stream stream = resp.GetResponseStream();
//获取响应内容
using (StreamReader reader = new StreamReader(stream, Encoding.ASCII))
{
result = reader.ReadToEnd();
return LoginCaes(result);
}
}
catch (Exception)
{
return "发生未知错误\r\n请检查登陆IP是否填写正确";
}
}
}
注销
public static string LogoutCsuNet(string nip)
{
try
{
string gethost = "http://" + nip + "/F.htm";
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(gethost);
HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
Stream stream = resp.GetResponseStream();
//获取响应内容
using (StreamReader reader = new StreamReader(stream, Encoding.ASCII))
{
string result = reader.ReadToEnd();
return LoginCaes(result);
}
}
catch (Exception)
{
return "发生未知错误";
}
}
至此,软件的基本功能就都已经实现了。
获取错误提示
这一部分纯粹靠抓包获得,我们抓 GET http://119.39.119.2/F.htm,在返回响应中获得了错误代码,并且得到了登陆时长和流量使用的数据,有用 的信息包括:
Msg = 14;
time = "5 ";
flow = "200 ";
fsele = 1;
fee = "0 ";
xsele = 0;
xip = "000.000.000.000.";
mac = "00-00-00-00-00-00";
va = 00;
vb = 00;
vc = 00;
vd = 0000;
ve = 0000;
vf = 0000;
ipm = "77277702";
ss1 = "000d48462d8d";
ss2 = "3905";
ss3 = "0a000104";
ss4 = "28d244d94383";
msga =
""; /* can not modify !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*/
pp = "<br>";
flow0 = flow % 1024;
flow1 = flow - flow0;
flow0 = flow0 * 1000;
flow0 = flow0 - (flow0 % 1024);
fee1 = fee - (fee % 100);
flow3 = ".";
if (flow0 / 1024 < 10) flow3 = ".00";
else {
if (flow0 / 1024 < 100) flow3 = ".0";
}
UT = "已使用时间 : " + time + " Min" + pp;
UF = "已使用流量 : " + flow1 / 1024 + flow3 + flow0 / 1024 + " MByte" + pp;
if (fsele == 1) UM = "本账号余额 : " + "RMB" + fee1 / 10000;
else UM = "";
function DispTFM() {
switch (Msg) {
case 0:
case 1:
if (Msg == 1 && msga != "") {
switch (msga) {
case "error0":
document.write("本IP不允许Web方式登录");
break;
case "error1":
document.write("本账号不允许Web方式登录");
break;
case "error2":
document.write("本账号不允许修改密码");
break;
default:
document.write(msga);
break;
}
} else document.write("账号或密码不对,请重新输入");
break;
case 2:
document.write("该账号正在使用中,请您与网管联系" + pp + xip + pp + mac);
break;
case 3:
document.write(
"本账号只能在指定地址使用<br>This account can be used on the appointed address only." +
pp +
xip
);
break;
case 4:
document.write("本账号费用超支或时长流量超过限制");
break;
case 5:
document.write("本账号暂停使用");
break;
case 6:
document.write("System buffer full");
break;
case 8:
document.write("本账号正在使用,不能修改");
break;
case 9:
document.write("新密码与确认新密码不匹配,不能修改");
break;
case 10:
document.write("密码修改成功");
break;
case 11:
document.write("本账号只能在指定地址使用 :" + pp + mac);
break;
case 7:
document.write(UT + UF + UM);
break;
case 14:
document.write("注销成功" + pp + UT + UF + UM);
break;
case 15:
document.write("登录成功" + pp + UT + UF + UM);
break;
}
}
内容有点长,我们使用字符串拆分,获取有用的信息
public static string LoginCaes(string result)
{
if (result.Contains("Msg="))
{
string[] FMsg = Regex.Split(result, "Msg=", RegexOptions.IgnoreCase);
int Msg = Convert.ToInt32(FMsg[1].Substring(0, 2));
switch (Msg)
{
case 0: return "未知错误";
case 1:
{
string msga = Regex.Split(FMsg[1], "msga=", RegexOptions.IgnoreCase)[1].Substring(1, 1);
if (msga != "\'") { return "错误代码:" + msga; }
else { return "账号或密码错误"; }
}
case 2: return "该账号正在使用中,请您与网管联系";
case 3: return "本账号只能在指定地址使用";
case 4: return "本账号费用超支或时长流量超过限制";
case 5: return "本账号暂停使用";
case 6: return "System buffer full";
case 8: return "本账号正在使用,不能修改";
case 7: return "未知错误";
case 9: return "新密码与确认新密码不匹配,不能修改";
case 10: return "密码修改成功";
case 11: return "本账号只能在指定地址使用";
case 12: return "未知错误";
case 13: return "未知错误";
//注销成功,还要获取一些信息
case 14:
{
try
{
int time = Convert.ToInt32(Regex.Split(FMsg[1], "time=", RegexOptions.IgnoreCase)[1].Substring(1, 9));
float flow = Convert.ToSingle(Regex.Split(FMsg[1], "flow=", RegexOptions.IgnoreCase)[1].Substring(1, 9));
return "注销成功 \r\n" +
"本次使用时长:" + time + " Min \r\n" +
"本次使用流量:" + (flow / 1024f).ToString("F2") + " MByte";
}
catch (Exception) { return "注销成功"; }
}
case 15: return "登录成功";
}
return "未知错误";
}
else { return "您应该大概也许可能已经成功登陆了"; }
}
获取登陆 IP
这一项功能的发现,纯属偶然,抓包中无意抓到的,在未登录状态下,随便发送一个 GET 请求,返回响应里就会有登陆 IP 的信息。
public static string LoginIP()
{
try
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://pingtcss.qq.com/");
HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
Stream stream = resp.GetResponseStream();
//获取响应内容
using (StreamReader reader = new StreamReader(stream, Encoding.ASCII))
{
string result = reader.ReadToEnd();
if (result.Contains("v4serip"))
{
string[] Fnip = Regex.Split(result, "v4serip='", RegexOptions.IgnoreCase);
string[] nip = Regex.Split(Fnip[1], "'", RegexOptions.IgnoreCase);
return nip[0];
}
else { return null; }
}
}
catch (Exception) { return null; }
}
保存设置和检查更新
读取和保存配置
using System;
using System.Configuration;
namespace Drcom.net
{
public class Setting
{
//获取设置
public static string GetSetting(string key)
{
try
{
string value = ConfigurationManager.AppSettings[key].ToString();
return value;
}
catch (Exception) { return null; }
}
//写入设置
public static void UpdateSetting(string key, string value)
{
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
try
{
if (config.AppSettings.Settings[key] != null)
{
config.AppSettings.Settings.Remove(key);
}
config.AppSettings.Settings.Add(key, value);
config.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection("appSettings");
}
catch (Exception) { }
}
}
}
软件检查更新
using System;
using System.Reflection;
using System.Xml.Linq;
namespace Drcom.net
{
public class NewApp
{
public static string[] IsNew()
{
string version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
try
{
string versionxml = "http://v.xxwhite.com/version/Drcom.xml?t=" + DateTime.Now.ToFileTimeUtc().ToString();
XDocument oXDoc = XDocument.Load(versionxml);
XElement root = oXDoc.Root;
XElement lastversion = root.Element("version");
XElement data = root.Element("data");
string[] versiondata = new string[3] { version, lastversion.Value, data.Value };
return versiondata;
}
catch (Exception)
{
return (new string[3] { version, "未检测到最新版本", "未检测到更新时间" });
}
}
}
}
写在后面的
至此,所有功能均已介绍完毕,有兴趣的同学可以接盘继续开发,项目地址在中南大学联通校园网络第三方客户端中。
友情提示,修改密码功能其实实现也很简单,GitHub 上已经找到有事项该功能的项目了,只要把 IP 改成自己的,剩下基本照抄就能实现了。