Updated Credentials Storage
This commit is contained in:
@@ -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(¤tURL).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)
|
||||
}
|
||||
Reference in New Issue
Block a user