Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 146 additions & 68 deletions backends/handle/client.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
package handle

import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"

"github.com/ugent-library/biblio-backoffice/models"
"golang.org/x/crypto/ssh"
)

type Config struct {
BaseURL string
FrontEndBaseURL string
Prefix string
Username string
Password string
ADMID string
ADMPrivateKey string
}

type Client struct {
config Config
http *http.Client
config Config
http *http.Client
sessionId string
}

type authResponse struct {
SessionID string `json:"sessionId,omitempty"`
Error string `json:"error,omitempty"`
ID string `json:"id,omitempty"`
Authenticated bool `json:"authenticated"`
}

func NewClient(c Config) *Client {
Expand All @@ -29,46 +45,24 @@ func NewClient(c Config) *Client {
}
}

// func (c *Client) get(path string, qp url.Values, responseData any) (*http.Response, error) {
// req, err := c.newRequest("GET", path, qp)
// if err != nil {
// return nil, err
// }
// req.SetBasicAuth(c.config.Username, c.config.Password)
// return c.doRequest(req, responseData)
// }

func (c *Client) put(path string, qp url.Values, responseData any) (*http.Response, error) {
req, err := c.newRequest("PUT", path, qp)
func (c *Client) put(path string, requestBody io.Reader, responseData any) (*http.Response, error) {
req, err := c.newRequest(http.MethodPut, path, requestBody)
if err != nil {
return nil, err
}
req.SetBasicAuth(c.config.Username, c.config.Password)
return c.doRequest(req, responseData)
}

// func (c *Client) delete(path string, qp url.Values, responseData any) (*http.Response, error) {
// req, err := c.newRequest("DELETE", path, qp)
// if err != nil {
// return nil, err
// }
// req.SetBasicAuth(c.config.Username, c.config.Password)
// return c.doRequest(req, responseData)
// }

func (c *Client) newRequest(method, path string, vals url.Values) (*http.Request, error) {
func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request, error) {
url := c.config.BaseURL + path
if vals != nil {
url = url + "?" + vals.Encode()
}

req, err := http.NewRequest(method, url, nil)
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}

req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Add("Authorization", fmt.Sprintf(`handle version="0",sessionId="%s"`, c.sessionId))

return req, nil
}
Expand All @@ -87,43 +81,127 @@ func (c *Client) doRequest(req *http.Request, responseData any) (*http.Response,
return res, nil
}

// func (client *Client) GetHandle(localId string) (*models.Handle, error) {
// h := &models.Handle{}
// _, err := client.get(
// fmt.Sprintf("/%s/LU-%s", client.config.Prefix, localId),
// nil,
// h,
// )
// if err != nil {
// return nil, err
// }
// return h, nil
// }

func (client *Client) UpsertHandle(localId string) (*models.Handle, error) {
h := &models.Handle{}
qp := url.Values{}
qp.Add("url", fmt.Sprintf("%s/%s", client.config.FrontEndBaseURL, localId))
_, err := client.put(
fmt.Sprintf("/%s/LU-%s", client.config.Prefix, localId),
qp,
h,
)
func (c *Client) UpsertHandle(localId string) (*models.UpsertHandleResponse, error) {
if !c.authenticated() {
if err := c.authenticate(); err != nil {
return nil, err
}
}

handle := fmt.Sprintf("%s/LU-%s", c.config.Prefix, localId)
handleReq := &models.UpsertHandleRequest{
ResponseCode: 1,
Handle: handle,
Values: []*models.HandleValue{
{
Index: 1,
Type: "URL",
Data: map[string]any{
"format": "string",
"value": fmt.Sprintf("%s/%s", c.config.FrontEndBaseURL, localId),
},
},
{
Index: 100,
Type: "HS_ADMIN",
Data: map[string]any{
"format": "admin",
"value": map[string]any{
"handle": c.config.ADMID,
"index": 200,
"permissions": "111111111111", //TODO
},
},
},
},
}
handleReqBytes, _ := json.MarshalIndent(handleReq, "", " ")
handleRes := &models.UpsertHandleResponse{}

_, err := c.put("/api/handles/"+handle+"?overwrite=true", bytes.NewReader(handleReqBytes), handleRes)
if err != nil {
return nil, err
}
return h, nil

return handleRes, nil
}

// func (client *Client) DeleteHandle(localId string) (*models.Handle, error) {
// h := &models.Handle{}
// _, err := client.delete(
// fmt.Sprintf("/%s/LU-%s", client.config.Prefix, localId),
// nil,
// h,
// )
// if err != nil {
// return nil, err
// }
// return h, nil
// }
func (c *Client) authenticated() bool {
return c.sessionId != ""
}

func (c *Client) authenticate() error {
rawPriv, err := ssh.ParseRawPrivateKey([]byte(c.config.ADMPrivateKey))
if err != nil {
return err
}
priv := rawPriv.(*rsa.PrivateKey)

var nonce string
var nonceBytes []byte
var cnonce string
var cnonceBytes []byte = make([]byte, 16)
var sessionId string

// generate session (without authorization)
cnonce2Bytes := make([]byte, 16)
rand.Read(cnonce2Bytes)
reqBody, _ := json.Marshal(map[string]string{
"version": "0",
"cnonce": base64.StdEncoding.EncodeToString(cnonce2Bytes),
})
req, _ := http.NewRequest(http.MethodPost, c.config.BaseURL+"/api/sessions", bytes.NewReader(reqBody))
res, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("unable to create session: %w", err)
}
resH := make(map[string]string)
if err := json.NewDecoder(res.Body).Decode(&resH); err != nil {
return fmt.Errorf("failed to decode session response: %w", err)
}
nonce = resH["nonce"]
sessionId = resH["sessionId"]
nonceBytes, _ = base64.StdEncoding.DecodeString(nonce)

//authenticate
rand.Read(cnonceBytes)
cnonce = base64.StdEncoding.EncodeToString(cnonceBytes)
msg := make([]byte, 0, len(nonceBytes)+len(cnonceBytes))
msg = append(msg, nonceBytes...)
msg = append(msg, cnonceBytes...)
hashGen := sha256.New()
hashGen.Write(msg)
hash := hashGen.Sum(nil)
sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, hash)
if err != nil {
return fmt.Errorf("unable to sign message: %w", err)
}
body, _ := json.MarshalIndent(map[string]string{
"version": "0",
"sessionId": sessionId,
"id": c.config.ADMID,
"cnonce": cnonce,
"type": "HS_PUBKEY",
"alg": "SHA256",
"signature": base64.StdEncoding.EncodeToString(sig),
}, "", " ")
req, _ = http.NewRequest(http.MethodPost, c.config.BaseURL+"/api/sessions/this", bytes.NewReader(body))
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Length", fmt.Sprint(len(body)))
res, err = c.http.Do(req)
if err != nil {
return fmt.Errorf("authentication failed: %w", err)
}
authRes := &authResponse{}
if err := json.NewDecoder(res.Body).Decode(authRes); err != nil {
return fmt.Errorf("unable to decode authentication response: %w", err)
}

c.sessionId = authRes.SessionID

if authRes.Authenticated {
return nil
}
return errors.New("authentication failed: " + authRes.Error)
}
2 changes: 1 addition & 1 deletion backends/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ type DatasetListExporter interface {
type DatasetListExporterFactory func(io.Writer) DatasetListExporter

type HandleService interface {
UpsertHandle(string) (*models.Handle, error)
UpsertHandle(string) (*models.UpsertHandleResponse, error)
}

const MissingValue = "missing"
Expand Down
4 changes: 2 additions & 2 deletions cli/backends.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ func newServices() *backends.Services {
BaseURL: config.Handle.URL,
FrontEndBaseURL: fmt.Sprintf("%s/publication", config.Frontend.URL),
Prefix: config.Handle.Prefix,
Username: config.Handle.Username,
Password: config.Handle.Password,
ADMID: config.Handle.ADMID,
ADMPrivateKey: config.Handle.ADMPrivateKey,
},
)
}
Expand Down
10 changes: 5 additions & 5 deletions cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ type Config struct {
MongoDBURL string `env:"MONGODB_URL"`
APIKey string `env:"API_KEY"`
Handle struct {
Enabled bool `env:"ENABLED"`
URL string `env:"URL"`
Prefix string `env:"PREFIX"`
Username string `env:"USERNAME"`
Password string `env:"PASSWORD"`
Enabled bool `env:"ENABLED"`
URL string `env:"URL"`
Prefix string `env:"PREFIX"`
ADMID string `env:"ADM_ID"` // e.g. 300:0.NA/1854
ADMPrivateKey string `env:"ADM_PRIVATE_KEY"` // rsa private key
} `envPrefix:"HDL_SRV_"`
OAI struct {
APIURL string `env:"API_URL"`
Expand Down
63 changes: 61 additions & 2 deletions cli/create_handles_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
)

func init() {
createHandles.Flags().Bool("force", false, "force a recreation of all handle records")
rootCmd.AddCommand(createHandles)
}

Expand All @@ -23,6 +24,12 @@ var createHandles = &cobra.Command{
return errors.New("handle server updates are not enabled")
}

if force, _ := cmd.Flags().GetBool("force"); !force {
recreatePublicationHandles(services)
recreateDatasetHandles(services)
return nil
}

createPublicationHandles(services)
createDatasetHandles(services)

Expand All @@ -38,7 +45,7 @@ func createPublicationHandles(services *backends.Services) {

repo.EachPublicationWithoutHandle(func(p *models.Publication) bool {
h, e := services.HandleService.UpsertHandle(p.ID)
if err != nil {
if e != nil {
err = fmt.Errorf("error adding handle for publication %s: %w", p.ID, e)
return false
} else if !h.IsSuccess() {
Expand Down Expand Up @@ -71,7 +78,7 @@ func createDatasetHandles(services *backends.Services) {

repo.EachDatasetWithoutHandle(func(d *models.Dataset) bool {
h, e := services.HandleService.UpsertHandle(d.ID)
if err != nil {
if e != nil {
err = fmt.Errorf("error adding handle for dataset %s: %w", d.ID, e)
return false
} else if !h.IsSuccess() {
Expand All @@ -95,3 +102,55 @@ func createDatasetHandles(services *backends.Services) {

logger.Info(fmt.Sprintf("created %d dataset handles", n))
}

func recreatePublicationHandles(services *backends.Services) {
repo := services.Repo

var n int
var err error

repo.EachPublicationWithHandle(func(p *models.Publication) bool {
h, e := services.HandleService.UpsertHandle(p.ID)
if e != nil {
err = fmt.Errorf("error adding handle for publication %s: %w", p.ID, e)
return false
} else if !h.IsSuccess() {
err = fmt.Errorf("error adding handle for publication %s: %s", p.ID, h.Message)
return false
}
n++
return true
})

if err != nil {
logger.Error(err.Error())
}

logger.Info(fmt.Sprintf("created %d publication handles", n))
}

func recreateDatasetHandles(services *backends.Services) {
repo := services.Repo

var n int
var err error

repo.EachDatasetWithHandle(func(d *models.Dataset) bool {
h, e := services.HandleService.UpsertHandle(d.ID)
if e != nil {
err = fmt.Errorf("error adding handle for dataset %s: %w", d.ID, e)
return false
} else if !h.IsSuccess() {
err = fmt.Errorf("error adding handle for dataset %s: %s", d.ID, h.Message)
return false
}
n++
return true
})

if err != nil {
logger.Error(err.Error())
}

logger.Info(fmt.Sprintf("recreated %d dataset handles", n))
}
Loading