Updated Credentials Storage

This commit is contained in:
2025-03-08 03:34:04 +00:00
commit 1142011a19
6 changed files with 548 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
Copyright 2024 FernandoJVideira
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+63
View File
@@ -0,0 +1,63 @@
# IPLeiria Auto Attendance Script
This project automates the process of navigating to the IPLeiria GISEM platform and marking your attendence using the `chromedp` library.
## Prerequisites
- Go 1.23.2 or later
- A Chromium browser installed
## Installation
1. Clone the repository:
```sh
git clone https://github.com/FernandoJVideira/gisem-presencas.git
cd gisem-presencas
```
2. Install dependencies:
```sh
go mod tidy
```
3. Add your credentials to the `utils.go` file.
## Usage
To run the project, execute the following command:
```sh
go run main.go
```
As another option you can build the project by running:
```sh
go build .
```
And run it like:
```sh
./gisem_presencas -w value
```
## Flags
- `-w` or `-wait`: Time to wait in between refreshes (default is 30 seconds).
## Project Structure
- `main.go`: The main entry point of the application.
- `utils.go`: Contains utility functions such as flag parsing and the welcome message.
- `.env`: Environment variables file.
- `example.env`: Example environment variables file.
- `go.mod`: Go module dependencies.
- `go.sum`: Go module dependency checksums.
- `README.md`: Project documentation.
## License
This project is licensed under the MIT License.
+20
View File
@@ -0,0 +1,20 @@
module github.com/FernandoJVideira/gisem-presencas
go 1.23.2
require (
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb
github.com/chromedp/chromedp v0.11.1
golang.org/x/crypto v0.36.0
)
require (
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
)
+27
View File
@@ -0,0 +1,27 @@
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb h1:noKVm2SsG4v0Yd0lHNtFYc9EUxIVvrr4kJ6hM8wvIYU=
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM=
github.com/chromedp/chromedp v0.11.1 h1:Spca8egFqUlv+JDW+yIs+ijlHlJDPufgrfXPwtq6NMs=
github.com/chromedp/chromedp v0.11.1/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+82
View File
@@ -0,0 +1,82 @@
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/chromedp/chromedp"
)
const (
webUrl = "https://gisem.dei.estg.ipleiria.pt"
)
var waitTime int
func main() {
//* Parse the flags
wait, headless, err := parseFlags()
if err != nil {
log.Fatalf("Failed to parse flags: %v", err)
}
waitTime = wait
fmt.Println("Waiting time:", waitTime)
//* Welcome message
welcomeMessage()
creds, err := getCreds()
if err != nil {
// If the credentials file doesn't exist, prompt for credentials
if err.Error() == "credentials file not found: stat "+credFile+": no such file or directory" {
// The file doesn't exist, create it by prompting for credentials
creds, err = promptAndSaveCreds(credFile)
if err != nil {
log.Fatalf("Failed to get credentials: %v", err)
}
} else {
log.Fatalf("Failed to get credentials: %v", err)
}
}
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", headless), // Disable headless mode
chromedp.Flag("disable-gpu", headless), // Enable GPU
chromedp.Flag("start-maximized", !headless), // Start maximized
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
//* Navigate to the website
if err := chromedp.Run(ctx, chromedp.Navigate(webUrl)); err != nil {
log.Fatalf("Failed to navigate: %v", err)
}
//* Check if there's a login button
var isVisible bool
if err := chromedp.Run(ctx, chromedp.Tasks{
chromedp.WaitVisible(loginButton),
chromedp.Evaluate(fmt.Sprintf("document.querySelector('%s').offsetParent !== null", loginButton), &isVisible),
}); err != nil {
log.Fatalf("Failed to evaluate: %v", err)
}
if isVisible {
log.Println("Logging in...")
if err := login(ctx, creds); err != nil {
log.Fatalf("Failed to login: %v", err)
}
time.Sleep(1 * time.Second)
log.Println("Waiting for activation...")
if err := marcarPresenca(ctx); err != nil {
log.Fatalf("Failed to mark presence: %v", err)
}
}
}
+349
View File
@@ -0,0 +1,349 @@
package main
import (
"bufio"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
"golang.org/x/crypto/ssh/terminal"
)
const (
loginButton = "#app-navbar-collapse > ul.nav.navbar-nav.navbar-right > li:nth-child(1) > a"
usernameInput = "body > div > div > div > div.modal-body > form > div:nth-child(2) > input"
passwordInput = "body > div > div > div > div.modal-body > form > div:nth-child(3) > input"
rememberMe = `body > div > div > div > div.modal-body > form > div.checkbox > label > input[type="checkbox"]`
submitButton = "body > div > div > div > div.modal-body > form > button"
errorDiv = "body > div > div > div > div.modal-body > form > div.alert.alert-danger"
menuPresencas = "#app-navbar-collapse > ul:nth-child(1) > li:nth-child(4) > a"
btnMarcarPresenca = ".btn.btn-primary"
maxRetries = 5
credFile = ".credentials"
encryptSalt = "IPLeiriaAutoAttendance" // Salt for encryption, ideally should be unique per installation
)
type Credentials struct {
Username string
Password string
}
func deriveKey(passphrase string) []byte {
hash := sha256.Sum256([]byte(passphrase))
return hash[:]
}
func encrypt(plaintext, passphrase string) (string, error) {
key := deriveKey(passphrase)
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, aesGCM.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil)
return hex.EncodeToString(ciphertext), nil
}
func decrypt(encryptedHex, passphrase string) (string, error) {
key := deriveKey(passphrase)
ciphertext, err := hex.DecodeString(encryptedHex)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := aesGCM.NonceSize()
if len(ciphertext) < nonceSize {
return "", fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
func parseFlags() (int, bool, error) {
//* Set the flags
wait := flag.Int("w", 10, "Time to wait in between refreshes")
flag.IntVar(wait, "wait", 10, "Time to wait in between refreshes")
// By default, run in headless mode
headless := flag.Bool("h", true, "Run in headless mode")
flag.BoolVar(headless, "headless", true, "Run in headless mode")
flag.Parse()
if *wait < 0 {
return 0, false, fmt.Errorf("invalid wait time: %d", *wait)
}
fmt.Println(*headless)
return *wait, *headless, nil
}
func welcomeMessage() {
fmt.Println("******************************************************************************************")
fmt.Println("* Welcome to IPLeiria AutoAttendance! *")
fmt.Println("* This program will automatically mark your attendance in the IPLeiria's GISEM platform. *")
fmt.Println("* Please make sure you have your credentials ready. *")
fmt.Println("* Enjoy! *")
fmt.Println("******************************************************************************************")
}
func getCredsFilePath() (string, error) {
var configDir string
var err error
switch runtime.GOOS {
case "windows":
configDir = os.Getenv("APPDATA")
if configDir == "" {
configDir, err = os.UserHomeDir()
if err != nil {
return "", err
}
}
default:
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
configDir = filepath.Join(home, ".config")
}
appConfigDir := filepath.Join(configDir, "gisem-presencas")
// Create directory if it doesn't exist
if err := os.MkdirAll(appConfigDir, 0700); err != nil {
return "", fmt.Errorf("failed to create config directory: %v", err)
}
return filepath.Join(appConfigDir, credFile), nil
}
func getCreds() (Credentials, error) {
credPath, err := getCredsFilePath()
if err != nil {
return Credentials{}, fmt.Errorf("could not determine credentials path: %v", err)
}
// Check if credentials file exists
if _, err := os.Stat(credPath); os.IsNotExist(err) {
// File doesn't exist, prompt user for credentials
return promptAndSaveCreds(credPath)
}
file, err := os.Open(credPath)
if err != nil {
return Credentials{}, fmt.Errorf("failed to open credentials file: %v", err)
}
defer file.Close()
var creds Credentials
if err := json.NewDecoder(file).Decode(&creds); err != nil {
return Credentials{}, fmt.Errorf("failed to read credentials: %v", err)
}
if creds.Username == "" || creds.Password == "" {
return promptAndSaveCreds(credPath)
}
return creds, nil
}
func promptAndSaveCreds(credPath string) (Credentials, error) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your GISEM username: ")
username, err := reader.ReadString('\n')
if err != nil {
return Credentials{}, fmt.Errorf("failed to read username: %v", err)
}
username = strings.TrimSpace(username)
fmt.Print("Enter your GISEM password: ")
passwordBytes, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
// Fallback to regular input if terminal.ReadPassword fails
fmt.Println("\nSecure password input not available, using regular input:")
fmt.Print("Enter your GISEM password: ")
passwordStr, err := reader.ReadString('\n')
if err != nil {
return Credentials{}, fmt.Errorf("could not read password: %v", err)
}
password := strings.TrimSpace(passwordStr)
return saveCreds(credPath, username, password)
}
fmt.Println()
password := strings.TrimSpace(string(passwordBytes))
return saveCreds(credPath, username, password)
}
func saveCreds(filePath, username, password string) (Credentials, error) {
creds := Credentials{
Username: username,
Password: password,
}
pwd, err := encrypt(creds.Password, encryptSalt+username)
if err != nil {
return Credentials{}, fmt.Errorf("failed to encrypt password: %v", err)
}
creds.Password = pwd
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return Credentials{}, fmt.Errorf("could not open file: %v", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
if err := encoder.Encode(creds); err != nil {
return Credentials{}, fmt.Errorf("could not write credentials to file: %v", err)
}
fmt.Println("Credentials saved successfully!")
return creds, nil
}
func login(ctx context.Context, creds Credentials) error {
//* Fill the login form and save the cookies
pwd, err := decrypt(creds.Password, encryptSalt+creds.Username)
if err != nil {
return fmt.Errorf("failed to decrypt password: %v", err)
}
if err := chromedp.Run(ctx,
chromedp.WaitVisible(".modal-content"),
chromedp.SendKeys(usernameInput, creds.Username),
chromedp.SendKeys(passwordInput, pwd),
chromedp.Click(submitButton),
); err != nil {
log.Fatalf("Failed to fill the login form: %v", err)
}
return nil
}
func marcarPresenca(ctx context.Context) error {
var attempt int
for attempt = 1; attempt <= maxRetries; attempt++ {
if err := chromedp.Run(ctx,
chromedp.ActionFunc(func(ctx context.Context) error {
for {
waitCtx, cancel := context.WithTimeout(ctx, time.Duration(waitTime)*time.Second)
defer cancel()
//Try to wait for the element
err := chromedp.Run(waitCtx,
chromedp.WaitVisible(menuPresencas))
if err == nil {
fmt.Println("Navigation menu is visible!")
}
chromedp.Run(waitCtx, chromedp.Click(menuPresencas))
fmt.Println("Navigation menu clicked!")
var currentURL string
if err := chromedp.Location(&currentURL).Do(ctx); err != nil {
return err
}
if strings.Contains(currentURL, "obterAulasMarcarPresenca") {
fmt.Println("Attendance page loaded!")
fmt.Println("Registering attendance...")
break
}
if err := chromedp.Reload().Do(ctx); err != nil {
return err
}
time.Sleep(time.Duration(waitTime) * time.Second)
}
return nil
}),
chromedp.WaitVisible(btnMarcarPresenca),
chromedp.Click(btnMarcarPresenca),
// Wait and check alert message
chromedp.ActionFunc(func(ctx context.Context) error {
ch := make(chan string, 1)
chromedp.ListenTarget(ctx, func(ev any) {
if ev, ok := ev.(*page.EventJavascriptDialogOpening); ok {
ch <- ev.Message
}
})
select {
case alertMessage := <-ch:
if strings.Contains(alertMessage, "sucesso") {
fmt.Println("Attendance registered successfully!")
} else if strings.Contains(alertMessage, "anteriormente") {
fmt.Println("Attendance previously registered!")
} else {
fmt.Println("Error registering attendance: ", alertMessage)
}
case <-time.After(time.Duration(waitTime) * time.Second):
return fmt.Errorf("timeout waiting for alert")
}
return nil
}),
); err != nil {
log.Printf("Attempt %d: Failed to register attendance: %v", attempt, err)
time.Sleep(5 * time.Second)
continue
}
return nil
}
return fmt.Errorf("failed to register attendance after %d attempts", maxRetries)
}