Skip to content

Commit

Permalink
feat: provide first cli tool for harica api
Browse files Browse the repository at this point in the history
  • Loading branch information
fritterhoff committed Dec 12, 2024
1 parent 8df5770 commit 0eaf6f2
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 66 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ go.work.sum

# env file
.env
harica
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# harica-client
# Inofficial Client for the HARICA API

177 changes: 114 additions & 63 deletions client.go → client/client.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package main
package client

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

Expand All @@ -11,47 +11,62 @@ import (
"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/hm-edu/harica/models"
"github.com/pquerna/otp/totp"
)

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

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

func NewClient(user, password, totpSeed string) (*Client, error) {
type Option func(*Client)

func NewClient(user, password, totpSeed string, options ...Option) (*Client, error) {
c := Client{}
for _, option := range options {
option(&c)
}
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)
_, err = s.NewJob(gocron.DurationJob(RefreshInterval), gocron.NewTask(func() {
err := c.prepareClient(user, password, totpSeed)
if err != nil {
slog.Error("failed to prepare client", slog.Any("error", err))
return
}
}))
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 WithDebug(debug bool) Option {
return func(c *Client) {
c.debug = debug
}
}

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

Expand Down Expand Up @@ -95,7 +110,7 @@ func (c *Client) loginTotp(user, password, totpSeed string) error {
SetBody(map[string]string{"email": user, "password": password, "token": otp}).
Post(BaseURL + LoginPathTotp)
if err != nil {
// handle error
return err
}
tokenResp := strings.Trim(resp.String(), "\"")
_, _, err = jwt.NewParser().ParseUnverified(tokenResp, jwt.MapClaims{})
Expand All @@ -104,16 +119,11 @@ func (c *Client) loginTotp(user, password, totpSeed string) error {
}
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
})
token, err := getVerificationToken(r)
if err != nil {
return err
}
r = r.SetHeaderVerbatim("RequestVerificationToken", token).SetDebug(c.debug)
c.client = r
return nil
}
Expand All @@ -132,39 +142,37 @@ func (c *Client) login(user, password string) error {
if err != nil {
return err
}
c.currentToken = strings.Trim(resp.String(), "\"")
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.Add("RequestVerificationToken", verificationToken)
return nil
})
token, err := getVerificationToken(r)
if err != nil {
return err
}
r = r.SetHeaderVerbatim("RequestVerificationToken", token).SetDebug(c.debug)
c.client = r
return nil
}

func (c *Client) GetRevocationReasons() error {
resp, err := c.client.R().Post(BaseURL + "/api/Certificate/GetRevocationReasons")
resp, err := c.client.R().Post(BaseURL + RevocationReasonsPath)
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")
resp, err := c.client.R().Post(BaseURL + DomainValidationsPath)
if err != nil {
return err
}
// handle response
fmt.Print(resp.String())
return nil
}
Expand All @@ -174,69 +182,112 @@ type Domain struct {
}

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

func (c *Client) GetCertificate(id string) (models.CertificateResponse, error) {
var cert models.CertificateResponse
_, err := c.client.R().
SetResult(&cert).
SetHeader("Content-Type", "application/json").
SetBody(map[string]interface{}{"id": id}).
Post(BaseURL + "/api/Certificate/GetCertificate")
if err != nil {
return cert, err
}
return cert, 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")
_, 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) RequestCertificate(domains []models.DomainResponse, csr string, transactionType string) (models.CertificateRequestResponse, error) {
domainJsonBytes, _ := json.Marshal(domains)
domainJson := string(domainJsonBytes)
var result models.CertificateRequestResponse
_, err := c.client.R().
SetHeader("Content-Type", "multipart/form-data").
SetResult(&result).
SetMultipartFormData(map[string]string{
"domains": domainJson,
"domainsString": domainJson,
"csr": csr,
"isManualCsr": "true",
"consentSameKey": "true",
"transactionType": transactionType,
"duration": "1",
}).
Post(BaseURL + "/api/ServerCertificate/RequestServerCertificate")
if err != nil {
return result, err
}
return result, nil
}

func (c *Client) GetPendingReviews() ([]models.ReviewResponse, error) {
pending := []models.ReviewResponse{}
var 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{}}).
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).
func (c *Client) ApproveRequest(id, message, value string) error {
_, err := c.client.R().
SetHeader("Content-Type", "multipart/form-data").
SetBody(map[string]string{"reviewId": id, "isValid": "true", "informApplicant": "true", "reviewMessage": message, "reviewValue": value}).
SetMultipartFormData(map[string]string{
"reviewId": id,
"isValid": "true",
"informApplicant": "true",
"reviewMessage": message,
"reviewValue": value,
}).
Post(BaseURL + "/api/OrganizationValidatorSSL/UpdateReviews")

if err != nil {
return err
}
return nil
}

func (c *Client) Shutdown() {
func (c *Client) Shutdown() error {
err := c.scheduler.Shutdown()
if err != nil {
// handle error
return err
}
return nil
}
2 changes: 1 addition & 1 deletion helper.go → client/helper.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package client

import (
"strings"
Expand Down
Loading

0 comments on commit 0eaf6f2

Please sign in to comment.