Skip to content

Commit

Permalink
Initial code for harica client
Browse files Browse the repository at this point in the history
  • Loading branch information
fritterhoff committed Dec 11, 2024
1 parent 2c1497e commit 70d45f0
Show file tree
Hide file tree
Showing 7 changed files with 478 additions and 0 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Perform linting of the code using golangci-lint
name: golangci-lint
on:
push:
branches:
- main
pull_request:

permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
# pull-requests: read

jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
242 changes: 242 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package main

import (
"fmt"
"net/http"
"strings"
"time"

"log/slog"

"github.com/go-co-op/gocron/v2"
"github.com/go-resty/resty/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/hm-edu/harica-client/models"
"github.com/pquerna/otp/totp"
)

const (
BaseURL = "https://cm.harica.gr"
LoginPath = "/api/User/Login"
LoginPathTotp = "/api/User/Login2FA"
RefreshInterval = 15 * time.Minute
)

type Client struct {
client *resty.Client
scheduler gocron.Scheduler
currentToken string
}

func NewClient(user, password, totpSeed string) (*Client, error) {
c := Client{}
err := c.prepareClient(user, password, totpSeed)
if err != nil {
return nil, err
}
s, err := gocron.NewScheduler()
if err != nil {
slog.Error("failed to create scheduler", slog.Any("error", err))
return nil, err
}
job, err := s.NewJob(gocron.DurationJob(RefreshInterval), gocron.NewTask(func() {
c.prepareClient(user, password, totpSeed)

Check failure on line 43 in client.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `c.prepareClient` is not checked (errcheck)
}))
if err != nil {
slog.Error("failed to create job", slog.Any("error", err))
return nil, err
}
slog.Info("added job", slog.Any("job", job))
s.Start()
c.scheduler = s
return &c, nil
}

func (c *Client) prepareClient(user, password, totpSeed string) error {
renew := false

if c.currentToken != "" {
// Check JWT
token, _, err := jwt.NewParser().ParseUnverified(c.currentToken, jwt.MapClaims{})
if err != nil {
return err
}
exp, err := token.Claims.GetExpirationTime()
if err != nil {
return err
}
if exp.Before(time.Now()) || exp.Before(time.Now().Add(RefreshInterval)) {
renew = true
}
}
if c.client == nil || c.currentToken == "" || renew {
if totpSeed != "" {
return c.loginTotp(user, password, totpSeed)
} else {
return c.login(user, password)
}
}
return nil
}

func (c *Client) loginTotp(user, password, totpSeed string) error {
r := resty.New()
verificationToken, err := getVerificationToken(r)
if err != nil {
return err
}
otp, err := totp.GenerateCode(totpSeed, time.Now())
if err != nil {
return err
}
resp, err := r.
R().SetHeaderVerbatim("RequestVerificationToken", verificationToken).
SetHeader("Content-Type", "application/json").
SetBody(map[string]string{"email": user, "password": password, "token": otp}).
Post(BaseURL + LoginPathTotp)
if err != nil {

Check failure on line 97 in client.go

View workflow job for this annotation

GitHub Actions / lint

SA9003: empty branch (staticcheck)
// handle error
}
tokenResp := strings.Trim(resp.String(), "\"")
_, _, err = jwt.NewParser().ParseUnverified(tokenResp, jwt.MapClaims{})
if err != nil {
return err
}
c.currentToken = tokenResp
r = r.SetHeaders(map[string]string{"Authorization": c.currentToken})
r.SetPreRequestHook(func(c *resty.Client, r *http.Request) error {
cli := resty.New().SetCookieJar(c.GetClient().Jar)
verificationToken, err := getVerificationToken(cli)
if err != nil {
slog.Error("failed to get verification token", slog.Any("error", err))
return err
}
r.Header["RequestVerificationToken"] = []string{verificationToken}
return nil
})
c.client = r
return nil
}

func (c *Client) login(user, password string) error {
r := resty.New()
verificationToken, err := getVerificationToken(r)
if err != nil {
return err
}
resp, err := r.
R().SetHeaderVerbatim("RequestVerificationToken", verificationToken).
SetHeader("Content-Type", "application/json").
SetBody(map[string]string{"email": user, "password": password}).
Post(BaseURL + LoginPath)
if err != nil {
return err
}
c.currentToken = strings.Trim(resp.String(), "\"")
r = r.SetHeaders(map[string]string{"Authorization": c.currentToken})
r.SetPreRequestHook(func(c *resty.Client, r *http.Request) error {
cli := resty.New().SetCookieJar(c.GetClient().Jar)
verificationToken, err := getVerificationToken(cli)
if err != nil {
slog.Error("failed to get verification token", slog.Any("error", err))
return err
}
r.Header.Add("RequestVerificationToken", verificationToken)
return nil
})
c.client = r
return nil
}

func (c *Client) GetRevocationReasons() error {
resp, err := c.client.R().Post(BaseURL + "/api/Certificate/GetRevocationReasons")
if err != nil {
return err
}
// handle response
data := resp.String()
fmt.Print(data)
return nil
}

func (c *Client) GetDomainValidations() error {
resp, err := c.client.R().Post(BaseURL + "/api/ServerCertificate/GetDomainValidations")
if err != nil {
return err
}
// handle response
fmt.Print(resp.String())
return nil
}

type Domain struct {
Domain string `json:"domain"`
}

func (c *Client) CheckMatchingOrganization(domains []string) ([]models.OrganizationResponse, error) {
domainDto := make([]Domain, 0)
for _, domain := range domains {
domainDto = append(domainDto, Domain{Domain: domain})

Check failure on line 179 in client.go

View workflow job for this annotation

GitHub Actions / lint

SA4010: this result of append is never used, except maybe in other appends (staticcheck)
}
response := []models.OrganizationResponse{}
_, err := c.client.R().SetHeader("Content-Type", "application/json").SetResult(&response).SetBody(domains).Post(BaseURL + "/api/ServerCertificate/CheckMachingOrganization")
if err != nil {
// handle error
return nil, err
}
// handle response
return response, nil
}

func (c *Client) CheckDomainNames(domains []string) ([]models.DomainResponse, error) {
domainDto := make([]Domain, 0)
for _, domain := range domains {
domainDto = append(domainDto, Domain{Domain: domain})
}
domainResp := make([]models.DomainResponse, 0)
_, err := c.client.R().SetResult(&domainResp).SetHeader("Content-Type", "application/json").SetBody(domainDto).Post(BaseURL + "/api/ServerCertificate/CheckDomainNames")
if err != nil {
return nil, err
}
// handle response
return domainResp, nil
}

func (c *Client) RequestCertificate() {

}

func (c *Client) RevokeCertificate() {

}

func (c *Client) GetPendingReviews() ([]models.ReviewResponse, error) {
pending := []models.ReviewResponse{}
_, err := c.client.R().
SetDebug(true).
SetResult(&pending).
SetHeader("Content-Type", "application/json").
SetBody(models.ReviewRequest{StartIndex: 0, Status: "Pending", FilterPostDTOs: []any{}}).
Post(BaseURL + "/api/OrganizationValidatorSSL/GetSSLReviewableTransactions")
if err != nil {
return nil, err
}
return pending, nil
}

func (c *Client) ApproveRequest(id, message, value string) {

c.client.R().
SetDebug(true).
SetHeader("Content-Type", "multipart/form-data").
SetBody(map[string]string{"reviewId": id, "isValid": "true", "informApplicant": "true", "reviewMessage": message, "reviewValue": value}).
Post(BaseURL + "/api/OrganizationValidatorSSL/UpdateReviews")

Check failure on line 233 in client.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `(*github.com/go-resty/resty/v2.Request).Post` is not checked (errcheck)

}

func (c *Client) Shutdown() {
err := c.scheduler.Shutdown()
if err != nil {

Check failure on line 239 in client.go

View workflow job for this annotation

GitHub Actions / lint

SA9003: empty branch (staticcheck)
// handle error
}
}
19 changes: 19 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module github.com/hm-edu/harica-client

go 1.23.3

require (
github.com/go-co-op/gocron/v2 v2.12.4
github.com/go-resty/resty/v2 v2.16.2
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/pquerna/otp v1.4.0
golang.org/x/net v0.27.0
)

require (
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
)
35 changes: 35 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-co-op/gocron/v2 v2.12.4 h1:h1HWApo3T+61UrZqEY2qG1LUpDnB7tkYITxf6YIK354=
github.com/go-co-op/gocron/v2 v2.12.4/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
52 changes: 52 additions & 0 deletions helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"strings"

"github.com/go-resty/resty/v2"
"golang.org/x/net/html"
)

func getVerificationToken(r *resty.Client) (string, error) {
resp, err := r.
R().
Get(BaseURL)
if err != nil {
return "", err
}
doc, err := html.Parse(strings.NewReader(resp.String()))
if err != nil {
return "", err
}
verificationToken := ""
var processHtml func(*html.Node)
processHtml = func(n *html.Node) {
if verificationToken != "" {
return
}
if n.Type == html.ElementNode && n.Data == "input" {
for _, a := range n.Attr {
if a.Key == "name" && a.Val == "__RequestVerificationToken" {
for _, a := range n.Attr {
if a.Key == "value" {
if verificationToken == "" {
verificationToken = a.Val
return
}
}
}
}
}
}

for c := n.FirstChild; c != nil; c = c.NextSibling {
if verificationToken != "" {
return
}
processHtml(c)
}
}

processHtml(doc)
return verificationToken, nil
}
Loading

0 comments on commit 70d45f0

Please sign in to comment.