Initial commit

master
Jonas Letzbor 2023-10-04 14:48:58 +02:00
commit 3346dacc67
Signed by: RPJosh
GPG Key ID: 46D72F589702E55A
13 changed files with 494 additions and 0 deletions

6
.gitignore vendored 100644
View File

@ -0,0 +1,6 @@
/scripts/temp_path
/config-dev.yaml
# Build binarys
/infoniqa
/infoniqa.exe

View File

@ -0,0 +1,92 @@
package main
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"git.rpjosh.de/RPJosh/go-logger"
"gitea.hama.de/LFS/infoniqa-scripts/internal/infoniqa"
"gitea.hama.de/LFS/infoniqa-scripts/internal/models"
)
func main() {
defer logger.CloseFile()
// Configure logger
logger.SetGlobalLogger(logger.GetLoggerFromEnv(&logger.Logger{
ColoredOutput: true,
Level: logger.LevelInfo,
PrintSource: true,
File: &logger.FileLogger{},
}))
// Check if the first argument is --help
if len(os.Args) == 1 || os.Args[1] == "--help" || os.Args[1] == "-h" || os.Args[1] == "?" {
printHelp()
}
// Get the configuration of the app
config := models.GetConfig()
logger.Info("Program started")
// Initialize infoniqa client
inf, err := infoniqa.NewInfoniqa(config.Url, config.Username, config.Password)
if err != nil {
logger.Fatal("Initialization of infoniqa client was not successfull: %s", err)
}
switch strings.ToLower(os.Args[1]) {
case "kommen":
if err := inf.Kommen(); err != nil {
logger.Fatal("Failed to book 'kommen': %s", err)
}
case "gehen":
if err := inf.Gehen(); err != nil {
logger.Fatal("Failed to book 'gehen': %s", err)
}
case "abwesend":
if len(os.Args) <= 2 {
logger.Fatal("Missing required parameter for option 'abwesend'")
}
// Parse the second argument to an int (amount of minutes)
minutes, err := strconv.Atoi(os.Args[2])
if err != nil {
logger.Fatal("Failed to convert the argument %q to a number: %s", os.Args[2], err)
}
// Buche kommen und dann gehen
if err := inf.Gehen(); err != nil {
logger.Fatal("Failed to book 'kommen': %s", err)
}
logger.Info("Waiting %d minutes....", minutes)
time.Sleep(time.Duration(minutes * int(time.Minute)))
if err := inf.Kommen(); err != nil {
logger.Fatal("Failed to book 'kommen': %s", err)
}
default:
logger.Fatal("Invalid argument given: %q", os.Args[0])
}
logger.Info("Program executed successfull")
}
// printHelp prints a help for the usage of this program and exists the program afterwards
func printHelp() {
fmt.Println(`
Command line arguments:
kommen Books "kommen"
gehen Books "gehen"
abwesend [minutes] Books "gehen" and waits the given amount of minutes for booking "kommen"
--help Prints this help
Environment variables:
INFONIQA_CONFIG File path of the configuration file to use (defaulting to ./config.yaml)`)
os.Exit(0)
}

7
config.yaml 100644
View File

@ -0,0 +1,7 @@
# Username to login
username: "0103710"
# Password of the user
password: "mySecret"
# Base URL of infoniqa
url: https://hama.infoniqa.co.at/TIMEWEB254834

9
go.mod 100644
View File

@ -0,0 +1,9 @@
module gitea.hama.de/LFS/infoniqa-scripts
go 1.19
require (
git.rpjosh.de/RPJosh/go-logger v1.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

9
go.sum 100644
View File

@ -0,0 +1,9 @@
git.rpjosh.de/RPJosh/go-logger v1.2.1 h1:yzx9+mFIC+2TXI93EGeuRvow++yte8bpQ0GtDvLnnIQ=
git.rpjosh.de/RPJosh/go-logger v1.2.1/go.mod h1:iD3KaRyOIkYMj7E+xFMn5uDVCzW1lSJQopz1Fl1+BSM=
git.rpjosh.de/RPJosh/go-logger v1.3.0 h1:oKjOEMC5RSge3qhyoXaegkvotNYOug67CADDnBKDXQU=
git.rpjosh.de/RPJosh/go-logger v1.3.0/go.mod h1:iD3KaRyOIkYMj7E+xFMn5uDVCzW1lSJQopz1Fl1+BSM=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,56 @@
package infoniqa
import (
"fmt"
"net/url"
"strings"
)
func (inf Infoniqa) Kommen() error {
if inf.lastBookingStatus == 1 {
return fmt.Errorf("last booking was already 'kommen'")
}
return inf.book(1)
}
func (inf *Infoniqa) Gehen() error {
if inf.lastBookingStatus == 2 {
return fmt.Errorf("last booking was already 'gehen'")
}
return inf.book(2)
}
// book books the given action that is identified behind the hotkey
func (inf *Infoniqa) book(hotkey int) error {
// Build body with x-www-form-urlencoded content type (First without password and second with callback)
data := url.Values{}
data.Set("__WPPS", `u`)
data.Set("__EVENTARGUMENT", ``)
data.Set("__EVENTTARGET", ``)
data.Set("__VIEWSTATEGENERATOR", inf.viewStateGenerator)
data.Set("__VIEWSTATE", inf.viewstate)
data.Set("HotKey_SI_KTO_NR", fmt.Sprintf("%d", hotkey))
// Request with password
req := inf.getRequest("POST", "/includes/checkworkflow.aspx", strings.NewReader(data.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Origin", "https://hama.infoniqa.co.at")
// Execute request
res, err := inf.client.Do(req)
if err != nil {
return err
}
// Check status code
if res.StatusCode != 200 {
return fmt.Errorf("invalid status code (%d)", res.StatusCode)
}
inf.lastBookingStatus = hotkey
return nil
}

View File

@ -0,0 +1,34 @@
package infoniqa
import (
"net/http"
"net/url"
"sync"
)
type Jar struct {
lk sync.Mutex
cookies map[string][]*http.Cookie
}
func NewJar() *Jar {
jar := new(Jar)
jar.cookies = make(map[string][]*http.Cookie)
return jar
}
// SetCookies handles the receipt of the cookies in a reply for the
// given URL. It may or may not choose to save the cookies, depending
// on the jar's policy and implementation.
func (jar *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
jar.lk.Lock()
jar.cookies[u.Host] = append(jar.cookies[u.Host], cookies...)
jar.lk.Unlock()
}
// Cookies returns the cookies to send in a request for the given URL.
// It is up to the implementation to honor the standard cookie use
// restrictions such as in RFC 6265.
func (jar *Jar) Cookies(u *url.URL) []*http.Cookie {
return jar.cookies[u.Host]
}

View File

@ -0,0 +1,180 @@
package infoniqa
import (
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"git.rpjosh.de/RPJosh/go-logger"
)
// Infoniqa is the base struct with all available functions that are
// implemented within this program
type Infoniqa struct {
// URL of the infoniqa instance
BaseUrl string
// Username to log in
Username string
// Password for the user
Password string
// The viewstate that was provided by infoniqa from the last request
viewstate string
// The last view state generator that was provided by infoniqa from the last request
viewStateGenerator string
// Client for executing the request. This does also store the cookies
client http.Client
// Last booking status (0 = unknown, 1 = kommen, 2 = gehen)
lastBookingStatus int
}
// NewInfoniqa creates a new infoniqa instance with the provided credentials.
// This function will execute an initialization sequence so that the other functions
// of this struct can be used.
// When this initialization sequence does fail, it will return an error
func NewInfoniqa(baseUrl string, username string, password string) (*Infoniqa, error) {
// Create instance with parameters
inf := &Infoniqa{
BaseUrl: baseUrl,
Username: username,
Password: password,
client: http.Client{Timeout: 5 * time.Second, Jar: NewJar()},
}
// Get the login page to login
_, res, err := inf.executeRequest(inf.getRequest("GET", "/Default.aspx", nil))
if err != nil {
return nil, fmt.Errorf("fetching of login page failed: %s", err)
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("unable to contact infoniqa site. Got status %d", res.StatusCode)
}
// Execute the login
if err := inf.login(); err != nil {
return nil, fmt.Errorf("failed to login: %s", err)
}
return inf, nil
}
// getRequest returns a new request to the infoniqa API
func (inf *Infoniqa) getRequest(method string, path string, body io.Reader) *http.Request {
req, err := http.NewRequest(method, inf.BaseUrl+path, body)
if err != nil {
logger.Error("Failed to get infoniqa request: %s", err)
}
// Set common headers
req.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
return req
}
// executeRequest executes the given requests and updates aspx specific variables like the viewstate
// accordingly
func (inf *Infoniqa) executeRequest(req *http.Request) (body string, resp *http.Response, err error) {
// Execute the request
res, err := inf.client.Do(req)
if err != nil {
return "", nil, err
}
// Read the body
defer res.Body.Close()
resBody, err := io.ReadAll(res.Body)
if err != nil {
return "", res, fmt.Errorf("reading of body failed: %s", err)
}
strBody := string(resBody)
// Get the viewstate from the body
if st, err := inf.findHiddenValue("__VIEWSTATE", strBody); err != nil {
return "", res, fmt.Errorf("no viewstate found in response")
} else {
inf.viewstate = st
}
if st, err := inf.findHiddenValue("__VIEWSTATEGENERATOR", strBody); err != nil {
return "", res, fmt.Errorf("no viewstateGenerator found in response")
} else {
inf.viewStateGenerator = st
}
return string(resBody), res, nil
}
// findHiddenValue searches the given aspx value within the
// response body as a hidden input type.
func (inf *Infoniqa) findHiddenValue(name string, body string) (value string, err error) {
// Get the viewstate from the body
regex, err := regexp.Compile(`<input.type="hidden".name="` + name + `".id="` + name + `" value="(?P<Viewstate>.*)".\/>`)
if err != nil {
return "", fmt.Errorf("failed to compile regex: %s", err)
}
matches := regex.FindStringSubmatch(body)
index := regex.SubexpIndex("Viewstate")
if index >= len(matches) {
return "", fmt.Errorf("no viewstate found in response")
}
return matches[index], nil
}
// login calls the login endpoint of infoniqa and sets the cookie and viewstate
// for all further requests correctly
func (inf *Infoniqa) login() error {
// Build body with x-www-form-urlencoded content type (First without password and second with callback)
data := url.Values{}
data.Set("__EVENTTARGET", `ctl00$ContentPlaceHolder1$PanelLogin$PageControl$Login1$btnApgLogin`)
data.Set("__EVENTARGUMENT", `Click`)
data.Set("__VIEWSTATE", inf.viewstate)
data.Set("__VIEWSTATEGENERATOR", inf.viewStateGenerator)
data.Set("ctl00$Logininfo1$CheckPopupControlState", `{"windowsState":"0:0:-1:0:0:0:-10000:-10000:1:0:0:0"}`)
data.Set("ctl00$ContentPlaceHolder1$PanelLogin$PageControl", `{"activeTabIndex":0}`)
data.Set("ctl00$ContentPlaceHolder1$PanelLogin$PageControl$Login1$UserName$State", `{"validationState":""}`)
data.Set("ctl00$ContentPlaceHolder1$PanelLogin$PageControl$Login1$UserName", inf.Username)
data.Set("ctl00$ContentPlaceHolder1$PanelLogin$PageControl$Login1$Password$State", `{"validationState":""}`)
data.Set("ctl00$ContentPlaceHolder1$PanelLogin$PageControl$Login1$Password", inf.Password)
data.Set("ctl00$ContentPlaceHolder1$PanelLogin$PageControl$PasswordRecovery$UserNameContainerID$UserName$State", `{"validationState":""}`)
data.Set("ctl00$ContentPlaceHolder1$PanelLogin$PageControl$PasswordRecovery$UserNameContainerID$UserName", "")
// Request with password
req := inf.getRequest("POST", "/Default.aspx", strings.NewReader(data.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Origin", "https://hama.infoniqa.co.at")
// Execute request
body, res, err := inf.executeRequest(req)
if err != nil {
return err
}
// Check status code
if res.StatusCode != 200 {
return fmt.Errorf("login failed (%d)", res.StatusCode)
}
// Get the last booking status
regex := regexp.MustCompile(`<td.*return overlib\('(?P<State>.*)', CAPTION.*\).*id="Zeitleiste".*<\/td>`)
matches := regex.FindStringSubmatch(body)
index := regex.SubexpIndex("State")
if index >= len(matches) {
logger.Debug("Couldn't find the last booking state")
} else if strings.HasPrefix(matches[index], "KO") {
inf.lastBookingStatus = 1
} else {
inf.lastBookingStatus = 2
logger.Debug("Found last booking state state %q", matches[index])
}
return nil
}

View File

@ -0,0 +1,37 @@
package models
import (
"os"
"git.rpjosh.de/RPJosh/go-logger"
"gitea.hama.de/LFS/infoniqa-scripts/pkg/utils"
"gopkg.in/yaml.v3"
)
type Config struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
Url string `yaml:"url"`
}
// GetConfig reads the configuration from the file and returns this config struct.
// The program will be left if no valid configuration file was found
func GetConfig() *Config {
rtc := Config{}
// Get the configuration path
configPath := utils.GetEnvString("INFONIQA_CONFIG", "./config")
// Parse the file
dat, err := os.ReadFile(configPath)
if err != nil {
logger.Fatal("Failed to read configuration file %q: %s", configPath, err)
}
// Unmarshal
if err := yaml.Unmarshal(dat, &rtc); err != nil {
logger.Fatal("Failed to unmarshal configuration file: %s", err)
}
return &rtc
}

15
pkg/utils/utils.go 100644
View File

@ -0,0 +1,15 @@
package utils
import "os"
// GetEnvString tries to get an environment variable from the system
// as a string value. If the env was not found the given default value
// will be returned
func GetEnvString(name string, defaultValue string) string {
val := defaultValue
if strVal, isSet := os.LookupEnv(name); isSet {
val = strVal
}
return val
}

8
scripts/build.sh 100644
View File

@ -0,0 +1,8 @@
#!/bin/sh
set -e
GOOS=linux GOARCH=amd64 go build -o "infoniqa" ./cmd/infoniqa
GOOS=windows GOARCH=amd64 go build -o "infoniqa.exe" ./cmd/infoniqa
echo "Build finished"

35
scripts/run.cmd 100644
View File

@ -0,0 +1,35 @@
@ECHO OFF
:: Bypass the "Terminate Batch Job" prompt
if "%~1"=="-FIXED_CTRL_C" (
:: Remove the -FIXED_CTRL_C parameter
SHIFT
) ELSE (
:: Run the batch with <NUL and -FIXED_CTRL_C
CALL <NUL %0 -FIXED_CTRL_C %*
GOTO :EOF
)
SET PATH=%PATH%;C:\Windows\System32
:: Custom go temp path
if exist .\scripts\temp_path (
set /p tempDir=< scripts\temp_path
set GOTMPDIR=%tempDir%
)
set args=%1
shift
:start
if [%1] == [] goto done
set args=%args% %1
shift
goto start
:done
:: Debug settings
set LOGGER_LEVEL=DEBUG
set INFONIQA_CONFIG=./config-dev.yaml
nodemon --delay 1s -e go,html,yaml --signal SIGKILL --ignore web/app/ --quiet ^
--exec "echo [Restarting] && go run ./cmd/infoniqa" -- %args% || "exit 1"

6
scripts/run.sh 100644
View File

@ -0,0 +1,6 @@
#!/bin/sh
GREEN='\033[0;32m'
NC='\033[0m'
nodemon --delay 1s -e go,html,yaml --signal SIGTERM --quiet --exec \
'echo "\n'"$GREEN"'[Restarting]'"$NC"'" && go run './cmd/infoniqa' -- "$@" "|| exit 1"