加载中...
95 |├── .gitignore ├── LICENSE ├── README.md ├── config ├── config.go ├── config_test.go ├── jsonemptypath.go ├── jsonemptypath_test.go ├── jsonpath.go ├── jsonpath_test.go ├── jsonproxy.go ├── jsonproxy_test.go └── ssr_config.go ├── crawler ├── crawler.go └── utils.go ├── geoip └── geoip.go ├── image ├── Removefixed.svg ├── close.png ├── download.svg ├── ic_copy_link.svg └── icon.svg ├── main.go ├── models ├── encrypt.go ├── encrypt_test.go ├── used_amount.go ├── used_amount_test.go ├── users.go └── users_test.go ├── parser ├── invoice.go ├── invoice_test.go ├── parser.go ├── parser_test.go ├── service.go ├── ssrinfo.go ├── ssrnode.go ├── ssrnode_test.go └── testdata │ └── invoice.html ├── pyclient ├── pyssrclient.go ├── pyssrclient_config.go ├── pyssrclient_config_test.go ├── pyssrclient_test.go └── testdata │ ├── test_config.json │ ├── test_default.json │ └── test_empty.json ├── resource.qrc ├── screenshots ├── charts.png ├── invoices.png ├── invoiceview.webp ├── logging.png ├── login.png ├── nodes.png ├── service1.webp ├── service2.webp └── settings.png ├── ssr └── ssr.go ├── urls └── urls.go └── widgets ├── account_item.go ├── charts_dialog.go ├── client_config_widget.go ├── color_label.go ├── config_widget.go ├── data_bridge.go ├── downloader.go ├── invoice_dialog.go ├── invoice_panel.go ├── invoice_view_widget.go ├── login_indicator.go ├── login_widget.go ├── main_widget.go ├── node_model.go ├── notify.go ├── progressbar.go ├── savepath_recorder.go ├── service_panel.go ├── shade_widget.go ├── ssr_node_detail_widget.go ├── ssr_node_info_panel.go ├── ssr_node_select_dialog.go ├── ssr_switch_panel.go ├── ssrclient_config_widget.go ├── summarized_widget.go ├── switch_button.go ├── transparent_button.go ├── used_panel.go ├── utils.go ├── utils_test.go └── widget_utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Qt template 3 | # C++ objects and libs 4 | *.slo 5 | *.lo 6 | *.o 7 | *.a 8 | *.la 9 | *.lai 10 | *.so 11 | *.dll 12 | *.dylib 13 | 14 | # Qt-es 15 | object_script.*.Release 16 | object_script.*.Debug 17 | *_plugin_import.cpp 18 | /.qmake.cache 19 | /.qmake.stash 20 | *.pro.user 21 | *.pro.user.* 22 | *.qbs.user 23 | *.qbs.user.* 24 | *.moc 25 | moc_*.cpp 26 | moc_*.h 27 | qrc_*.cpp 28 | ui_*.h 29 | *.qmlc 30 | *.jsc 31 | Makefile* 32 | *build-* 33 | rcc.cpp 34 | rcc_cgo_* 35 | 36 | # Qt unit tests 37 | target_wrapper.* 38 | 39 | # QtCreator 40 | *.autosave 41 | 42 | # QtCreator Qml 43 | *.qmlproject.user 44 | *.qmlproject.user.* 45 | 46 | # QtCreator CMake 47 | CMakeLists.txt.user* 48 | ### Go template 49 | # Binaries for programs and plugins 50 | *.exe 51 | *.exe~ 52 | 53 | # IDE 54 | .idea/* 55 | 56 | # Auto code gen 57 | widgets/moc.* 58 | widgets/moc_*.* 59 | widgets/rcc.cpp 60 | widgets/rcc_cgo_*.go 61 | 62 | # Test binary, build with `go test -c` 63 | *.test 64 | 65 | # Output of the go coverage tool, specifically when used with LiteIDE 66 | *.out 67 | 68 | # go build result 69 | schannel-qt5 70 | 71 | emoji-flags/ 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2019 apocelipes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## schannel-qt5 2 | A QT based GUI client for [schannel](https://schannel.net/) - written in Golang 3 | 4 | ### Features: 5 | - clear data usage display 6 | - quickly view service information 7 | - supports lots of user settings 8 | - simply & easy to use 9 | - freely configure the ssr client program 10 | - use charts to show data more clearly 11 | 12 | ### Installation 13 | At first we need to install [therecipe/qt](https://github.com/therecipe/qt) 14 | 15 | Then: 16 | ```bash 17 | go get -u github.com/astaxie/beego/orm 18 | go get -u github.com/mattn/go-sqlite3 19 | go get -u github.com/PuerkitoBio/goquery 20 | cd $GOPATH/src 21 | git clone 'https://github.com/apocelipes/schannel-qt5' 22 | # install country flags info 23 | git clone 'https://github.com/matiassingers/emoji-flags' schannel-qt5/emoji-flags 24 | cd schannel-qt5/widgets 25 | qtmoc && qtrcc 26 | cd .. && go build 27 | ``` 28 | 29 | Note that schannel-qt5 needs golang >= go1.12. 30 | 31 | Because of the flaw in QFont on x11, schannel-qt5 can't set its own font without fontconfig, so you need to install the font "NotoColorEmoji" to support emoji. 32 | 33 | Now you can enjoy schannel-qt5! 34 | 35 | ### Screenshots 36 | login: 37 | 38 |  39 | 40 | logging: 41 | 42 |  43 | 44 | invoices view: 45 | 46 |  47 | 48 |  49 | 50 | select nodes: 51 | 52 |  53 | 54 | service info & client switch: 55 | 56 |  57 | 58 |  59 | 60 | user settings: 61 | 62 |  63 | 64 | data charts: 65 | 66 |  67 | 68 | ### important configurations: 69 | - `ssrclient.json`: Configure the behavior of the ssr client. 70 | - `~/.local/share/schannel-qt5.json`: Configure the behavior of the schannel-qt5. 71 | - `~/.local/share/schannel-users.db`: Store encrypted user information and traffic usage records (traffic records for chart display). 72 | - `~/.local/share/data/schannel-qt5/GeoIP/`: Store the GeoIP database. 73 | 74 | ### Options in schannel-qt5.json: 75 | - `proxy_url`: The proxy server address used by schannel-qt5, or empty if you don't use a proxy. 76 | - `log_file`: schannel-qt5's log file, uses stdout if it is empty. 77 | - `ssr_node_config_path`: The path of a ssr node config file. 78 | - `ssr_client_config_path"`: The path of ssr client config file. 79 | - `ssr_bin`: The path of ssr client bin. 80 | 81 | ### Todo: 82 | - support system tray icon 83 | - more clearly document 84 | - more tests 85 | 86 | Welcome feedback questions and submit PRs, 87 | 88 | I am looking forward to working with you. 89 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | const ( 12 | // 默认配置文件路径 13 | configPath = ".local/share/schannel-qt5.json" 14 | ) 15 | 16 | var ( 17 | // ErrHOME 无法查找$HOME 18 | ErrHOME = errors.New("can't find $HOME in your environments") 19 | // ErrNotAbs 路径无法解析为绝对路径 20 | ErrNotAbs = errors.New("path is not an abs path") 21 | ) 22 | 23 | // UserConfig 用户配置 24 | type UserConfig struct { 25 | // client config 26 | Proxy JSONProxy `json:"proxy_url"` 27 | LogFile JSONEmptyPath `json:"log_file"` 28 | 29 | // ssr config 30 | SSRNodeConfigPath JSONPath `json:"ssr_node_config_path"` 31 | SSRClientConfigPath JSONPath `json:"ssr_client_config_path"` 32 | 33 | // ssr client bin path 34 | SSRBin JSONPath `json:"ssr_bin"` 35 | 36 | // ssr client config的实体数据 37 | SSRClientConfig ClientConfig `json:"-"` 38 | } 39 | 40 | // ConfigPath 返回`~`被替换为$HOME的config path 41 | func ConfigPath() (string, error) { 42 | home, err := os.UserHomeDir() 43 | if err != nil { 44 | return "", ErrHOME 45 | } 46 | 47 | return filepath.Join(home, configPath), nil 48 | } 49 | 50 | // StoreConfig 将配置存储进ConfigPath路径的文件 51 | func (u *UserConfig) StoreConfig() error { 52 | storePath, err := ConfigPath() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | f, err := os.OpenFile(storePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664) 58 | if err != nil { 59 | return err 60 | } 61 | defer f.Close() 62 | 63 | data, err := json.MarshalIndent(u, "", "\t") 64 | if err != nil { 65 | return err 66 | } 67 | 68 | if _, err = f.Write(data); err != nil { 69 | return err 70 | } 71 | 72 | clientConfigPath, err := u.SSRClientConfigPath.AbsPath() 73 | if err != nil { 74 | return err 75 | } 76 | if err := u.SSRClientConfig.Store(clientConfigPath); err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // LoadConfig 从ConfigPath给出的配置文件路径读出配置 84 | func (u *UserConfig) LoadConfig() error { 85 | loadPath, err := ConfigPath() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | f, err := os.Open(loadPath) 91 | if err != nil { 92 | return err 93 | } 94 | defer f.Close() 95 | 96 | data, err := ioutil.ReadAll(f) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | if err = json.Unmarshal(data, u); err != nil { 102 | return err 103 | } 104 | 105 | clientConfigPath, err := u.SSRClientConfigPath.AbsPath() 106 | if err != nil { 107 | return err 108 | } 109 | if err := u.SSRClientConfig.Load(clientConfigPath); err != nil { 110 | return err 111 | } 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "encoding/json" 7 | "os" 8 | ) 9 | 10 | func TestConfigPath(t *testing.T) { 11 | testData := []*struct { 12 | // 设置环境变量HOME的值 13 | home string 14 | res string 15 | }{ 16 | { 17 | home: "/home/test", 18 | res: "/home/test/" + configPath, 19 | }, 20 | { 21 | home: "/home/test/", 22 | res: "/home/test/" + configPath, 23 | }, 24 | { 25 | home: "/home/用户1/", 26 | res: "/home/用户1/" + configPath, 27 | }, 28 | } 29 | 30 | for _, v := range testData { 31 | os.Clearenv() 32 | err := os.Setenv("HOME", v.home) 33 | if err != nil { 34 | t.Fatalf("无法设置$HOME: %v\n", err) 35 | } 36 | res, err := ConfigPath() 37 | if err != nil { 38 | t.Fatalf("获取Config Path错误:%v\n", err) 39 | } 40 | if v.res != res { 41 | format := "不正确的Config Path:\n\twant: %s\n\thave: %v\n" 42 | t.Errorf(format, v.res, res) 43 | } 44 | } 45 | } 46 | 47 | func TestMarshalUserConf(t *testing.T) { 48 | u := new(UserConfig) 49 | u.SSRNodeConfigPath.Data = "/tmp/testing/t.json" 50 | u.SSRClientConfigPath.Data = "/tmp/testing/client.json" 51 | u.SSRBin.Data = "/tmp/testing/a.out" 52 | u.LogFile.Data = "/tmp/a.log" 53 | 54 | data, err := json.MarshalIndent(u, "", "\t") 55 | if err != nil { 56 | t.Error(err) 57 | } 58 | t.Log(string(data)) 59 | } 60 | 61 | func TestUnmarshalUserConf(t *testing.T) { 62 | u := new(UserConfig) 63 | // 需要解析成config的原始数据 64 | data := `{"ssr_node_config_path":"/tmp/t.json","ssr_bin":"/tmp/a.out","log_file":"/tmp/a.log","ssr_client_config_path":"/tmp/c.json"}` 65 | if err := json.Unmarshal([]byte(data), u); err != nil { 66 | t.Error(err) 67 | } 68 | t.Log(*u) 69 | } 70 | -------------------------------------------------------------------------------- /config/jsonemptypath.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "strings" 4 | 5 | // 允许设置路径为空的JSONPath 6 | type JSONEmptyPath struct { 7 | JSONPath 8 | } 9 | 10 | func (ep *JSONEmptyPath) UnmarshalJSON(b []byte) error { 11 | if string(b) == `""` { 12 | ep.Data = string(b) 13 | ep.Data = strings.TrimSuffix(ep.Data, `"`) 14 | ep.Data = strings.TrimPrefix(ep.Data, `"`) 15 | return nil 16 | } 17 | 18 | return ep.JSONPath.UnmarshalJSON(b) 19 | } 20 | 21 | func (ep *JSONEmptyPath) MarshalJSON() ([]byte, error) { 22 | if ep.Data == "" { 23 | return []byte(`""`), nil 24 | } 25 | 26 | return ep.JSONPath.MarshalJSON() 27 | } 28 | 29 | // 路径数据是否为空 30 | func (ep *JSONEmptyPath) IsEmpty() bool { 31 | return ep.Data == "" 32 | } 33 | -------------------------------------------------------------------------------- /config/jsonemptypath_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "encoding/json" 7 | ) 8 | 9 | // 测试路径json转换 10 | type e struct { 11 | Path *JSONEmptyPath `json:"path"` 12 | } 13 | 14 | func TestJSONEmptyPathMarshalJSON(t *testing.T) { 15 | testData := []*struct { 16 | data *e 17 | success bool 18 | res string 19 | }{ 20 | { 21 | data: &e{ 22 | Path: &JSONEmptyPath{JSONPath{Data: ""}}, 23 | }, 24 | success: true, 25 | res: `{"path":""}`, 26 | }, 27 | { 28 | data: &e{ 29 | Path: &JSONEmptyPath{JSONPath{Data: "/tmp/a"}}, 30 | }, 31 | success: true, 32 | res: `{"path":"/tmp/a"}`, 33 | }, 34 | { 35 | data: &e{ 36 | Path: &JSONEmptyPath{JSONPath{Data: "tmp/a"}}, 37 | }, 38 | success: false, 39 | res: ``, 40 | }, 41 | } 42 | 43 | for _, v := range testData { 44 | res, err := json.Marshal(v.data) 45 | if (err == nil) != v.success { 46 | format := "marshal error:\n\tdata: %s\n\terror: %v\n\twant :%v\n" 47 | t.Errorf(format, v.data.Path, err, v.success) 48 | } else if v.success && string(res) != v.res { 49 | format := "marshal wrong:\n\twant: %s\n\thave: %s\n" 50 | t.Errorf(format, v.res, string(res)) 51 | } 52 | } 53 | } 54 | 55 | func TestJSONEmptyPathUnmarshalJSON(t *testing.T) { 56 | testData := []*struct { 57 | data string 58 | success bool 59 | // 存放解析结果 60 | res *e 61 | // 正确的路径 62 | resPath string 63 | }{ 64 | { 65 | data: `{"path":""}`, 66 | success: true, 67 | res: &e{ 68 | Path: &JSONEmptyPath{JSONPath{}}, 69 | }, 70 | resPath: "", 71 | }, 72 | { 73 | data: `{"path":"/tmp/a"}`, 74 | success: true, 75 | res: &e{ 76 | Path: &JSONEmptyPath{JSONPath{}}, 77 | }, 78 | resPath: "/tmp/a", 79 | }, 80 | { 81 | data: `{"path":"tmp/a"}`, 82 | success: false, 83 | res: &e{ 84 | Path: &JSONEmptyPath{JSONPath{}}, 85 | }, 86 | resPath: "", 87 | }, 88 | } 89 | 90 | for _, v := range testData { 91 | err := json.Unmarshal([]byte(v.data), v.res) 92 | if (err == nil) != v.success { 93 | format := "unmarshal error:\n\tdata: %s\n\terror: %v\n\twant :%v\n" 94 | t.Errorf(format, v.data, err, v.success) 95 | } else if v.success && v.res.Path.String() != v.resPath { 96 | format := "unmarshal wrong:\n\twant: %s\n\thave: %s\n" 97 | t.Errorf(format, v.resPath, v.res.Path) 98 | } 99 | } 100 | } 101 | 102 | func TestJSONEmptyPathIsEmpty(t *testing.T) { 103 | testData := []*struct{ 104 | data string 105 | empty bool 106 | }{ 107 | { 108 | data: "/tmp/a", 109 | empty: false, 110 | }, 111 | { 112 | data: "", 113 | empty: true, 114 | }, 115 | } 116 | 117 | for _,v := range testData { 118 | ep := &JSONEmptyPath{JSONPath{Data: v.data}} 119 | if ep.IsEmpty() != v.empty { 120 | format := "isEmpty wrong:\n\tpath: %s\n\twant: %v\n\thave: %v\n" 121 | t.Errorf(format, v.data, v.empty, ep.IsEmpty()) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /config/jsonpath.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "strings" 7 | ) 8 | 9 | // JSONPath unmarshal/marshal时验证路径是否为绝对路径 10 | type JSONPath struct { 11 | Data string 12 | } 13 | 14 | func (jp *JSONPath) UnmarshalJSON(b []byte) error { 15 | data := string(b) 16 | // 对于字符串的json值,需要手动去除双引号 17 | data = strings.TrimSuffix(data, "\"") 18 | data = strings.TrimPrefix(data, "\"") 19 | if !strings.HasPrefix(data, "~") && !path.IsAbs(data) { 20 | return ErrNotAbs 21 | } 22 | 23 | jp.Data = data 24 | return nil 25 | } 26 | 27 | func (jp JSONPath) String() string { 28 | return jp.Data 29 | } 30 | 31 | func (jp *JSONPath) MarshalJSON() ([]byte, error) { 32 | if !strings.HasPrefix(jp.Data, "~") && !path.IsAbs(jp.Data) { 33 | return nil, ErrNotAbs 34 | } 35 | 36 | return []byte("\"" + jp.Data + "\""), nil 37 | } 38 | 39 | // AbsPath 返回对应的绝对路径 40 | func (jp *JSONPath) AbsPath() (string, error) { 41 | if path.IsAbs(jp.Data) { 42 | return jp.Data, nil 43 | } 44 | 45 | if !strings.HasPrefix(jp.Data, "~") { 46 | return "", ErrNotAbs 47 | } 48 | 49 | home, err := os.UserHomeDir() 50 | if err != nil { 51 | return "", ErrHOME 52 | } 53 | 54 | return strings.Replace(jp.Data, "~", home, 1), nil 55 | } 56 | -------------------------------------------------------------------------------- /config/jsonpath_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "encoding/json" 7 | "os" 8 | ) 9 | 10 | func TestAbsPath(t *testing.T) { 11 | // 测试绝对路径 12 | abs := JSONPath{"/testing/abs/path"} 13 | data1, err := abs.AbsPath() 14 | if data1 != abs.Data { 15 | t.Error("wrong on abs path: " + data1) 16 | } else if err != nil { 17 | t.Error(err) 18 | } 19 | 20 | // 测试~开头的HOME下路径 21 | os.Clearenv() 22 | err = os.Setenv("HOME", "/home/testing") 23 | if err != nil { 24 | t.Errorf("set $HOME ERROR: %v\n", err) 25 | } 26 | 27 | underHome := JSONPath{"~/testing/path"} 28 | data2, err := underHome.AbsPath() 29 | if data2 != "/home/testing/testing/path" { 30 | t.Error("wrong on home path: " + data2) 31 | } else if err != nil { 32 | t.Error(err) 33 | } 34 | 35 | // 测试非绝对路径 36 | notAbs := JSONPath{"testing/path"} 37 | _, err = notAbs.AbsPath() 38 | if err == nil { 39 | t.Error("ErrNotAbs didn't work") 40 | } 41 | 42 | empty := JSONPath{""} 43 | _, err = empty.AbsPath() 44 | if err == nil { 45 | t.Error("empty string passed") 46 | } 47 | } 48 | 49 | // testing type 50 | type p struct { 51 | Port int64 `json:"port"` 52 | Path JSONPath `json:"path"` 53 | } 54 | 55 | func TestUnmarshalJson(t *testing.T) { 56 | // 测试json数据 57 | data := "{\"port\":12345,\"path\":\"~/.test/\"}" 58 | 59 | j := new(p) 60 | err := json.Unmarshal([]byte(data), j) 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | 65 | if j.Port != 12345 || j.Path.Data != "~/.test/" { 66 | t.Error("unmarshal error") 67 | } 68 | } 69 | 70 | func TestMarshalJson(t *testing.T) { 71 | j := &p{ 72 | Port: 12345, 73 | Path: JSONPath{Data: "~/.test/"}, 74 | } 75 | data, err := json.Marshal(j) 76 | if err != nil { 77 | t.Error(err) 78 | } 79 | 80 | if string(data) != "{\"port\":12345,\"path\":\"~/.test/\"}" { 81 | t.Error("marshal error") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /config/jsonproxy.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | // ErrNotURL 不是合法的URL 11 | ErrNotURL = errors.New("not an valid URL") 12 | 13 | matcher = regexp.MustCompile(`^(http(s)?|socks5)://([\w\-]+\.)+[\w\-]+(:\d+)?(/[\w\- ./?%&=]*)?$`) 14 | ) 15 | 16 | // JSONProxy 验证给定字符串是否是合法的URL 17 | type JSONProxy struct { 18 | Data string 19 | } 20 | 21 | // IsURL 如果p的值是合法的URL,则返回true 22 | func (p *JSONProxy) IsURL() bool { 23 | return matcher.MatchString(p.Data) 24 | } 25 | 26 | func (p JSONProxy) String() string { 27 | return p.Data 28 | } 29 | 30 | func (p *JSONProxy) UnmarshalJSON(b []byte) error { 31 | data := string(b) 32 | // 对于字符串的json值,需要手动去除双引号 33 | data = strings.TrimSuffix(data, "\"") 34 | data = strings.TrimPrefix(data, "\"") 35 | p.Data = data 36 | 37 | // 允许""表示不使用proxy 38 | if !p.IsURL() && p.Data != "" { 39 | return ErrNotURL 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (p *JSONProxy) MarshalJSON() ([]byte, error) { 46 | if !p.IsURL() && p.Data != "" { 47 | return nil, ErrNotURL 48 | } 49 | 50 | return []byte("\"" + p.Data + "\""), nil 51 | } 52 | -------------------------------------------------------------------------------- /config/jsonproxy_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "testing" 4 | 5 | func TestIsURL(t *testing.T) { 6 | testData := []struct { 7 | data string 8 | res bool 9 | }{ 10 | { 11 | data: "http://example.com", 12 | res: true, 13 | }, 14 | { 15 | data: "https://example.com:8080", 16 | res: true, 17 | }, 18 | { 19 | data: "socks5://127.0.0.1:8000/", 20 | res: true, 21 | }, 22 | { 23 | data: "ftp://test.org/", 24 | res: false, 25 | }, 26 | { 27 | data: "127.0.0.1:1025/", 28 | res: false, 29 | }, 30 | { 31 | data: "https://socks5://www.test.com/", 32 | res: false, 33 | }, 34 | } 35 | 36 | for _, v := range testData { 37 | proxy := JSONProxy{v.data} 38 | if proxy.IsURL() != v.res { 39 | t.Errorf("failed with %s\n", v.data) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/ssr_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // ClientConfigGetter 获取ClientConfig 4 | type ClientConfigGetter interface { 5 | // LocalPort 获取本地监听端口 6 | LocalPort() string 7 | // LocalAddr 获取本地监听地址 8 | LocalAddr() string 9 | // FastOpen 是否使用fast-open 10 | FastOpen() bool 11 | // PidFilePath pidfile存放路径 12 | PidFilePath() string 13 | } 14 | 15 | // ClientConfigSetter 设置ClientConfig 16 | type ClientConfigSetter interface { 17 | // SetLocalPort 设置本地监听端口 18 | SetLocalPort(port string) error 19 | // SetLocalAddr 设置本地监听地址 20 | SetLocalAddr(addr string) error 21 | // SetFastOpen 设置是否使用fast-open 22 | SetFastOpen(isFOP bool) 23 | // SetPidFilePath 设置pidfile存放路径 24 | SetPidFilePath(path string) error 25 | } 26 | 27 | // ssr client配置接口 28 | type ClientConfig interface { 29 | ClientConfigGetter 30 | ClientConfigSetter 31 | // Load 从配置文件解析config 32 | Load(path string) error 33 | // Store 保存到配置文件 34 | Store(path string) error 35 | } 36 | 37 | // ClientConfigMaker 产生新的config对象,所有选项使用初始默认值 38 | type ClientConfigMaker func() ClientConfig 39 | -------------------------------------------------------------------------------- /crawler/crawler.go: -------------------------------------------------------------------------------- 1 | package crawler 2 | 3 | import ( 4 | "compress/gzip" 5 | "errors" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/PuerkitoBio/goquery" 11 | 12 | "schannel-qt5/parser" 13 | "schannel-qt5/urls" 14 | ) 15 | 16 | // GetCSRFToken 返回本次回话所使用的CSRFToken 17 | func GetCSRFToken(proxy string) (string, []*http.Cookie, error) { 18 | client, err := GenClientWithProxy(proxy) 19 | if err != nil { 20 | return "", nil, err 21 | } 22 | 23 | request, err := http.NewRequest("GET", urls.AccountPath, nil) 24 | if err != nil { 25 | return "", nil, err 26 | } 27 | SetRequestHeader(request, nil, urls.RootPath, "gzip") 28 | 29 | resp, err := client.Do(request) 30 | if err != nil { 31 | return "", nil, err 32 | } 33 | defer resp.Body.Close() 34 | 35 | htmlReader, err := gzip.NewReader(resp.Body) 36 | if err != nil { 37 | return "", nil, err 38 | } 39 | defer htmlReader.Close() 40 | 41 | dom, err := goquery.NewDocumentFromReader(htmlReader) 42 | if err != nil { 43 | return "", nil, err 44 | } 45 | 46 | CSRFToken, exists := dom.Find("input[type='hidden'][name='token']").Eq(0).Attr("value") 47 | if !exists { 48 | return "", nil, errors.New("CSRFToken doesn't exist") 49 | } 50 | 51 | u2, _ := url.Parse(urls.RootPath) 52 | return CSRFToken, client.Jar.Cookies(u2), nil 53 | } 54 | 55 | // GetAuth 登录schannel并返回登陆成功后获得的cookies 56 | // 这些cookies在后续的页面访问中需要使用 57 | func GetAuth(user, passwd, proxy string) ([]*http.Cookie, error) { 58 | client, err := GenClientWithProxy(proxy) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | CSRFToken, session, err := GetCSRFToken(proxy) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | form := url.Values{} 69 | form.Set("token", CSRFToken) 70 | form.Set("username", user) 71 | form.Set("password", passwd) 72 | getLogin, err := http.NewRequest("POST", urls.LoginPath, strings.NewReader(form.Encode())) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | SetRequestHeader(getLogin, session, urls.AccountPath, "gzip") 78 | getLogin.Header.Set("content-type", "application/x-www-form-urlencoded") 79 | 80 | resp, err := client.Do(getLogin) 81 | if err != nil { 82 | return nil, err 83 | } 84 | defer resp.Body.Close() 85 | 86 | // 验证登录是否成功,如果incorrect的值为“true”,则登录失败 87 | incorrect := resp.Request.FormValue("incorrect") 88 | if incorrect == "true" { 89 | return nil, errors.New("登录验证失败") 90 | } 91 | 92 | u2, _ := url.Parse(urls.RootPath) 93 | cookies := make([]*http.Cookie, 0, 2) 94 | cookies = append(cookies, client.Jar.Cookies(u2)...) 95 | // 添加cfduid会话cookie 96 | for _, c := range session { 97 | if c.Name == "__cfduid" { 98 | cookies = append(cookies, c) 99 | } 100 | } 101 | 102 | return cookies, nil 103 | } 104 | 105 | // GetServiceHTML 获取所有已购买服务的状态信息,包括详细页面的地址 106 | func GetServiceHTML(cookies []*http.Cookie, proxy string) (string, error) { 107 | return getPage(urls.ServiceListPath, urls.AccountPath, cookies, proxy) 108 | } 109 | 110 | // GetInvoiceHTML 获取账单页面的HTML,包含未付款和已付款账单 111 | // 未付款账单显示在最前列 112 | // 现只支持获取第一页 113 | func GetInvoiceHTML(cookies []*http.Cookie, proxy string) (string, error) { 114 | return getPage(urls.InvoicePath, urls.AccountPath, cookies, proxy) 115 | } 116 | 117 | // GetSSRInfoHTML 获取服务详细信息页面的HTML,包含使用情况和节点信息 118 | func GetSSRInfoHTML(service *parser.Service, cookies []*http.Cookie, proxy string) (string, error) { 119 | return getPage(service.Link, urls.ServiceListPath, cookies, proxy) 120 | } 121 | 122 | // GetInvoiceInfoHTML 获取账单详情页面内容 123 | func GetInvoiceInfoHTML(invoice *parser.Invoice, cookies []*http.Cookie, proxy string) (string, error) { 124 | return getPage(invoice.Link, urls.InvoicePath, cookies, proxy) 125 | } 126 | -------------------------------------------------------------------------------- /crawler/utils.go: -------------------------------------------------------------------------------- 1 | package crawler 2 | 3 | import ( 4 | "compress/gzip" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/cookiejar" 8 | "net/url" 9 | 10 | "golang.org/x/net/publicsuffix" 11 | ) 12 | 13 | const ( 14 | // UA is Chrome's User-Agent 15 | UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36" 16 | // AcceptType is Chrome's Accept-Type 17 | AcceptType = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" 18 | ) 19 | 20 | // GenClientWithProxy 生成http.Client,并设置代理为proxy指定的代理服务器 21 | // proxy url支持http,https和socks5协议 22 | func GenClientWithProxy(proxy string) (*http.Client, error) { 23 | client := new(http.Client) 24 | // all cookieJar users should use "golang.org/x/net/publicsuffix" 25 | jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) 26 | if err != nil { 27 | return nil, err 28 | } 29 | client.Jar = jar 30 | 31 | if proxy != "" { 32 | proxyURL, err := url.Parse(proxy) 33 | if err != nil { 34 | return nil, err 35 | } 36 | // 设置proxy 37 | client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)} 38 | } 39 | 40 | return client, nil 41 | } 42 | 43 | // SetRequestHeader 设置请求头信息 44 | // cookies为nil时将被忽略,referer和compress为“”时同样被忽略 45 | func SetRequestHeader(request *http.Request, cookies []*http.Cookie, referer, compress string) { 46 | request.Header.Set("accept", AcceptType) 47 | if compress != "" { 48 | request.Header.Set("accept-encoding", compress) 49 | } 50 | if referer != "" { 51 | request.Header.Set("referer", referer) 52 | } 53 | request.Header.Set("user-agent", UA) 54 | 55 | for _, c := range cookies { 56 | request.AddCookie(c) 57 | } 58 | } 59 | 60 | // getPage 获取url指定的各种账户管理页面信息, cookies用于身份认证 61 | func getPage(url, referer string, cookies []*http.Cookie, proxy string) (string, error) { 62 | client, err := GenClientWithProxy(proxy) 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | request, err := http.NewRequest("GET", url, nil) 68 | if err != nil { 69 | return "", err 70 | } 71 | SetRequestHeader(request, cookies, referer, "gzip") 72 | 73 | resp, err := client.Do(request) 74 | if err != nil { 75 | return "", err 76 | } 77 | defer resp.Body.Close() 78 | 79 | htmlReader, err := gzip.NewReader(resp.Body) 80 | if err != nil { 81 | return "", err 82 | } 83 | defer htmlReader.Close() 84 | 85 | data, err := ioutil.ReadAll(htmlReader) 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | return string(data), nil 91 | } 92 | -------------------------------------------------------------------------------- /geoip/geoip.go: -------------------------------------------------------------------------------- 1 | package geoip 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "os" 7 | "path/filepath" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/oschwald/geoip2-golang" 12 | ) 13 | 14 | const ( 15 | // GeoIP Database的存放路径 16 | geoIPSavePath = ".local/share/data/schannel-qt5/GeoIP" 17 | // GeoIP Database下载地址 18 | DownloadPath = "https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz" 19 | // 数据库文件名 20 | DatabaseName = "GeoLite2-City.mmdb" 21 | ) 22 | 23 | // GetGeoIPSavePath 返回完整的数据库路径,默认在$HOME/.local/share/data/schannel-qt5/GeoIP下 24 | func GetGeoIPSavePath() (string, error) { 25 | home, err := os.UserHomeDir() 26 | if err != nil { 27 | return "", errors.New("cannot find user home dir") 28 | } 29 | 30 | return filepath.Join(home, geoIPSavePath), nil 31 | } 32 | 33 | // getRecord 根据ip返回geoIP查询结果 34 | func getRecord(ip string) (*geoip2.City, error) { 35 | dbDir, err := GetGeoIPSavePath() 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | dbPath := filepath.Join(dbDir, DatabaseName) 41 | db, err := geoip2.Open(dbPath) 42 | if err != nil { 43 | return nil, err 44 | } 45 | defer db.Close() 46 | 47 | ipAddr := net.ParseIP(ip) 48 | record, err := db.City(ipAddr) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return record, nil 54 | } 55 | 56 | // GetCountryCity 返回ip的国家/地区和城市名称 57 | // lang为语言名称,e.g. "zh-CN", "en" 58 | func GetCountryCity(ip string, lang string) (string, string, error) { 59 | record, err := getRecord(ip) 60 | if err != nil { 61 | return "", "", err 62 | } 63 | 64 | var country, city string 65 | country = record.Country.Names[lang] 66 | city, exists := record.City.Names[lang] 67 | if !exists { 68 | for _, sub := range record.Subdivisions { 69 | city, exists = sub.Names[lang] 70 | if exists { 71 | break 72 | } 73 | } 74 | } 75 | 76 | return country, city, nil 77 | } 78 | 79 | // GetCountryISOCode 获取国家代码 80 | func GetCountryISOCode(ip string) (string, error) { 81 | record, err := getRecord(ip) 82 | if err != nil { 83 | return "", nil 84 | } 85 | 86 | return record.Country.IsoCode, nil 87 | } 88 | 89 | // IsGeoIPOutdated 判断数据库文件是否过期 90 | // 文件不存在时也会返回true 91 | func IsGeoIPOutdated(expires time.Duration) bool { 92 | dbPath, err := GetGeoIPSavePath() 93 | if err != nil { 94 | return true 95 | } 96 | 97 | stat, err := os.Stat(dbPath) 98 | if err != nil { 99 | return true 100 | } 101 | 102 | cTime := timespec2Time(stat.Sys().(*syscall.Stat_t).Ctim) 103 | now := time.Now() 104 | return now.Sub(cTime) >= expires 105 | } 106 | 107 | func timespec2Time(ts syscall.Timespec) time.Time { 108 | return time.Unix(int64(ts.Sec), int64(ts.Nsec)) 109 | } 110 | -------------------------------------------------------------------------------- /image/Removefixed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /image/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apocelipes/schannel-qt5/9beddec07645de2ff8d2e487bf0302e25a67a93f/image/close.png -------------------------------------------------------------------------------- /image/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /image/ic_copy_link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /image/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/astaxie/beego/orm" 8 | _ "github.com/mattn/go-sqlite3" 9 | "github.com/therecipe/qt/core" 10 | std_widgets "github.com/therecipe/qt/widgets" 11 | 12 | "schannel-qt5/config" 13 | "schannel-qt5/models" 14 | _ "schannel-qt5/pyclient" 15 | "schannel-qt5/ssr" 16 | "schannel-qt5/widgets" 17 | ) 18 | 19 | const ( 20 | // 日志主前缀 21 | prefix = "schannel-qt5: " 22 | ) 23 | 24 | func init() { 25 | dbPath, err := models.GetDBPath() 26 | if err != nil { 27 | panic(err) 28 | } 29 | orm.Debug = false 30 | orm.RegisterDataBase("default", "sqlite3", dbPath) 31 | err = orm.RunSyncdb("default", false, false) 32 | if err != nil { 33 | panic(err) 34 | } 35 | } 36 | 37 | func main() { 38 | app := std_widgets.NewQApplication(len(os.Args), os.Args) 39 | app.SetAttribute(core.Qt__AA_EnableHighDpiScaling, true) 40 | 41 | // 初始化用户配置 42 | conf := &config.UserConfig{} 43 | conf.SSRClientConfig = ssr.NewClientConfig("python") 44 | err := conf.LoadConfig() 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | var logger *log.Logger 50 | if !conf.LogFile.IsEmpty() { 51 | logPath, err := conf.LogFile.AbsPath() 52 | if err != nil { 53 | panic(err) 54 | } 55 | logFile, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0664) 56 | if err != nil { 57 | panic(err) 58 | } 59 | defer logFile.Close() 60 | logger = log.New(logFile, prefix, log.LstdFlags|log.Lshortfile) 61 | } else { 62 | // 未指定logfile时使用stdout替代 63 | logger = log.New(os.Stdout, prefix, log.LstdFlags|log.Lshortfile) 64 | } 65 | 66 | // 获取用户数据库连接 67 | db := orm.NewOrm() 68 | 69 | // 初始化GUI 70 | mainWindow := widgets.NewMainWidget2(conf, logger, db) 71 | mainWindow.Show() 72 | 73 | app.Exec() 74 | } 75 | -------------------------------------------------------------------------------- /models/encrypt.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/sha256" 8 | "encoding/base64" 9 | ) 10 | 11 | // genKey 根据用户名生成key 12 | func genKey(user string) []byte { 13 | salt := user[:len(user)/2] + "models" 14 | data := salt[:len(salt)/2] + user + salt[len(salt)/2:] 15 | hash := sha256.New() 16 | return hash.Sum([]byte(data))[:aes.BlockSize] 17 | } 18 | 19 | // encryptPassword 加密用户名密码,返回加密后的数据 20 | func encryptPassword(user string, password string) (string, error) { 21 | key := genKey(user) 22 | block, err := aes.NewCipher(key) 23 | if err != nil { 24 | return "", err 25 | } 26 | 27 | origData := PKCS5Padding([]byte(password), block.BlockSize()) 28 | blockMode := cipher.NewCBCEncrypter(block, key) 29 | crypted := make([]byte, len(origData)) 30 | blockMode.CryptBlocks(crypted, origData) 31 | res := base64.StdEncoding.EncodeToString(crypted) 32 | return res, nil 33 | } 34 | 35 | // decryptPassword 返回解密后的信息 36 | func decryptPassword(user string, crypted string) (string, error) { 37 | key := genKey(user) 38 | block, err := aes.NewCipher(key) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | blockMode := cipher.NewCBCDecrypter(block, key) 44 | unbase, err := base64.StdEncoding.DecodeString(crypted) 45 | if err != nil { 46 | return "", err 47 | } 48 | 49 | n := len(unbase) 50 | origData := make([]byte, n) 51 | blockMode.CryptBlocks(origData, unbase) 52 | origData = PKCS5UnPadding(origData) 53 | return string(origData), nil 54 | } 55 | 56 | // PKCS5Padding 将数据填充至合适的大小,以便加密算法处理 57 | func PKCS5Padding(origData []byte, blockSize int) []byte { 58 | padding := blockSize - len(origData)%blockSize 59 | padText := bytes.Repeat([]byte{byte(padding)}, padding) 60 | return append(origData, padText...) 61 | } 62 | 63 | // PKCS5UnPadding 去除填充 64 | func PKCS5UnPadding(origData []byte) []byte { 65 | length := len(origData) 66 | // 去掉最后一个字节 unpadding 次 67 | unpadding := int(origData[length-1]) 68 | return origData[:(length - unpadding)] 69 | } 70 | -------------------------------------------------------------------------------- /models/encrypt_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "testing" 4 | 5 | func TestEncryptDecrypt(t *testing.T) { 6 | testData := []*struct { 7 | user string 8 | password string 9 | }{ 10 | { 11 | user: "test", 12 | password: "abcdefg123", 13 | }, 14 | { 15 | user: "example user", 16 | password: "foratest1990812", 17 | }, 18 | { 19 | user: "用户A1@", 20 | password: "worldHello17.,", 21 | }, 22 | { 23 | user: "sample1", 24 | password: genPassword(), 25 | }, 26 | } 27 | 28 | for _, v := range testData { 29 | crypted, err := encryptPassword(v.user, v.password) 30 | if err != nil { 31 | t.Errorf("encrypto: %v\n", err) 32 | } 33 | password, err := decryptPassword(v.user, crypted) 34 | if err != nil { 35 | t.Errorf("decrypto: %v\n", err) 36 | } 37 | if password != v.password { 38 | format := "password not equal, old: %s, new: %s\n" 39 | t.Errorf(format, v.password, password) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /models/used_amount.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/astaxie/beego/orm" 7 | 8 | "schannel-qt5/parser" 9 | ) 10 | 11 | // 用户的每日的流量使用记录,以service进行区分 12 | type UsedAmount struct { 13 | Id int `orm:"auto"` 14 | Service string `orm:"size(50)"` 15 | Total int 16 | Upload int 17 | Download int 18 | Date time.Time `orm:"type(date)"` 19 | User *User `orm:"rel(fk);on_delete(cascade)"` 20 | } 21 | 22 | // 设置数据库时区为UTC 23 | func init() { 24 | orm.RegisterModel(&UsedAmount{}) 25 | orm.DefaultTimeLoc = time.UTC 26 | } 27 | 28 | // SetUsedAmount 根据sevice插入使用量信息. 29 | // 若date已经存在,则更新数据 30 | func SetUsedAmount(db orm.Ormer, 31 | user string, 32 | service *parser.Service, 33 | total, upload, download int, 34 | date time.Time) error { 35 | amount := &UsedAmount{ 36 | Service: service.Name, 37 | Total: total, 38 | Upload: upload, 39 | Download: download, 40 | Date: date, 41 | User: &User{ 42 | Name: user, 43 | }, 44 | } 45 | // 用户不存在无法insert 46 | err := db.QueryTable(amount.User).Filter("Name", user).One(amount.User) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | cond := orm.NewCondition() 52 | cond = cond.And("Date", amount.Date). 53 | And("Service", amount.Service). 54 | And("User__Name", amount.User.Name) 55 | if db.QueryTable(amount).SetCond(cond).Exist() { 56 | db.QueryTable(amount).SetCond(cond).Update(orm.Params{ 57 | "Upload": amount.Upload, 58 | "Download": amount.Download, 59 | }) 60 | return nil 61 | } 62 | 63 | if _, err := db.Insert(amount); err != nil { 64 | return err 65 | } 66 | 67 | return nil 68 | } 69 | 70 | const ( 71 | // 需要获取的天数 72 | maxDays = 5 73 | ) 74 | 75 | // GetRecentUsedAmount 返回date开始最近maxDays天的使用量数据 76 | // 如果数量不足5天,则以最早一天的数据进行复制补足 77 | func GetRecentUsedAmount(db orm.Ormer, 78 | user, service string, 79 | date time.Time) ([]*UsedAmount, error) { 80 | amounts := make([]*UsedAmount, 0) 81 | cond := orm.NewCondition() 82 | cond = cond.And("User__Name", user). 83 | And("Service", service). 84 | And("Date__lte", date) 85 | n, err := db.QueryTable(&UsedAmount{}). 86 | SetCond(cond). 87 | OrderBy("-Date"). 88 | Limit(maxDays).All(&amounts) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | // 将时间转回本地时区 94 | for _, v := range amounts { 95 | v.Date = v.Date.In(time.Local) 96 | } 97 | 98 | if n < maxDays { 99 | amounts = paddingDate(amounts, int(n), maxDays) 100 | } 101 | 102 | return amounts, nil 103 | } 104 | 105 | // paddingDate 填充UsedAmount至长度为max 106 | func paddingDate(amounts []*UsedAmount, length, max int) []*UsedAmount { 107 | origin := amounts[length-1] 108 | // 需要减去的天数 109 | dayCount := 1 110 | for i := length; i < max; i++ { 111 | duplicate := &UsedAmount{ 112 | Service: origin.Service, 113 | Total: origin.Total, 114 | Upload: origin.Upload, 115 | Download: origin.Download, 116 | Date: origin.Date.Add(time.Duration(-dayCount) * time.Hour * 24), 117 | User: origin.User, 118 | } 119 | dayCount++ 120 | amounts = append(amounts, duplicate) 121 | } 122 | 123 | return amounts 124 | } 125 | -------------------------------------------------------------------------------- /models/users.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/astaxie/beego/orm" 8 | _ "github.com/mattn/go-sqlite3" 9 | 10 | "schannel-qt5/config" 11 | ) 12 | 13 | const ( 14 | // 数据库存放路径 15 | databasePath = ".local/share/schannel-users.db" 16 | ) 17 | 18 | // 注册模型 19 | func init() { 20 | orm.RegisterModel(&User{}) 21 | } 22 | 23 | // GetDBPath 获取数据库存放路径 24 | func GetDBPath() (string, error) { 25 | home, err := os.UserHomeDir() 26 | if err != nil { 27 | return "", config.ErrHOME 28 | } 29 | 30 | return filepath.Join(home, databasePath), nil 31 | } 32 | 33 | // User 用户表,将和使用量表关联 34 | type User struct { 35 | Name string `orm:"size(100);pk"` 36 | Passwd string `orm:"size(100);null"` 37 | } 38 | 39 | // GetUserPassword 获取用户名以及密码 40 | func GetUserPassword(db orm.Ormer, user string) (*User, error) { 41 | u := &User{Name: user} 42 | 43 | if err := db.Read(u); err != nil { 44 | return nil, err 45 | } 46 | 47 | // 密码解密 48 | if u.Passwd != "" { 49 | data, err := decryptPassword(u.Name, u.Passwd) 50 | if err != nil { 51 | return nil, err 52 | } 53 | u.Passwd = data 54 | } 55 | 56 | return u, nil 57 | } 58 | 59 | // SetUserPassword 将用户名密码保存 60 | // password为nil表示不记住密码 61 | func SetUserPassword(db orm.Ormer, user string, password string) error { 62 | u := &User{ 63 | Name: user, 64 | Passwd: password, 65 | } 66 | 67 | if u.Passwd != "" { 68 | data, err := encryptPassword(u.Name, u.Passwd) 69 | if err != nil { 70 | return err 71 | } 72 | u.Passwd = data 73 | } 74 | 75 | if db.QueryTable(u).Filter("Name", u.Name).Exist() { 76 | old := &User{Name: u.Name} 77 | db.QueryTable(old).Filter("Name", old.Name).One(old) 78 | // 和旧值一样,不更新,返回nil 79 | if u.Passwd == old.Passwd { 80 | return nil 81 | } 82 | _, err := db.QueryTable(u).Filter("Name", u.Name).Update(orm.Params{ 83 | "Passwd": u.Passwd, 84 | }) 85 | if err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | 91 | if _, err := db.Insert(u); err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // GetAllUsers 返回所有user,包括未 99 | func GetAllUsers(db orm.Ormer) ([]*User, error) { 100 | users := make([]*User, 0) 101 | 102 | if _, err := db.QueryTable(&User{}).All(&users); err != nil { 103 | return nil, err 104 | } 105 | 106 | return users, nil 107 | } 108 | 109 | // DelPassword 将指定user的password设置为null 110 | func DelPassword(db orm.Ormer, user string) error { 111 | u := &User{Name: user} 112 | 113 | _, err := db.QueryTable(u).Filter("Name", u.Name).Update(orm.Params{ 114 | "Passwd": "", 115 | }) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | 123 | // DelUser 删除名字与name相同的User,同时会删除UserAmount记录 124 | func DelUser(db orm.Ormer, name string) error { 125 | user := &User{Name: name} 126 | _, err := db.QueryTable(user).Filter("Name", user.Name).Delete() 127 | return err 128 | } 129 | -------------------------------------------------------------------------------- /models/users_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "crypto/rand" 7 | "encoding/hex" 8 | "os" 9 | 10 | "github.com/astaxie/beego/orm" 11 | _ "github.com/mattn/go-sqlite3" 12 | ) 13 | 14 | const ( 15 | // 测试数据库存储路径 16 | dbPath = "/tmp/db_users_test.db" 17 | ) 18 | 19 | func TestMain(m *testing.M) { 20 | orm.Debug = true 21 | orm.RegisterDataBase("default", "sqlite3", dbPath) 22 | orm.RegisterDataBase("testAmount", "sqlite3", amountPath) 23 | os.Exit(m.Run()) 24 | } 25 | 26 | // initUserDB 初始化测试数据 27 | func initUserDB(t *testing.T) (orm.Ormer, []*User) { 28 | os.Truncate(dbPath, 0) 29 | err := orm.RunSyncdb("default", false, true) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | users := []*User{ 35 | { 36 | Name: "test@test.com", 37 | Passwd: "", 38 | }, 39 | { 40 | Name: "test@example.com", 41 | Passwd: genPassword(), 42 | }, 43 | { 44 | Name: "example", 45 | Passwd: genPassword(), 46 | }, 47 | } 48 | 49 | db := orm.NewOrm() 50 | for _, v := range users { 51 | if err := SetUserPassword(db, v.Name, v.Passwd); err != nil { 52 | t.Fatalf("initdb error: %v\n", err) 53 | } 54 | } 55 | 56 | return db, users 57 | } 58 | 59 | // genPassword 生成随机密码 60 | func genPassword() string { 61 | origData := make([]byte, 16) 62 | n, err := rand.Read(origData) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | pw := make([]byte, hex.EncodedLen(n)) 68 | hex.Encode(pw, origData[:n]) 69 | return string(pw) 70 | } 71 | 72 | func TestGetUserPassword(t *testing.T) { 73 | db, users := initUserDB(t) 74 | 75 | for _, v := range users { 76 | user, err := GetUserPassword(db, v.Name) 77 | if err != nil { 78 | t.Errorf("get user: %s error: %v\n", v.Name, err) 79 | } 80 | if user.Passwd != v.Passwd { 81 | format := "get user: %s password different\nhave: %v\n\twant: %v\n" 82 | t.Errorf(format, v.Name, user.Passwd, v.Passwd) 83 | } 84 | } 85 | } 86 | 87 | func TestSetUserPassword(t *testing.T) { 88 | db, _ := initUserDB(t) 89 | 90 | testData := []*struct { 91 | // 用户对象 92 | u *User 93 | // 是否insert成功 94 | inserted bool 95 | }{ 96 | { 97 | u: &User{ 98 | Name: "example", 99 | Passwd: "", 100 | }, 101 | inserted: true, 102 | }, 103 | { 104 | u: &User{ 105 | Name: "a", 106 | Passwd: genPassword(), 107 | }, 108 | inserted: true, 109 | }, 110 | { 111 | u: &User{ 112 | Name: "b", 113 | Passwd: "", 114 | }, 115 | inserted: true, 116 | }, 117 | } 118 | 119 | for _, v := range testData { 120 | err := SetUserPassword(db, v.u.Name, v.u.Passwd) 121 | if (err == nil) != v.inserted { 122 | t.Errorf("set user: %v error: %v\n", v.u, err) 123 | } 124 | } 125 | } 126 | 127 | func TestGetAllUsers(t *testing.T) { 128 | db, users := initUserDB(t) 129 | 130 | u, err := GetAllUsers(db) 131 | if err != nil { 132 | t.Errorf("get all users error: %v\n", err) 133 | } 134 | 135 | // 取得的数据量是否相同 136 | if len(u) != len(users) { 137 | format := "length error: have: %v\n\twant: %v\n" 138 | t.Errorf(format, len(u), len(users)) 139 | } 140 | } 141 | 142 | func TestDelPassword(t *testing.T) { 143 | db, users := initUserDB(t) 144 | 145 | for _, v := range users { 146 | if v.Passwd != "" { 147 | err := DelPassword(db, v.Name) 148 | if err != nil { 149 | format := "del %s password error: %v\n" 150 | t.Errorf(format, v.Name, err) 151 | } 152 | 153 | // 查看是否已将密码设置为null 154 | u, err := GetUserPassword(db, v.Name) 155 | if err != nil { 156 | format := "del %s password error: %v\n" 157 | t.Errorf(format, v.Name, err) 158 | } 159 | if u.Passwd != "" { 160 | t.Errorf("del password failed: %v\n", u.Passwd) 161 | } 162 | } 163 | } 164 | } 165 | 166 | func TestGetDBPath(t *testing.T) { 167 | testData := []*struct { 168 | // 设置环境变量HOME的值 169 | home string 170 | res string 171 | }{ 172 | { 173 | home: "/home/test", 174 | res: "/home/test/" + databasePath, 175 | }, 176 | { 177 | home: "/home/user1/", 178 | res: "/home/user1/" + databasePath, 179 | }, 180 | { 181 | home: "/home/用户/", 182 | res: "/home/用户/" + databasePath, 183 | }, 184 | } 185 | 186 | for _, v := range testData { 187 | os.Clearenv() 188 | err := os.Setenv("HOME", v.home) 189 | if err != nil { 190 | t.Fatalf("无法设置$HOME: %v\n", err) 191 | } 192 | res, err := GetDBPath() 193 | if err != nil { 194 | t.Fatalf("获取DB Path错误:%v\n", err) 195 | } 196 | if v.res != res { 197 | format := "不正确的DB Path:\n\twant: %s\n\thave: %v\n" 198 | t.Errorf(format, v.res, res) 199 | } 200 | } 201 | } 202 | 203 | func TestDelUser(t *testing.T) { 204 | db, users := initUserDB(t) 205 | for _, v := range users { 206 | err := DelUser(db, v.Name) 207 | if err != nil { 208 | format := "DelUser error: %+v\n" 209 | t.Errorf(format, v) 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /parser/invoice.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // PaymentState 表示账单的付款状态 8 | type PaymentState int 9 | 10 | const ( 11 | // NeedPay 未付款 12 | NeedPay PaymentState = iota 13 | // FinishedPay 已付款 14 | FinishedPay 15 | ) 16 | 17 | // Invoice 账单信息 18 | type Invoice struct { 19 | // 账单编号 20 | Number string 21 | // 账单链接 22 | Link string 23 | // 账单开始日期 24 | StartDate time.Time 25 | // 账单结束日期 26 | ExpireDate time.Time 27 | // 支付金额 28 | Payment int64 29 | // 付款状态 30 | State PaymentState 31 | } 32 | 33 | // GetStatus 返回账单的状态 34 | // 未付款会返回false 35 | // time.Now()超过ExpireDate将视为账单过期 36 | func (i *Invoice) GetStatus() (string, bool) { 37 | msg := "" 38 | flag := false 39 | 40 | if i.State == NeedPay { 41 | msg += "需要付款" 42 | } else if i.State == FinishedPay { 43 | msg += "无需付款" 44 | flag = true 45 | } 46 | 47 | current := GetCurrentDay() 48 | if current.After(i.ExpireDate) { 49 | msg += ",账单过期" 50 | } 51 | 52 | return msg, flag 53 | } 54 | 55 | // GetCurrentDay 返回当前的时间,精确到day 56 | func GetCurrentDay() time.Time { 57 | timeFormat := "2006-01-02" 58 | dayText := time.Now().Format(timeFormat) 59 | day, _ := time.ParseInLocation(timeFormat, dayText, time.Local) 60 | return day 61 | } 62 | -------------------------------------------------------------------------------- /parser/invoice_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "time" 7 | ) 8 | 9 | func TestInvoiceGetStatus(t *testing.T) { 10 | // 与过期时间比较 11 | now := GetCurrentDay() 12 | 13 | testData := []*struct { 14 | // 账单信息 15 | i *Invoice 16 | // err string 17 | info string 18 | // 是否付款 19 | isPayed bool 20 | }{ 21 | { 22 | i: &Invoice{ 23 | // 设置未过期时间 24 | ExpireDate: now.Add(24 * time.Hour), 25 | State: FinishedPay, 26 | }, 27 | info: "无需付款", 28 | isPayed: true, 29 | }, 30 | { 31 | i: &Invoice{ 32 | // 测试当前时间 33 | ExpireDate: now, 34 | State: FinishedPay, 35 | }, 36 | info: "无需付款", 37 | isPayed: true, 38 | }, 39 | { 40 | i: &Invoice{ 41 | // 设置已过期时间 42 | ExpireDate: now.Add(-24 * time.Hour), 43 | State: FinishedPay, 44 | }, 45 | info: "无需付款,账单过期", 46 | isPayed: true, 47 | }, 48 | { 49 | i: &Invoice{ 50 | ExpireDate: now.Add(24 * time.Hour), 51 | State: NeedPay, 52 | }, 53 | info: "需要付款", 54 | isPayed: false, 55 | }, 56 | { 57 | i: &Invoice{ 58 | ExpireDate: now.Add(-24 * time.Hour), 59 | State: NeedPay, 60 | }, 61 | info: "需要付款,账单过期", 62 | isPayed: false, 63 | }, 64 | } 65 | 66 | for _, v := range testData { 67 | info, state := v.i.GetStatus() 68 | if info != v.info { 69 | format := "wrong info:\n\twant :%s\n\thave: %s\n%v\n" 70 | t.Errorf(format, v.info, info, v) 71 | } 72 | if state != v.isPayed { 73 | format := "wrong payment state:\n\twant: %v\n\thave: %v\n%v\n" 74 | t.Errorf(format, v.isPayed, state, v) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/PuerkitoBio/goquery" 10 | 11 | "schannel-qt5/urls" 12 | ) 13 | 14 | var ( 15 | getDataInfo = regexp.MustCompile(`.+ \((.+(?:GB|MB|KB))\)`) 16 | getTotal = regexp.MustCompile(`.+ \(流量:(.+(?:GB|MB|KB))\)`) 17 | ) 18 | 19 | func isServiceValid(stat string) bool { 20 | return stat == "有效的" 21 | } 22 | 23 | // GetService 返回所有可用的套餐的信息 24 | func GetService(data string) []*Service { 25 | res := make([]*Service, 0) 26 | 27 | table, _ := goquery.NewDocumentFromReader(strings.NewReader(data)) 28 | // id为tableServicesList的table里有所有的服务信息 29 | table.Find("#tableServicesList tbody tr").Each(func(i int, s *goquery.Selection) { 30 | tds := s.Find("td") 31 | if !isServiceValid(tds.Eq(3).Text()) { 32 | // 滤除已暂停的服务 33 | return 34 | } 35 | 36 | ser := new(Service) 37 | // 第一列是服务名称 38 | ser.Name = tds.Eq(0).Text() 39 | // 第二列是详细信息页面链接和价格 40 | link, _ := tds.Eq(1).Find("a").Attr("href") 41 | ser.Link = urls.RootPath + link 42 | ser.Price = tds.Eq(1).Text() 43 | // 第三列是服务到期时间 44 | expire := tds.Eq(2).Find("span").Text() 45 | ser.Expires, _ = time.ParseInLocation("2006-01-02", expire, time.Local) 46 | // 第四列是服务状态信息 47 | ser.State = tds.Eq(3).Text() 48 | res = append(res, ser) 49 | }) 50 | 51 | return res 52 | } 53 | 54 | // GetSSRInfo 获取套餐的详细使用信息 55 | func GetSSRInfo(data string, ser *Service) *SSRInfo { 56 | res := NewSSRInfo(ser) 57 | 58 | dom, _ := goquery.NewDocumentFromReader(strings.NewReader(data)) 59 | // 信息包含在第2-4个section.panel里 60 | sections := dom.Find("section.panel") 61 | 62 | // 第2个section的table是端口和密码 63 | serverInfo := sections.Eq(1) 64 | portAndPasswd := serverInfo.Find("table").First() 65 | res.Port, _ = strconv.ParseInt(portAndPasswd.Find("tbody tr").Find("td").First().Text(), 10, 64) 66 | res.Passwd = portAndPasswd.Find("tbody tr").Find("td").Eq(1).Text() 67 | 68 | // 第3个section的header里是套餐总量 69 | usageInfo := sections.Eq(2) 70 | total := usageInfo.Find("header").Text() 71 | total = strings.TrimSpace(total) 72 | res.TotalData = getTotal.FindStringSubmatch(total)[1] 73 | 74 | usage := usageInfo.Find("#plugin-usage").Find("p") 75 | res.UsedData = getDataInfo.FindStringSubmatch(usage.First().Text())[1] 76 | res.Upload = getDataInfo.FindStringSubmatch(usage.Eq(1).Text())[1] 77 | res.Download = getDataInfo.FindStringSubmatch(usage.Eq(2).Text())[1] 78 | 79 | // 第4个section的table是节点信息表 80 | sections.Eq(3).Find("table"). 81 | Find("tbody tr").Each(func(i int, s *goquery.Selection) { 82 | node := new(SSRNode) 83 | tds := s.Children() 84 | 85 | node.NodeName = tds.First().Text() 86 | node.Type = tds.Eq(1).Text() 87 | node.IP = tds.Eq(2).Text() 88 | node.Crypto = tds.Eq(3).Text() 89 | node.Proto = tds.Eq(4).Text() 90 | node.Minx = tds.Eq(5).Text() 91 | node.Port = res.Port 92 | node.Passwd = res.Passwd 93 | 94 | res.Nodes = append(res.Nodes, node) 95 | }) 96 | 97 | return res 98 | } 99 | 100 | // GetInvoices 返回所有账单信息 101 | func GetInvoices(data string) []*Invoice { 102 | invoiceList := make([]*Invoice, 0, 2) 103 | 104 | dom, _ := goquery.NewDocumentFromReader(strings.NewReader(data)) 105 | invoiceTable := dom.Find("#tableInvoicesList") 106 | 107 | invoiceTable.Find("tbody tr").Each(func(_ int, s *goquery.Selection) { 108 | invoice := new(Invoice) 109 | tds := s.Find("td") 110 | 111 | invoice.Number = tds.First().Text() 112 | 113 | startDate := tds.Eq(1).Find("span").Text() 114 | expireDate := tds.Eq(2).Find("span").Text() 115 | invoice.StartDate, _ = time.ParseInLocation("2006-01-02", startDate, time.Local) 116 | invoice.ExpireDate, _ = time.ParseInLocation("2006-01-02", expireDate, time.Local) 117 | 118 | payment, exists := tds.Eq(3).Attr("data-order") 119 | // 如果取不到,就用默认值0 120 | if exists { 121 | invoice.Payment, _ = strconv.ParseInt(payment, 10, 64) 122 | } 123 | 124 | if tds.Eq(4).Text() == "已付款" { 125 | invoice.State = FinishedPay 126 | } else if tds.Eq(4).Text() == "未付款" { 127 | invoice.State = NeedPay 128 | } 129 | 130 | link, _ := tds.Eq(5).Find("a").Attr("href") 131 | invoice.Link = urls.RootPath + link 132 | 133 | invoiceList = append(invoiceList, invoice) 134 | }) 135 | 136 | return invoiceList 137 | } 138 | 139 | // GetInvoiceDownloadURL 获取invoice下载地址 140 | func GetInvoiceDownloadURL(data string) string { 141 | dom, _ := goquery.NewDocumentFromReader(strings.NewReader(data)) 142 | downloadBtn := dom.Find("i.fa-download").Parent() 143 | downloadURL, exists := downloadBtn.Attr("href") 144 | if !exists { 145 | return "" 146 | } 147 | 148 | return urls.RootPath + downloadURL 149 | } 150 | 151 | // GetInvoiceViewHTML 处理invoice html生成用于QWebEngine展示的页面 152 | func GetInvoiceViewHTML(data string) string { 153 | dom, _ := goquery.NewDocumentFromReader(strings.NewReader(data)) 154 | 155 | // 修改css链接为绝对路径 156 | dom.Find("head link").Each(func(_ int, link *goquery.Selection) { 157 | if val, exists := link.Attr("href"); exists { 158 | link.SetAttr("href", urls.RootPath+val[1:]) 159 | } 160 | }) 161 | 162 | dom.Find(".hidden-print").Each(func(_ int, s *goquery.Selection) { 163 | s.Remove() 164 | }) 165 | 166 | ret, _ := dom.Html() 167 | return ret 168 | } 169 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "io/ioutil" 7 | "os" 8 | "time" 9 | 10 | "schannel-qt5/urls" 11 | ) 12 | 13 | func TestGetTotal(t *testing.T) { 14 | testData := []*struct { 15 | data string 16 | // parse得到的值 17 | res string 18 | }{ 19 | { 20 | data: `使用报表 (流量:50GB)`, 21 | res: "50GB", 22 | }, 23 | { 24 | data: `使用报表 (流量:25.25GB)`, 25 | res: "25.25GB", 26 | }, 27 | } 28 | 29 | for _, v := range testData { 30 | res := getTotal.FindStringSubmatch(v.data)[1] 31 | if res != v.res { 32 | format := "regexp getTotal failed: %v\n\twant: %v\n\thave: %v\n" 33 | t.Errorf(format, v, v.res, res) 34 | } 35 | } 36 | } 37 | 38 | func TestGetDataInfo(t *testing.T) { 39 | testData := []*struct { 40 | data string 41 | // parse得到的值 42 | res string 43 | }{ 44 | { 45 | data: `已使用 (16.14GB)`, 46 | res: "16.14GB", 47 | }, 48 | { 49 | data: `上传 (14.66MB)`, 50 | res: "14.66MB", 51 | }, 52 | { 53 | data: `下载 (16.12GB)`, 54 | res: "16.12GB", 55 | }, 56 | { 57 | data: `下载 (5.74KB)`, 58 | res: "5.74KB", 59 | }, 60 | } 61 | 62 | for _, v := range testData { 63 | res := getDataInfo.FindStringSubmatch(v.data)[1] 64 | if res != v.res { 65 | format := "regexp getDataInfo failed: %s\n\twant: %s\n\thave: %s\n" 66 | t.Errorf(format, v.data, v.res, res) 67 | } 68 | } 69 | } 70 | 71 | func TestGetInvoice(t *testing.T) { 72 | // 应该被解析出来的信息 73 | correctRes := []Invoice{ 74 | { 75 | Number: "12345", 76 | Link: urls.RootPath + "test1", 77 | Payment: 10, 78 | State: NeedPay, 79 | }, { 80 | Number: "2345", 81 | Link: urls.RootPath + "test2", 82 | Payment: 10, 83 | State: FinishedPay, 84 | }, { 85 | Number: "345", 86 | Link: urls.RootPath + "test3", 87 | Payment: 10, 88 | State: FinishedPay, 89 | }, { 90 | Number: "4390", 91 | Link: urls.RootPath + "test4", 92 | Payment: 10, 93 | State: FinishedPay, 94 | }, 95 | } 96 | 97 | correctRes[0].StartDate, _ = time.ParseInLocation("2006-01-02", "2018-04-10", time.Local) 98 | correctRes[0].ExpireDate, _ = time.ParseInLocation("2006-01-02", "2018-04-11", time.Local) 99 | 100 | correctRes[1].StartDate, _ = time.ParseInLocation("2006-01-02", "2018-04-30", time.Local) 101 | correctRes[1].ExpireDate, _ = time.ParseInLocation("2006-01-02", "2018-04-30", time.Local) 102 | 103 | correctRes[2].StartDate, _ = time.ParseInLocation("2006-01-02", "2018-05-28", time.Local) 104 | correctRes[2].ExpireDate, _ = time.ParseInLocation("2006-01-02", "2018-05-29", time.Local) 105 | 106 | correctRes[3].StartDate, _ = time.ParseInLocation("2006-01-02", "2018-06-28", time.Local) 107 | correctRes[3].ExpireDate, _ = time.ParseInLocation("2006-01-02", "2018-06-29", time.Local) 108 | 109 | f, err := os.Open("testdata/invoice.html") 110 | if err != nil { 111 | t.Error(err) 112 | } 113 | defer f.Close() 114 | testData, err := ioutil.ReadAll(f) 115 | if err != nil { 116 | t.Error(err) 117 | } 118 | 119 | res := GetInvoices(string(testData)) 120 | if len(res) != len(correctRes) { 121 | format := "解析到的数据量不正确,期望%d个,实际%d个\n" 122 | t.Errorf(format, len(correctRes), len(res)) 123 | } 124 | 125 | for i := range res { 126 | if res[i].Number != correctRes[i].Number { 127 | format := "订单号错误,期望%v,实际%v\n" 128 | t.Errorf(format, correctRes[i].Number, res[i].Number) 129 | } 130 | if res[i].Link != correctRes[i].Link { 131 | format := "链接错误,期望%v,实际%v\n" 132 | t.Errorf(format, correctRes[i].Link, res[i].Link) 133 | } 134 | if res[i].ExpireDate != correctRes[i].ExpireDate { 135 | format := "过期日期错误,期望%v,实际%v\n" 136 | t.Errorf(format, correctRes[i].ExpireDate, res[i].ExpireDate) 137 | } 138 | if res[i].StartDate != correctRes[i].StartDate { 139 | format := "开始日期错误, 期望%v,实际%v\n" 140 | t.Errorf(format, correctRes[i].StartDate, res[i].StartDate) 141 | } 142 | if res[i].Payment != correctRes[i].Payment { 143 | format := "支付金额错误,期望%v,实际%v\n" 144 | t.Errorf(format, correctRes[i].Payment, res[i].Payment) 145 | } 146 | if res[i].State != correctRes[i].State { 147 | format := "状态错误,期望%v,实际%v\n" 148 | t.Errorf(format, correctRes[i].State, res[i].State) 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /parser/service.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Service 购买的服务信息 8 | type Service struct { 9 | // 服务名称 10 | Name string 11 | // 服务详细信息链接 12 | Link string 13 | // 服务价格 14 | Price string 15 | // 服务过期时间 16 | Expires time.Time 17 | // 服务状态:是否可用/是否需要付费 18 | State string 19 | } 20 | -------------------------------------------------------------------------------- /parser/ssrinfo.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // SSRInfo ssr套餐信息 4 | type SSRInfo struct { 5 | *Service 6 | 7 | // 节点的端口和密码 8 | Port int64 9 | Passwd string 10 | 11 | // 可用数据总量 12 | TotalData string 13 | // 已用数据总量 14 | UsedData string 15 | // 下载用量 16 | Download string 17 | // 上传用量 18 | Upload string 19 | 20 | // 可用节点信息 21 | Nodes []*SSRNode 22 | } 23 | 24 | // NewSSRInfo 生成SSRInfo 25 | func NewSSRInfo(ser *Service) *SSRInfo { 26 | s := new(SSRInfo) 27 | s.Service = ser 28 | s.Nodes = make([]*SSRNode, 0) 29 | return s 30 | } 31 | -------------------------------------------------------------------------------- /parser/ssrnode.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // SSRNode ssr节点信息 11 | type SSRNode struct { 12 | // 节点名字 13 | NodeName string `json:"node_name"` 14 | // 节点类型 15 | Type string `json:"-"` 16 | 17 | // 节点IP地址 18 | IP string `json:"server"` 19 | Port int64 `json:"server_port"` 20 | Passwd string `json:"password"` 21 | 22 | // 加密算法 23 | Crypto string `json:"method"` 24 | // 连接协议 25 | Proto string `json:"protocol"` 26 | // 混淆算法 27 | Minx string `json:"obfs"` 28 | } 29 | 30 | // Store 将配置信息存入json文件 31 | func (s *SSRNode) Store(path string) error { 32 | f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0664) 33 | if err != nil { 34 | return err 35 | } 36 | defer f.Close() 37 | 38 | data, err := json.MarshalIndent(s, "", "\t") 39 | if err != nil { 40 | return err 41 | } 42 | 43 | if _, err := f.Write(data); err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | } 49 | 50 | // Load 从配置文件读取node信息 51 | func (s *SSRNode) Load(path string) error { 52 | f, err := os.Open(path) 53 | if err != nil { 54 | return err 55 | } 56 | defer f.Close() 57 | 58 | data, err := ioutil.ReadAll(f) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | err = json.Unmarshal(data, s) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // NameNumber 获取节点的编号 72 | func (s *SSRNode) NameNumber() string { 73 | nameSplit := strings.Split(s.NodeName, "_") 74 | return "节点" + nameSplit[len(nameSplit)-1] 75 | } 76 | -------------------------------------------------------------------------------- /parser/ssrnode_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "encoding/json" 7 | ) 8 | 9 | func TestMarshalNode(t *testing.T) { 10 | node := new(SSRNode) 11 | node.NodeName = "unreachable" 12 | node.Type = "ssr" 13 | node.IP = "0.0.0.0" 14 | node.Port = 1 15 | node.Passwd = "123" 16 | node.Crypto = "aes" 17 | node.Proto = "http" 18 | node.Minx = "non" 19 | 20 | data, err := json.Marshal(node) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | t.Log(string(data)) 25 | } 26 | 27 | func TestUnmarshalNode(t *testing.T) { 28 | node := new(SSRNode) 29 | node.NodeName = "test" 30 | node.Type = "ssr" 31 | 32 | data := `{"node_name":"a","type":"ss","server":"0.0.0.0","server_port":1,"password":"123","method":"aes","protocol":"http","obfs":"non"}` 33 | if err := json.Unmarshal([]byte(data), node); err != nil { 34 | t.Error(err) 35 | } 36 | if node.NodeName != "test" || node.Type != "ssr" { 37 | t.Error("unmarshal error") 38 | } 39 | t.Log(*node) 40 | } 41 | -------------------------------------------------------------------------------- /parser/testdata/invoice.html: -------------------------------------------------------------------------------- 1 | 2 |
加载中...
95 |