跳至主要內容

写一个Drcom第三方客户端

MonoLogueChiC#软件大约 7 分钟

前一阵子弄了一个 Drcom 登陆客户端,用于登陆校园网,具体可以看中南大学联通校园网络第三方客户端open in new window,这次简单说一下这个东西是怎么做出来的。

这也是我第二次写 WPF 程序,很多地方都是一边学习一边瞎搞的。

项目地址:https://github.com/MonoLogueChi/Drcomopen in new window

原理分析

首先是自己抓吧,还有参考 GitHub 上已有的项目,得出了登陆方式,然后写了一个简单的网页版,测试了一下。

就是利用 POST 提交表单登陆,用 Get 方式注销,表单为:

NameValue
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,在返回响应中获得了错误代码,并且得到了登陆时长和流量使用的数据,有用open in new window 的信息包括:

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, "未检测到最新版本", "未检测到更新时间" });
            }
        }
    }
}

写在后面的

至此,所有功能均已介绍完毕,有兴趣的同学可以接盘继续开发,项目地址在中南大学联通校园网络第三方客户端open in new window中。

友情提示,修改密码功能其实实现也很简单,GitHub 上已经找到有事项该功能的项目了,只要把 IP 改成自己的,剩下基本照抄就能实现了。