Skip to content

Instantly share code, notes, and snippets.

@Bo0mer

Bo0mer/main.go Secret

Last active May 10, 2022
Embed
What would you like to do?
Yellowfin Upload Avatar
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
func main() {
var (
addr = os.Getenv("YF_ADDRESS")
username = os.Getenv("YF_USERNAME")
password = os.Getenv("YF_PASSWORD")
filename = os.Getenv("AVATAR_FILE")
userIDString = os.Getenv("YF_AVATAR_CHANGE_USER_ID")
)
ctx := context.Background()
yf, err := NewClient(addr, username, password)
if err != nil {
log.Fatalf("error initializing yellowfin client: %v", err)
}
imageBytes, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatalf("error reading file contents: %v", err)
}
userID, err := strconv.Atoi(userIDString)
if err != nil {
log.Fatalf("invalid user id provided: %v", err)
}
err = yf.UpdateUserAvatar(ctx, userID, filename, imageBytes)
if err != nil {
log.Fatalf("error updating avatar: %s", err)
}
}
const defaultOrg = ""
type token struct {
raw string
exp time.Time
}
func parseToken(t string) (*token, error) {
parts := strings.Split(t, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid number of parts: %d", len(parts))
}
body := parts[1]
bodyString, err := base64.RawStdEncoding.DecodeString(body)
if err != nil {
return nil, fmt.Errorf("invalid token body: %w", err)
}
// Format is as follows.
// {"role":"YFADMIN","ti":109298,"person":"5","client":"1","exp":1647519351,"iat":1647518151}
var claims struct {
Role string `json:"role"`
TI int64 `json:"ti"`
Person string `json:"person"`
Client string `json:"client"`
Expiry int64 `json:"exp"`
IssuedAt int64 `json:"iat"`
}
err = json.Unmarshal([]byte(bodyString), &claims)
if err != nil {
return nil, fmt.Errorf("invalid token body: %w", err)
}
return &token{
raw: t,
exp: time.Unix(claims.Expiry, 0).UTC(),
}, nil
}
func (t *token) Valid() bool {
return t != nil && t.exp.After(time.Now().UTC().Add(-5*time.Minute))
}
func (t *token) Raw() string {
return t.raw
}
type Client struct {
httpClient *http.Client
addr string
username string
password string
refreshToken string
mu sync.Mutex // guards
accessToken *token
}
func NewClient(addr, username, password string) (*Client, error) {
c := &Client{
httpClient: http.DefaultClient,
addr: addr,
username: username,
password: password,
}
var (
ctx = context.Background()
err error
)
c.refreshToken, err = c.issueRefreshToken(ctx, defaultOrg)
if err != nil {
return nil, fmt.Errorf("error authenticating client: %w", err)
}
return c, nil
}
func (c *Client) UpdateUserAvatar(ctx context.Context, userID int, filename string, file []byte) error {
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
formWriter, err := writer.CreateFormFile("avatar", filename)
if err != nil {
return err
}
formWriter.Write(file)
writer.Close()
req, err := c.newMultipartRequestWithToken(ctx, "POST", fmt.Sprintf("/api/users/%d/avatar", userID), body, writer.FormDataContentType())
if err != nil {
return err
}
resp, err := c.do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func (c *Client) issueRefreshToken(ctx context.Context, org string) (string, error) {
body := new(bytes.Buffer)
err := json.NewEncoder(body).Encode(struct {
Username string `json:"userName"`
Password string `json:"password"`
Organization string `json:"clientOrgRef"`
}{c.username, c.password, org})
if err != nil {
return "", fmt.Errorf("error encoding request body: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", c.addr+"/api/refresh-tokens", body)
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/vnd.yellowfin.api-v2+json;")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization",
fmt.Sprintf("YELLOWFIN ts=%d, nonce=%s",
time.Now().UnixMilli(),
uuid.Must(uuid.NewRandom()).String(),
))
resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("error performing request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected response code: %d", resp.StatusCode)
}
d := struct {
SecurityToken string `json:"securityToken"`
}{}
err = json.NewDecoder(resp.Body).Decode(&d)
if err != nil {
return "", err
}
return d.SecurityToken, nil
}
func (c *Client) token(ctx context.Context) (string, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.accessToken.Valid() {
return c.accessToken.Raw(), nil
}
var err error
c.accessToken, err = c.issueAccessToken(ctx, c.refreshToken)
if err != nil {
return "", fmt.Errorf("error issuing access token: %w", err)
}
return c.accessToken.Raw(), nil
}
func (c *Client) issueAccessToken(ctx context.Context, refreshToken string) (*token, error) {
req, err := c.newRequestWithToken(ctx, refreshToken, "POST", "/api/access-tokens", nil)
if err != nil {
return nil, err
}
resp, err := c.do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected http status code: %d", resp.StatusCode)
}
d := struct {
SecurityToken string `json:"securityToken"`
}{}
err = json.NewDecoder(resp.Body).Decode(&d)
if err != nil {
return nil, fmt.Errorf("error parsing response: %w", err)
}
return parseToken(d.SecurityToken)
}
func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
token, err := c.token(ctx)
if err != nil {
return nil, err
}
return c.newRequestWithToken(ctx, token, method, path, body)
}
func (c *Client) newRequestWithToken(ctx context.Context, t, method, path string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, c.addr+path, body)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
req.Header.Set("Accept", "application/vnd.yellowfin.api-v2+json;")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization",
fmt.Sprintf("YELLOWFIN ts=%d, nonce=%s, token=%s",
time.Now().UnixMilli(),
uuid.Must(uuid.NewRandom()).String(),
t,
),
)
return req, nil
}
func (c *Client) newMultipartRequestWithToken(ctx context.Context, method, path string, body io.Reader, contentType string) (*http.Request, error) {
token, err := c.token(ctx)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, method, c.addr+path, body)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
req.Header.Set("Accept", "application/vnd.yellowfin.api-v2+json;")
req.Header.Set("Content-Type", contentType)
req.Header.Set("Authorization",
fmt.Sprintf("YELLOWFIN ts=%d, nonce=%s, token=%s",
time.Now().UnixMilli(),
uuid.Must(uuid.NewRandom()).String(),
token,
),
)
return req, nil
}
func (c *Client) do(req *http.Request) (*http.Response, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error performing request: %w", err)
}
if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
r := io.LimitReader(resp.Body, 1024*1024*1024)
body, err := io.ReadAll(r)
if err == nil {
return nil, fmt.Errorf("unexpected http status code %d with body:\n%s", resp.StatusCode, body)
}
return nil, fmt.Errorf("unexpected http status code: %d", resp.StatusCode)
}
return resp, nil
}
func (c *Client) doReq(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
req, err := c.newRequest(ctx, method, path, body)
if err != nil {
return nil, err
}
resp, err := c.do(req)
if err != nil {
return nil, err
}
return resp, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment