Initial commit
commit
3346dacc67
|
@ -0,0 +1,6 @@
|
|||
/scripts/temp_path
|
||||
/config-dev.yaml
|
||||
|
||||
# Build binarys
|
||||
/infoniqa
|
||||
/infoniqa.exe
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
|
@ -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"
|
|
@ -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"
|
Loading…
Reference in New Issue