Skip to content
Snippets Groups Projects
Commit d78a5294 authored by ko_oler's avatar ko_oler
Browse files

добавлен field и urlsigner

parent fa821ebd
No related branches found
No related tags found
No related merge requests found
package files
import (
"context"
"errors"
"fmt"
"net/url"
"reflect"
"git.perx.ru/perxis/perxis-go/pkg/items"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
signer "git.perx.ru/perxis/perxis-go/pkg/urlsigner"
"github.com/mitchellh/mapstructure"
)
const FileTypeName = "file"
type FileParameters struct {
t *FileType
}
func (p FileParameters) Type() field.Type { return p.t }
func (p *FileParameters) Clone(reset bool) field.Parameters { return p }
type FileType struct {
fs Files
signer signer.URLSigner
fileServerUrl string
uploader Uploader
}
func NewFileType(fs Files, signer signer.URLSigner, fileServerUrl string) *FileType {
return &FileType{fs: fs, signer: signer, fileServerUrl: fileServerUrl, uploader: NewUploader()}
}
func (t *FileType) WithUploader(uploader Uploader) *FileType {
t.uploader = uploader
return t
}
func (t FileType) Name() string { return FileTypeName }
func (t *FileType) NewParameters() field.Parameters {
return &FileParameters{t}
}
func (t FileType) Decode(_ context.Context, fld *field.Field, v interface{}) (interface{}, error) {
if v == nil {
return nil, nil
}
var f File
if err := mapstructure.Decode(v, &f); err != nil {
return nil, err
}
return &f, nil
}
func (t FileType) Encode(ctx context.Context, fld *field.Field, v interface{}) (interface{}, error) {
if v == nil {
return nil, nil
}
f, ok := v.(*File)
if !ok {
return nil, fmt.Errorf("FileField encode error: incorrect type: \"%s\", expected \"file\"", reflect.ValueOf(v).Kind())
}
if f.File != nil { // upload from file system
upload, err := t.fs.Upload(ctx, f)
if err != nil {
return nil, err
}
if err = t.uploader.Upload(f.File, upload); err != nil {
return nil, err
}
f = &upload.File
}
u := fmt.Sprintf("%s/%s", t.fileServerUrl, f.ID)
resURL, err := url.Parse(u)
if err != nil {
return nil, err
}
if t.signer != nil {
f.URL = t.signer.Sign(resURL).String()
} else {
f.URL = resURL.String()
}
res := make(map[string]interface{})
if err := mapstructure.Decode(f, &res); err != nil {
return nil, err
}
return res, nil
}
// PreSave - функция буде вызвана перед сохранением поля в Storage. Реализует интерфейс `perxis.PreSaver`
// Выполняет проверку поля является ли файл только что загруженным и переносит его при необходимости для
// постоянного хранения
func (t FileType) PreSave(ctx context.Context, fld *field.Field, v interface{}, itemCtx *items.Context) (interface{}, bool, error) {
if v == nil {
return nil, false, nil
}
f := v.(*File)
if f.ID == "" {
return nil, false, errors.New("FileType: file id required")
}
if !f.Temporary() {
return f, false, nil
}
f, err := t.fs.MoveUpload(ctx, &MultipartUpload{File: *f})
if err != nil {
return nil, false, err
}
return f, true, nil
}
// Field - создает новое поле Field типа FileType
// FileType должен быть предварительно создан `NewFileType` и зарегистрирован `field.Register`
func Field(o ...interface{}) *field.Field {
t, ok := field.GetType(FileTypeName)
if !ok {
panic("field file type not registered")
}
return field.NewField(t.NewParameters(), o...)
}
func (t *FileType) IsEmpty(v interface{}) bool {
if v == nil {
return true
}
f, ok := v.(*File)
return !ok || f.ID == ""
}
func (p FileParameters) GetField(path string) (fld *field.Field) {
switch path {
case "id", "name", "mimeType", "url", "key":
return field.String()
case "size":
return field.Number(field.NumberFormatInt)
default:
return nil
}
}
func init() {
// По умолчанию без FS
// Если нужны подписанные URL, и загрузка на FS, нужно зарегистрировать корректный типа
// См. cmd/content/command/server.go:195
field.Register(NewFileType(nil, nil, ""))
}
package urlsigner
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"net/url"
"strconv"
"strings"
"time"
"git.perx.ru/perxis/perxis-go/pkg/data"
)
type URLSigner interface {
Sign(url *url.URL) *url.URL
Check(url *url.URL) bool
}
const (
defaultSignatureExpire = 15 * time.Minute
defaultQueryKey = "sign"
separator = "|"
saltSize = 16
)
type urlSigner struct {
secret string
expirationTime time.Duration
queryKey string
params []string
}
func NewURLSigner(secret string, expirationTime time.Duration, queryKey string, params ...string) URLSigner {
if len(params) == 0 {
params = make([]string, 0)
}
if expirationTime == 0 {
expirationTime = defaultSignatureExpire
}
if queryKey == "" {
queryKey = defaultQueryKey
}
return &urlSigner{
secret: secret,
expirationTime: expirationTime,
queryKey: queryKey,
params: params,
}
}
func (s *urlSigner) Sign(u *url.URL) *url.URL {
q := u.Query()
h := sha256.New()
salt := data.GenerateRandomString(saltSize)
for _, p := range s.params {
if vv := q[p]; len(vv) > 0 {
for _, v := range vv {
h.Write([]byte(v))
}
}
}
h.Write([]byte(u.Path))
h.Write([]byte(s.expirationTime.String()))
h.Write([]byte(s.secret))
h.Write([]byte(salt))
expTime := time.Now().Add(s.expirationTime).Unix()
res := strings.Join([]string{strconv.FormatInt(expTime, 16), salt, string(h.Sum(nil))}, separator)
q.Set(s.queryKey, base64.URLEncoding.EncodeToString([]byte(res)))
u.RawQuery = q.Encode()
return u
}
func (s *urlSigner) Check(u *url.URL) bool {
q := u.Query()
sign := q.Get(s.queryKey)
if sign == "" {
return false
}
b, err := base64.URLEncoding.DecodeString(sign)
if err != nil {
return false
}
m := bytes.Split(b, []byte(separator))
if len(m) < 3 {
return false
}
expTime, err := strconv.ParseInt(string(m[0]), 16, 64)
if err != nil || time.Now().Unix() > expTime {
return false
}
salt := m[1]
var hash []byte
for i := 2; i < len(m); i++ {
hash = append(hash, m[i]...)
if len(m) > i+1 {
hash = append(hash, []byte(separator)...)
}
}
h := sha256.New()
for _, p := range s.params {
if vv := q[p]; len(vv) > 0 {
for _, v := range vv {
h.Write([]byte(v))
}
}
}
h.Write([]byte(u.Path))
h.Write([]byte(s.expirationTime.String()))
h.Write([]byte(s.secret))
h.Write(salt)
return bytes.Equal(hash, h.Sum(nil))
}
package urlsigner
import (
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
)
const secret = "secret_key"
func TestSigner(t *testing.T) {
t.Run("Not Signed", func(t *testing.T) {
u, _ := url.Parse("http://example.com/path")
signer := NewURLSigner(secret, time.Minute, "")
require.False(t, signer.Check(u))
})
t.Run("Simple", func(t *testing.T) {
u, _ := url.Parse("http://example.com/path")
signer := NewURLSigner(secret, time.Minute, "")
u = signer.Sign(u)
require.True(t, signer.Check(u))
})
t.Run("Custom Query Key", func(t *testing.T) {
u, _ := url.Parse("http://example.com/path")
signer := NewURLSigner(secret, time.Minute, "custom")
u = signer.Sign(u)
require.True(t, signer.Check(u))
})
t.Run("URL Expired", func(t *testing.T) {
u, _ := url.Parse("http://example.com/path")
signer := NewURLSigner(secret, time.Millisecond, "")
u = signer.Sign(u)
time.Sleep(time.Second)
require.False(t, signer.Check(u))
})
t.Run("Required Params", func(t *testing.T) {
t.Run("Correct", func(t *testing.T) {
u, _ := url.Parse("http://example.com/path?param1=1&param2=2")
signer := NewURLSigner(secret, time.Minute, "", "param1", "param2")
u = signer.Sign(u)
require.True(t, signer.Check(u))
})
t.Run("One Param Empty", func(t *testing.T) {
u, _ := url.Parse("http://example.com/path?param1=1")
signer := NewURLSigner(secret, time.Minute, "", "param1", "param2")
u = signer.Sign(u)
require.True(t, signer.Check(u))
})
t.Run("Exchanged Values", func(t *testing.T) {
u, _ := url.Parse("http://example.com/path?param1=1&param2=2")
signer := NewURLSigner(secret, time.Minute, "", "param1", "param2")
u = signer.Sign(u)
q := u.Query()
q.Set("param1", "2")
q.Set("param2", "1")
u.RawQuery = q.Encode()
require.False(t, signer.Check(u))
})
})
t.Run("Extra params", func(t *testing.T) {
u, _ := url.Parse("http://example.com/path?param1=1")
signer := NewURLSigner(secret, time.Minute, "", "param1")
u = signer.Sign(u)
q := u.Query()
q.Set("extra", "100")
u.RawQuery = q.Encode()
require.True(t, signer.Check(u))
})
t.Run("Array Param Value", func(t *testing.T) {
u, _ := url.Parse("http://example.com/path?param1=1&param1=2&param1=3")
signer := NewURLSigner(secret, time.Minute, "", "param1")
u = signer.Sign(u)
q := u.Query()
q.Set("param1", "1")
u.RawQuery = q.Encode()
require.False(t, signer.Check(u))
})
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment