├── .gitignore ├── README.md ├── bin ├── build.sh └── mock.sh ├── build.sh ├── build └── .gitkeep ├── cli.go ├── client.go ├── client_test.go ├── config.go ├── go.mod ├── go.sum ├── goer.go ├── goer_test.go ├── img └── options.png ├── main.go ├── mock ├── .gitkeep └── client.go ├── payload.go └── payload_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | !build/.gitkeep 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goer 2 | 3 | During course registration, the Edusoft server is overloaded. As a result, request processings are also negatively affected. Moreover, sending a bunch of redundant requests for supporting UI and validation by the client (website) makes us wait a very long time to register for a course. In order not to complicate, this tool ignores these redundancies and only sends requests for course registration. 4 | 5 | ## Install 6 | 7 | Which file should you [download](https://github.com/TP-O/goer/releases)? 8 | - Linux: goer 9 | - Windows: goer.exe 10 | - MacOS: mgoer 11 | 12 | ## Options 13 | ![options](/img/options.png) 14 | 15 | ## Example 16 | [How to get course ID](https://youtu.be/nPnCHI7AVZg) 17 | 18 | ```bash 19 | $ goer \ 20 | -i ITITIU19180 \ 21 | -p Mypassword \ 22 | -I "IT092IU02 01|IT092IU|Principles of Programming Languages|02|4|0|01/01/0001|0|0|0| |0|ITIT19CS31" \ 23 | -I "IT093IU02 01|IT093IU|Web Application Development|02|4|0|01/01/0001|0|0|0| |0|ITIT19CS31" 24 | ``` 25 | -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | GOOS=windows GOARCH=amd64 go build -o build 5 | GOOS=linux GOARCH=amd64 go build -o build 6 | GOOS=darwin GOARCH=amd64 go build -o build/mgoer 7 | -------------------------------------------------------------------------------- /bin/mock.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | mockgen -source client.go -destination mock/client.go -package mock 5 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | GOOS=windows GOARCH=amd64 go build -o build 2 | GOOS=linux GOARCH=amd64 go build -o build 3 | GOOS=darwin GOARCH=amd64 go build -o build/mgoer 4 | -------------------------------------------------------------------------------- /build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TP-O/goer/f5dcd20fb40574952b53455eeff1c96507dbbf5f/build/.gitkeep -------------------------------------------------------------------------------- /cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | type Options struct { 13 | ID string 14 | Password string 15 | Origin string 16 | Workers uint64 17 | CarefulMode bool 18 | SpamInterval uint64 19 | CourseIDs []string 20 | } 21 | 22 | func RunCLI() *Options { 23 | shouldExit := true 24 | options := &Options{} 25 | app := &cli.App{ 26 | Name: "goer", 27 | Usage: "A simple tool to help students enroll in their courses on the Edusoft website", 28 | Version: "2.0.2", 29 | Flags: []cli.Flag{ 30 | &cli.StringFlag{ 31 | Name: "id", 32 | Aliases: []string{"i"}, 33 | Usage: "Login to account with the provided `ID`", 34 | Required: true, 35 | Destination: &options.ID, 36 | }, 37 | &cli.StringFlag{ 38 | Name: "password", 39 | Aliases: []string{"p"}, 40 | Usage: "Account's password of the provied ID", 41 | Required: true, 42 | Destination: &options.Password, 43 | }, 44 | &cli.StringFlag{ 45 | Name: "origin", 46 | Aliases: []string{"o"}, 47 | Usage: "Origin", 48 | Value: "https://edusoftweb.hcmiu.edu.vn", 49 | Destination: &options.Origin, 50 | }, 51 | &cli.BoolFlag{ 52 | Name: "careful", 53 | Aliases: []string{"c"}, 54 | Usage: "Save after each single successful registration", 55 | Value: true, 56 | Destination: &options.CarefulMode, 57 | }, 58 | &cli.Uint64Flag{ 59 | Name: "spam", 60 | Aliases: []string{"s"}, 61 | Usage: "Repeat the registrations every `TIME` seconds", 62 | Destination: &options.SpamInterval, 63 | Action: func(ctx *cli.Context, u uint64) error { 64 | if u < 1 { 65 | logrus.Fatalf("Flag `spam` value %v must be equal to or greater than 1", u) 66 | } 67 | 68 | return nil 69 | }, 70 | }, 71 | &cli.Uint64Flag{ 72 | Name: "workers", 73 | Aliases: []string{"w"}, 74 | Usage: "Set the number of workers which register for courses", 75 | Value: 1, 76 | Destination: &options.Workers, 77 | Action: func(ctx *cli.Context, u uint64) error { 78 | if u < 1 { 79 | logrus.Fatalf("Flag `workers` value %v must be equal to or greater than 1", u) 80 | } 81 | 82 | return nil 83 | }, 84 | }, 85 | &cli.StringSliceFlag{ 86 | Name: "course-id", 87 | Aliases: []string{"I"}, 88 | Required: true, 89 | Usage: "ID of registered course", 90 | }, 91 | }, 92 | Action: func(ctx *cli.Context) error { 93 | // Fix: https://github.com/TP-O/goer/issues/1 94 | courseIDs := ctx.StringSlice("course-id") 95 | for i := 0; i < len(courseIDs); i++ { 96 | if len(strings.Split(courseIDs[i], "|")) < 10 && i < len(courseIDs[i])-1 { 97 | options.CourseIDs = append(options.CourseIDs, fmt.Sprintf("%s, %s", courseIDs[i], courseIDs[i+1])) 98 | i++ 99 | } else { 100 | options.CourseIDs = append(options.CourseIDs, courseIDs[i]) 101 | } 102 | } 103 | 104 | shouldExit = false 105 | return nil 106 | }, 107 | } 108 | 109 | if err := app.Run(os.Args); err != nil { 110 | logrus.Fatal(err) 111 | } 112 | 113 | if shouldExit { 114 | os.Exit(0) 115 | } 116 | 117 | return options 118 | } 119 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/cookiejar" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type IGoerClient interface { 11 | Do(req *http.Request) (*http.Response, error) 12 | DeleteSessionId(domain string) error 13 | } 14 | 15 | type GoerClient struct { 16 | HttpClient *http.Client 17 | } 18 | 19 | func NewGoerClient() *GoerClient { 20 | if jar, err := cookiejar.New(nil); err != nil { 21 | panic(SystemFailureMessage + err.Error()) 22 | } else { 23 | return &GoerClient{ 24 | &http.Client{ 25 | Jar: jar, 26 | Timeout: 60 * time.Second, 27 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 28 | return http.ErrUseLastResponse 29 | }, 30 | }, 31 | } 32 | } 33 | } 34 | 35 | func (c *GoerClient) Do(req *http.Request) (*http.Response, error) { 36 | return c.HttpClient.Do(req) 37 | } 38 | 39 | func (c *GoerClient) DeleteSessionId(domain string) error { 40 | if url, err := url.Parse(domain); err != nil { 41 | return err 42 | } else { 43 | c.HttpClient.Jar.SetCookies(url, []*http.Cookie{ 44 | { 45 | Name: SessionIDCookieField, 46 | Value: "", 47 | HttpOnly: true, 48 | }, 49 | }) 50 | 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewGoerClient(t *testing.T) { 10 | client := NewGoerClient() 11 | 12 | assert.NotNil(t, client) 13 | assert.NotNil(t, client.HttpClient.Jar) 14 | } 15 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | LoginPath = "/default.aspx" 5 | HomePath = "/default.aspx" 6 | CourseListPath = "/Default.aspx?page=dkmonhoc" 7 | RegisterCoursePath = "/ajaxpro/EduSoft.Web.UC.DangKyMonHoc,EduSoft.Web.ashx" 8 | SaveCoursePath = "/ajaxpro/EduSoft.Web.UC.DangKyMonHoc,EduSoft.Web.ashx" 9 | ) 10 | 11 | const ( 12 | SessionIDCookieField = "ASP.NET_SessionId" 13 | ) 14 | 15 | const ( 16 | UserGreetingSelector = "#ctl00_Header1_Logout1_lblNguoiDung" 17 | CourseAlertSelector = "#ContentPlaceHolder1_ctl00_lblThongBaoNgoaiTGDK" 18 | ) 19 | 20 | const ( 21 | IDInputName = "ctl00$ContentPlaceHolder1$ctl00$ucDangNhap$txtTaiKhoa" 22 | PasswordInputName = "ctl00$ContentPlaceHolder1$ctl00$ucDangNhap$txtMatKhau" 23 | LoginActionInputName = "ctl00$ContentPlaceHolder1$ctl00$ucDangNhap$btnDangNhap" 24 | ) 25 | 26 | const ( 27 | RegisterCourseAjaxMethod = "LuuVaoKetQuaDangKy" 28 | SaveCourseAjaxMethod = "LuuDanhSachDangKy_HopLe" 29 | ) 30 | 31 | const ( 32 | SystemFailureMessage = "System error 😢 " 33 | LoginSuccessMessage = "Login successfully!!! 😆 " 34 | LoginFailureMessage = "Login failed 😢 " 35 | LogoutSuccessMessage = "Logut successfully!!! 😆 " 36 | LogoutFailureMessage = "Logout failed 😢 " 37 | RegistrationIsOpenMessage = "Registration is open 😆 " 38 | RegistrationIsNotOpenMessage = "Registration is not open 😢 " 39 | RegistrationSuccessMessage = "Registered 😆 " 40 | RegistrationFailureMessage = "Register failed 😢 " 41 | SaveSuccessMessage = "Saved!! 😆 " 42 | SaveFailureMessage = "Save failed 😢 " 43 | ) 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tp-o/goer 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/PuerkitoBio/goquery v1.8.0 7 | github.com/golang/mock v1.6.0 8 | github.com/sirupsen/logrus v1.9.0 9 | github.com/stretchr/testify v1.8.1 10 | github.com/urfave/cli/v2 v2.23.5 11 | ) 12 | 13 | require ( 14 | github.com/andybalholm/cascadia v1.3.1 // indirect 15 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 19 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 20 | golang.org/x/net v0.2.0 // indirect 21 | golang.org/x/sys v0.2.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= 2 | github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= 3 | github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= 4 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 11 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 15 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 16 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 17 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 20 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 25 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 26 | github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw= 27 | github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 28 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 29 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 30 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 31 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 32 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 33 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 34 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 35 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 36 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 37 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 38 | golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= 39 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 40 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 41 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 42 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 43 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 45 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 47 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= 50 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 52 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 53 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 54 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 55 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 56 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 57 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 58 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 59 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 60 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 61 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 64 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 65 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 66 | -------------------------------------------------------------------------------- /goer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/PuerkitoBio/goquery" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type Goer struct { 14 | Origin string 15 | Client IGoerClient 16 | RegisteredCourses []string 17 | } 18 | 19 | func NewGoer(origin string, client IGoerClient) *Goer { 20 | return &Goer{ 21 | origin, 22 | client, 23 | make([]string, 0), 24 | } 25 | } 26 | 27 | func (g *Goer) Login(credentials *Credentials) bool { 28 | payload := GenerateLoginPayload(credentials) 29 | req, _ := http.NewRequest("POST", g.Origin+LoginPath, payload.Body) 30 | req.Header.Add("Content-Type", payload.Type) 31 | 32 | if res, err := g.Client.Do(req); err != nil { 33 | logrus.Warn(SystemFailureMessage + err.Error()) 34 | return false 35 | } else if res.StatusCode != 302 || 36 | strings.Contains(res.Header.Values("Location")[0], "sessionreuse") { 37 | 38 | logrus.Warn(LoginFailureMessage) 39 | return false 40 | } else { 41 | logrus.Info(LoginSuccessMessage) 42 | return true 43 | } 44 | } 45 | 46 | func (g *Goer) Clear() bool { 47 | if err := g.Client.DeleteSessionId(g.Origin); err != nil { 48 | logrus.Warn(LogoutFailureMessage) 49 | return false 50 | } 51 | 52 | logrus.Info(LogoutSuccessMessage) 53 | return true 54 | } 55 | 56 | func (g *Goer) Greet() { 57 | req, _ := http.NewRequest("GET", g.Origin+HomePath, nil) 58 | 59 | if res, err := g.Client.Do(req); err != nil { 60 | logrus.Fatalf(SystemFailureMessage + err.Error()) 61 | } else { 62 | document, _ := goquery.NewDocumentFromReader(res.Body) 63 | logrus.Info(document.Find(UserGreetingSelector).Text()) 64 | } 65 | } 66 | 67 | func (g *Goer) IsRegistrationOpen() bool { 68 | req, _ := http.NewRequest("GET", g.Origin+CourseListPath, nil) 69 | 70 | if res, err := g.Client.Do(req); err != nil { 71 | logrus.Warn(SystemFailureMessage + err.Error()) 72 | return false 73 | } else { 74 | document, _ := goquery.NewDocumentFromReader(res.Body) 75 | 76 | if document.Find(CourseAlertSelector).Text() == "" { 77 | logrus.Info(RegistrationIsOpenMessage) 78 | return true 79 | } 80 | 81 | logrus.Warn(RegistrationIsNotOpenMessage) 82 | return false 83 | } 84 | } 85 | 86 | func (g *Goer) RegisterCourse(courseId string) bool { 87 | courseName := strings.Split(courseId, "|")[2] 88 | payload := GenerateRegisterCoursePayload(courseId) 89 | req, _ := http.NewRequest("POST", g.Origin+RegisterCoursePath, payload.Body) 90 | req.Header.Add("Content-Type", payload.Type) 91 | req.Header.Add("X-AjaxPro-Method", RegisterCourseAjaxMethod) 92 | 93 | if res, err := g.Client.Do(req); err != nil { 94 | logrus.Warn(SystemFailureMessage + err.Error()) 95 | return false 96 | } else { 97 | resBody, _ := io.ReadAll(res.Body) 98 | 99 | if bytes.Contains(resBody, []byte(courseName)) { 100 | g.RegisteredCourses = append(g.RegisteredCourses, courseName) 101 | 102 | logrus.Info(RegistrationSuccessMessage + "[" + courseName + "]") 103 | return true 104 | } 105 | 106 | logrus.Warn(RegistrationFailureMessage + "[" + courseName + "]") 107 | return false 108 | } 109 | } 110 | 111 | func (g *Goer) SaveRegistration() bool { 112 | payload := GenerateCourseSavePayload() 113 | req, _ := http.NewRequest("POST", g.Origin+SaveCoursePath, payload.Body) 114 | req.Header.Add("Content-Type", payload.Type) 115 | req.Header.Add("X-AjaxPro-Method", SaveCourseAjaxMethod) 116 | 117 | if res, err := g.Client.Do(req); err != nil { 118 | logrus.Warn(SystemFailureMessage + err.Error()) 119 | return false 120 | } else { 121 | resBody, _ := io.ReadAll(res.Body) 122 | 123 | if bytes.Contains(resBody, []byte("||default.aspx?page=dkmonhoc")) { 124 | logrus.Info(SaveSuccessMessage, "[", strings.Join(g.RegisteredCourses, ", "), "]") 125 | return true 126 | } 127 | 128 | logrus.Info(SaveFailureMessage) 129 | return false 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /goer_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/golang/mock/gomock" 13 | "github.com/sirupsen/logrus" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/tp-o/goer/mock" 16 | ) 17 | 18 | func TestMain(m *testing.M) { 19 | logrus.SetOutput(ioutil.Discard) 20 | code := m.Run() 21 | os.Exit(code) 22 | } 23 | 24 | func TestNewGoer(t *testing.T) { 25 | origin := "http://google.com/" 26 | client := NewGoerClient() 27 | goer := NewGoer(origin, client) 28 | 29 | assert.NotNil(t, goer) 30 | assert.Equal(t, origin, goer.Origin) 31 | assert.Equal(t, client, goer.Client) 32 | } 33 | 34 | func TestGoerLogin(t *testing.T) { 35 | origin := "http://google.com/" 36 | credentials := &Credentials{} 37 | failedHeader := http.Header{} 38 | failedHeader.Add("Location", "sessionreuse") 39 | successfulHeader := http.Header{} 40 | successfulHeader.Add("Location", "/") 41 | 42 | ctrl := gomock.NewController(t) 43 | defer ctrl.Finish() 44 | 45 | mockClient := mock.NewMockIGoerClient(ctrl) 46 | //================================================ 47 | goer := NewGoer(origin, mockClient) 48 | 49 | mockClient. 50 | EXPECT(). 51 | Do(gomock.Any()). 52 | Return(nil, errors.New("Test error")) 53 | assert.False(t, goer.Login(credentials)) 54 | 55 | mockClient. 56 | EXPECT(). 57 | Do(gomock.Any()). 58 | Return(&http.Response{ 59 | StatusCode: 200, 60 | }, nil) 61 | assert.False(t, goer.Login(credentials)) 62 | 63 | mockClient.EXPECT().Do(gomock.Any()). 64 | Return(&http.Response{ 65 | Header: failedHeader, 66 | }, nil) 67 | assert.False(t, goer.Login(credentials)) 68 | 69 | mockClient.EXPECT().Do(gomock.Any()). 70 | Return(&http.Response{ 71 | StatusCode: 302, 72 | Header: successfulHeader, 73 | }, nil) 74 | assert.True(t, goer.Login(credentials)) 75 | } 76 | 77 | func TestGoerClear(t *testing.T) { 78 | origin := "http://google.com/" 79 | 80 | ctrl := gomock.NewController(t) 81 | defer ctrl.Finish() 82 | 83 | mockClient := mock.NewMockIGoerClient(ctrl) 84 | //================================================ 85 | goer := NewGoer(origin, mockClient) 86 | 87 | mockClient. 88 | EXPECT(). 89 | DeleteSessionId(gomock.Eq(origin)). 90 | Return(errors.New("Test error")) 91 | assert.False(t, goer.Clear()) 92 | 93 | mockClient. 94 | EXPECT(). 95 | DeleteSessionId(gomock.Eq(origin)). 96 | Return(nil) 97 | assert.True(t, goer.Clear()) 98 | } 99 | 100 | func TestGoerGreet(t *testing.T) { 101 | // 102 | } 103 | 104 | func TestGoerIsRegistrationOpen(t *testing.T) { 105 | origin := "http://google.com/" 106 | 107 | ctrl := gomock.NewController(t) 108 | defer ctrl.Finish() 109 | 110 | mockClient := mock.NewMockIGoerClient(ctrl) 111 | //================================================ 112 | goer := NewGoer(origin, mockClient) 113 | 114 | mockClient. 115 | EXPECT(). 116 | Do(gomock.Any()). 117 | Return(nil, errors.New("Test error")) 118 | assert.False(t, goer.IsRegistrationOpen()) 119 | 120 | mockClient. 121 | EXPECT(). 122 | Do(gomock.Any()). 123 | Return(&http.Response{ 124 | Body: io.NopCloser(strings.NewReader( 125 | "

Alert message

", 128 | )), 129 | }, nil) 130 | assert.False(t, goer.IsRegistrationOpen()) 131 | 132 | mockClient. 133 | EXPECT(). 134 | Do(gomock.Any()). 135 | Return(&http.Response{ 136 | Body: io.NopCloser(strings.NewReader("

")), 137 | }, nil) 138 | assert.True(t, goer.IsRegistrationOpen()) 139 | } 140 | 141 | func TestRegisterCourse(t *testing.T) { 142 | origin := "http://google.com/" 143 | courseID := "IT093IU02 01|IT093IU|Web Application Development|02|4|0|01/01/0001|0|0|0| |0|ITIT19CS31" 144 | coruseName := strings.Split(courseID, "|")[2] 145 | 146 | ctrl := gomock.NewController(t) 147 | defer ctrl.Finish() 148 | 149 | mockClient := mock.NewMockIGoerClient(ctrl) 150 | //================================================ 151 | goer := NewGoer(origin, mockClient) 152 | 153 | mockClient. 154 | EXPECT(). 155 | Do(gomock.Any()). 156 | Return(nil, errors.New("Test error")) 157 | assert.False(t, goer.RegisterCourse(courseID)) 158 | assert.NotContains(t, goer.RegisteredCourses, coruseName) 159 | 160 | mockClient. 161 | EXPECT(). 162 | Do(gomock.Any()). 163 | Return(&http.Response{ 164 | Body: io.NopCloser(strings.NewReader("Response ncc")), 165 | }, nil) 166 | assert.False(t, goer.RegisterCourse(courseID)) 167 | assert.NotContains(t, goer.RegisteredCourses, coruseName) 168 | 169 | mockClient. 170 | EXPECT(). 171 | Do(gomock.Any()). 172 | Return(&http.Response{ 173 | Body: io.NopCloser(strings.NewReader("{" + coruseName + "}")), 174 | }, nil) 175 | assert.True(t, goer.RegisterCourse(courseID)) 176 | assert.Contains(t, goer.RegisteredCourses, coruseName) 177 | } 178 | 179 | func TestSaveRegistration(t *testing.T) { 180 | origin := "http://google.com/" 181 | 182 | ctrl := gomock.NewController(t) 183 | defer ctrl.Finish() 184 | 185 | mockClient := mock.NewMockIGoerClient(ctrl) 186 | //================================================ 187 | goer := NewGoer(origin, mockClient) 188 | 189 | mockClient. 190 | EXPECT(). 191 | Do(gomock.Any()). 192 | Return(nil, errors.New("Test error")) 193 | assert.False(t, goer.SaveRegistration()) 194 | 195 | mockClient. 196 | EXPECT(). 197 | Do(gomock.Any()). 198 | Return(&http.Response{ 199 | Body: io.NopCloser(strings.NewReader("Response ncc")), 200 | }, nil) 201 | assert.False(t, goer.SaveRegistration()) 202 | 203 | mockClient. 204 | EXPECT(). 205 | Do(gomock.Any()). 206 | Return(&http.Response{ 207 | Body: io.NopCloser(strings.NewReader("||default.aspx?page=dkmonhoc")), 208 | }, nil) 209 | assert.True(t, goer.SaveRegistration()) 210 | } 211 | -------------------------------------------------------------------------------- /img/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TP-O/goer/f5dcd20fb40574952b53455eeff1c96507dbbf5f/img/options.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "time" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func init() { 12 | logrus.SetFormatter(&logrus.TextFormatter{ 13 | FullTimestamp: true, 14 | }) 15 | 16 | logrus.SetOutput(os.Stdout) 17 | } 18 | 19 | func main() { 20 | options := RunCLI() 21 | credentials := &Credentials{ 22 | ID: options.ID, 23 | Password: options.Password, 24 | } 25 | goer := NewGoer(options.Origin, NewGoerClient()) 26 | 27 | logrus.Warn("=======================================================") 28 | logrus.Warn("DO NOT ACCESS YOUR ACCOUNT WHEN THIS TOOL IS RUNNING!!!") 29 | logrus.Warn("=======================================================") 30 | 31 | // Try to log in again until the registration is ready 32 | for { 33 | if ok := goer.Login(credentials); ok { 34 | // goer.Greet() 35 | 36 | if isOpen := goer.IsRegistrationOpen(); isOpen { 37 | break 38 | } else { 39 | goer.Clear() 40 | } 41 | } 42 | 43 | time.Sleep(1 * time.Second) 44 | logrus.Info("Login again...") 45 | } 46 | 47 | // Start registration 48 | var wg sync.WaitGroup 49 | var mu sync.Mutex 50 | courseCounter := 0 51 | courseIDChannel := make(chan string, options.Workers) 52 | 53 | go func() { 54 | for _, courseID := range options.CourseIDs { 55 | courseIDChannel <- courseID 56 | } 57 | }() 58 | 59 | for i := 0; i < int(options.Workers); i++ { 60 | wg.Add(1) 61 | 62 | go func() { 63 | for courseID := range courseIDChannel { 64 | if ok := goer.RegisterCourse(courseID); !ok { 65 | courseIDChannel <- courseID 66 | } else { 67 | // Save the enrolled course after successful registration 68 | if options.CarefulMode { 69 | goer.SaveRegistration() 70 | } 71 | 72 | // Put the course ID back if spam is enable 73 | if options.SpamInterval != 0 { 74 | courseIDChannel <- courseID 75 | time.Sleep(time.Duration(options.SpamInterval) * time.Second) 76 | } else { 77 | mu.Lock() 78 | 79 | courseCounter++ 80 | 81 | // Close the channel if all courses are registered 82 | if courseCounter == len(options.CourseIDs) { 83 | close(courseIDChannel) 84 | } 85 | 86 | mu.Unlock() 87 | } 88 | } 89 | } 90 | 91 | wg.Done() 92 | }() 93 | } 94 | 95 | wg.Wait() 96 | 97 | // Save all registered courses 98 | if !options.CarefulMode { 99 | goer.SaveRegistration() 100 | } 101 | 102 | logrus.Info("Done!") 103 | } 104 | -------------------------------------------------------------------------------- /mock/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TP-O/goer/f5dcd20fb40574952b53455eeff1c96507dbbf5f/mock/.gitkeep -------------------------------------------------------------------------------- /mock/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: client.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | http "net/http" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockIGoerClient is a mock of IGoerClient interface. 15 | type MockIGoerClient struct { 16 | ctrl *gomock.Controller 17 | recorder *MockIGoerClientMockRecorder 18 | } 19 | 20 | // MockIGoerClientMockRecorder is the mock recorder for MockIGoerClient. 21 | type MockIGoerClientMockRecorder struct { 22 | mock *MockIGoerClient 23 | } 24 | 25 | // NewMockIGoerClient creates a new mock instance. 26 | func NewMockIGoerClient(ctrl *gomock.Controller) *MockIGoerClient { 27 | mock := &MockIGoerClient{ctrl: ctrl} 28 | mock.recorder = &MockIGoerClientMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockIGoerClient) EXPECT() *MockIGoerClientMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // DeleteSessionId mocks base method. 38 | func (m *MockIGoerClient) DeleteSessionId(domain string) error { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "DeleteSessionId", domain) 41 | ret0, _ := ret[0].(error) 42 | return ret0 43 | } 44 | 45 | // DeleteSessionId indicates an expected call of DeleteSessionId. 46 | func (mr *MockIGoerClientMockRecorder) DeleteSessionId(domain interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSessionId", reflect.TypeOf((*MockIGoerClient)(nil).DeleteSessionId), domain) 49 | } 50 | 51 | // Do mocks base method. 52 | func (m *MockIGoerClient) Do(req *http.Request) (*http.Response, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "Do", req) 55 | ret0, _ := ret[0].(*http.Response) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // Do indicates an expected call of Do. 61 | func (mr *MockIGoerClientMockRecorder) Do(req interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockIGoerClient)(nil).Do), req) 64 | } 65 | -------------------------------------------------------------------------------- /payload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "mime/multipart" 8 | "strings" 9 | ) 10 | 11 | type Credentials struct { 12 | ID string 13 | Password string 14 | } 15 | 16 | type Payload struct { 17 | Type string 18 | Body *bytes.Buffer 19 | } 20 | 21 | type RegisterCourseBody struct { 22 | IsValidCoso bool `json:"isValidCoso"` 23 | IsValidTKB bool `json:"isValidTKB"` 24 | MaDK string `json:"maDK"` 25 | MaMH string `json:"maMH"` 26 | Sotc string `json:"sotc"` 27 | TenMH string `json:"tenMH"` 28 | MaNh string `json:"maNh"` 29 | StrsoTCHP string `json:"strsoTCHP"` 30 | IsCheck string `json:"isCheck"` 31 | OldMaDK string `json:"oldMaDK"` 32 | StrngayThi string `json:"strngayThi"` 33 | TietBD string `json:"tietBD"` 34 | SoTiet string `json:"soTiet"` 35 | IsMHDangKyCungKhoiSV string `json:"isMHDangKyCungKhoiSV"` 36 | } 37 | 38 | func GenerateMultipartFormPayload(fields map[string]string) Payload { 39 | payload := &bytes.Buffer{} 40 | writer := multipart.NewWriter(payload) 41 | defer writer.Close() 42 | 43 | for key, val := range fields { 44 | fw, _ := writer.CreateFormField(key) 45 | io.Copy(fw, strings.NewReader(val)) 46 | } 47 | 48 | return Payload{ 49 | Type: writer.FormDataContentType(), 50 | Body: payload, 51 | } 52 | } 53 | 54 | func GenerateLoginPayload(credentials *Credentials) Payload { 55 | loginFileds := map[string]string{ 56 | "__EVENTTARGET": "", 57 | "__EVENTARGUMENT": "", 58 | IDInputName: credentials.ID, 59 | PasswordInputName: credentials.Password, 60 | LoginActionInputName: "Đăng Nhập", 61 | } 62 | 63 | return GenerateMultipartFormPayload(loginFileds) 64 | } 65 | 66 | func GenerateRegisterCoursePayload(courseId string) Payload { 67 | extractedCourseInfo := strings.Split(courseId, "|") 68 | body := RegisterCourseBody{ 69 | IsValidCoso: false, 70 | IsValidTKB: false, 71 | MaDK: extractedCourseInfo[0], 72 | MaMH: extractedCourseInfo[1], 73 | Sotc: extractedCourseInfo[4], 74 | TenMH: extractedCourseInfo[2], 75 | MaNh: extractedCourseInfo[3], 76 | StrsoTCHP: "0", 77 | IsCheck: "true", 78 | OldMaDK: extractedCourseInfo[10], 79 | StrngayThi: extractedCourseInfo[6], 80 | TietBD: extractedCourseInfo[8], 81 | SoTiet: extractedCourseInfo[9], 82 | IsMHDangKyCungKhoiSV: "0", 83 | } 84 | jsonBody, _ := json.Marshal(body) 85 | 86 | return Payload{ 87 | Type: "text/plain; charset=utf-8", 88 | Body: bytes.NewBuffer(jsonBody), 89 | } 90 | } 91 | 92 | func GenerateCourseSavePayload() Payload { 93 | body := bytes.NewBuffer([]byte(`{ 94 | "isCheckSongHanh": false, 95 | "ChiaHP": false 96 | }`)) 97 | 98 | return Payload{ 99 | Type: "text/plain; charset=utf-8", 100 | Body: body, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /payload_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGenerateMultipartFormPayload(t *testing.T) { 12 | fields := map[string]string{ 13 | "F1": "V1", 14 | "F2": "V2", 15 | } 16 | payload := GenerateMultipartFormPayload(fields) 17 | body := payload.Body.String() 18 | 19 | assert.True(t, strings.HasPrefix(payload.Type, "multipart/form-data; boundary=")) 20 | 21 | for key, val := range fields { 22 | assert.Contains(t, body, "name=\""+key+"\"") 23 | assert.Contains(t, body, val) 24 | } 25 | } 26 | 27 | func TestGenerateLoginPayload(t *testing.T) { 28 | credentials := &Credentials{ 29 | ID: "ITITIU19180", 30 | Password: "Mypassword", 31 | } 32 | payload := GenerateLoginPayload(credentials) 33 | body := payload.Body.String() 34 | 35 | assert.Contains(t, body, "name=\""+IDInputName+"\"") 36 | assert.Contains(t, body, "name=\""+PasswordInputName+"\"") 37 | assert.Contains(t, body, "name=\""+LoginActionInputName+"\"") 38 | assert.Contains(t, body, credentials.ID) 39 | assert.Contains(t, body, credentials.Password) 40 | } 41 | 42 | func TestGenerateRegisterCoursePayload(t *testing.T) { 43 | courseID := "IT093IU02 01|IT093IU|Web Application Development|02|4|0|01/01/0001|0|0|0| |0|ITIT19CS31" 44 | extractedCourseInfo := strings.Split(courseID, "|") 45 | payload := GenerateRegisterCoursePayload(courseID) 46 | var body RegisterCourseBody 47 | json.Unmarshal(payload.Body.Bytes(), &body) 48 | 49 | assert.Equal(t, payload.Type, "text/plain; charset=utf-8") 50 | assert.Equal(t, body.MaDK, extractedCourseInfo[0]) 51 | assert.Equal(t, body.MaMH, extractedCourseInfo[1]) 52 | assert.Equal(t, body.TenMH, extractedCourseInfo[2]) 53 | assert.Equal(t, body.MaNh, extractedCourseInfo[3]) 54 | assert.Equal(t, body.Sotc, extractedCourseInfo[4]) 55 | assert.Equal(t, body.StrngayThi, extractedCourseInfo[6]) 56 | assert.Equal(t, body.SoTiet, extractedCourseInfo[8]) 57 | assert.Equal(t, body.SoTiet, extractedCourseInfo[9]) 58 | assert.Equal(t, body.OldMaDK, extractedCourseInfo[10]) 59 | } 60 | 61 | func TestGenerateCourseSavePayload(t *testing.T) { 62 | payload := GenerateCourseSavePayload() 63 | body := payload.Body.String() 64 | 65 | assert.Equal(t, payload.Type, "text/plain; charset=utf-8") 66 | assert.NotEmpty(t, body) 67 | } 68 | --------------------------------------------------------------------------------