Re-organise the code to be more modular (#3172)

This commit is contained in:
Siddarth Kumar 2023-03-01 17:23:17 +05:30 committed by GitHub
parent 42ee98e295
commit 6ac2308ee1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1071 additions and 275 deletions

View File

@ -51,3 +51,4 @@ exclude_patterns:
- "protocol/pushnotificationclient/migrations/migrations.go"
- "protocol/pushnotificationserver/migrations/migrations.go"
- "protocol/transport/migrations/migrations.go"
- "images/qr-assets.go"

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
_assets/tests/qr/status.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -152,3 +152,16 @@ func isWebp(buf []byte) bool {
buf[8] == 0x57 && buf[9] == 0x45 &&
buf[10] == 0x42 && buf[11] == 0x50
}
func GetImageDimensions(imgBytes []byte) (int, int, error) {
// Decode image bytes
img, _, err := image.Decode(bytes.NewReader(imgBytes))
if err != nil {
return 0, 0, err
}
// Get the image dimensions
bounds := img.Bounds()
width := bounds.Max.X - bounds.Min.X
height := bounds.Max.Y - bounds.Min.Y
return width, height, nil
}

View File

@ -1,16 +1,41 @@
package images
import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"math"
"os"
"github.com/nfnt/resize"
"github.com/oliamb/cutter"
"go.uber.org/zap"
xdraw "golang.org/x/image/draw"
"github.com/ethereum/go-ethereum/log"
)
type Circle struct {
X, Y, R int
}
func (c *Circle) ColorModel() color.Model {
return color.AlphaModel
}
func (c *Circle) Bounds() image.Rectangle {
return image.Rect(c.X-c.R, c.Y-c.R, c.X+c.R, c.Y+c.R)
}
func (c *Circle) At(x, y int) color.Color {
xx, yy, rr := float64(x-c.X)+0.5, float64(y-c.Y)+0.5, float64(c.R)
if xx*xx+yy*yy < rr*rr {
return color.Alpha{255}
}
return color.Alpha{0}
}
func Resize(size ResizeDimension, img image.Image) image.Image {
var width, height uint
@ -84,3 +109,122 @@ func CropCenter(img image.Image) (image.Image, error) {
}
return Crop(img, cropRect)
}
func ImageToBytes(imagePath string) ([]byte, error) {
// Open the image file
file, err := os.Open(imagePath)
if err != nil {
return nil, err
}
defer file.Close()
// Decode the image
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
// Create a new buffer to hold the image data
var imgBuffer bytes.Buffer
// Encode the image to the desired format and save it in the buffer
err = png.Encode(&imgBuffer, img)
if err != nil {
return nil, err
}
// Return the image data as a byte slice
return imgBuffer.Bytes(), nil
}
func AddPadding(img image.Image, padding int) *image.RGBA {
bounds := img.Bounds()
newBounds := image.Rect(bounds.Min.X-padding, bounds.Min.Y-padding, bounds.Max.X+padding, bounds.Max.Y+padding)
paddedImg := image.NewRGBA(newBounds)
draw.Draw(paddedImg, newBounds, &image.Uniform{C: color.White}, image.ZP, draw.Src)
return paddedImg
}
func EncodePNG(img *image.RGBA) ([]byte, error) {
resultImg := &bytes.Buffer{}
err := png.Encode(resultImg, img)
if err != nil {
return nil, err
}
return resultImg.Bytes(), nil
}
func CreateCircle(img image.Image) *image.RGBA {
bounds := img.Bounds()
circle := image.NewRGBA(bounds)
draw.DrawMask(circle, bounds, img, image.ZP, &Circle{
X: bounds.Dx() / 2,
Y: bounds.Dy() / 2,
R: bounds.Dx() / 2,
}, image.ZP, draw.Over)
return circle
}
func PlaceCircleInCenter(paddedImg, circle *image.RGBA) *image.RGBA {
bounds := circle.Bounds()
centerX := (paddedImg.Bounds().Min.X + paddedImg.Bounds().Max.X) / 2
centerY := (paddedImg.Bounds().Min.Y + paddedImg.Bounds().Max.Y) / 2
draw.Draw(paddedImg, bounds.Add(image.Pt(centerX-bounds.Dx()/2, centerY-bounds.Dy()/2)), circle, image.ZP, draw.Over)
return paddedImg
}
func ResizeImage(imgBytes []byte, width, height int) ([]byte, error) {
// Decode image bytes
img, _, err := image.Decode(bytes.NewReader(imgBytes))
if err != nil {
return nil, err
}
// Create a new image with the desired dimensions
newImg := image.NewNRGBA(image.Rect(0, 0, width, height))
xdraw.BiLinear.Scale(newImg, newImg.Bounds(), img, img.Bounds(), draw.Over, nil)
// Encode the new image to bytes
var newImgBytes bytes.Buffer
if err = png.Encode(&newImgBytes, newImg); err != nil {
return nil, err
}
return newImgBytes.Bytes(), nil
}
func SuperimposeLogoOnQRImage(imageBytes []byte, qrFilepath []byte) []byte {
// Read the two images from bytes
img1, _, err := image.Decode(bytes.NewReader(imageBytes))
if err != nil {
log.Error("error decoding logo Image", zap.Error(err))
return nil
}
img2, _, err := image.Decode(bytes.NewReader(qrFilepath))
if err != nil {
log.Error("error decoding QR Image", zap.Error(err))
return nil
}
// Create a new image with the dimensions of the first image
result := image.NewRGBA(img1.Bounds())
// Draw the first image on the new image
draw.Draw(result, img1.Bounds(), img1, image.ZP, draw.Src)
// Get the dimensions of the second image
img2Bounds := img2.Bounds()
// Calculate the x and y coordinates to center the second image
x := (img1.Bounds().Dx() - img2Bounds.Dx()) / 2
y := (img1.Bounds().Dy() - img2Bounds.Dy()) / 2
// Draw the second image on top of the first image at the calculated coordinates
draw.Draw(result, img2Bounds.Add(image.Pt(x, y)), img2, image.ZP, draw.Over)
// Encode the final image to a desired format
var b bytes.Buffer
err = png.Encode(&b, result)
if err != nil {
log.Error("error encoding final result Image to Buffer", zap.Error(err))
return nil
}
return b.Bytes()
}

327
images/qr-assets.go Normal file

File diff suppressed because one or more lines are too long

View File

@ -2,11 +2,12 @@ package images
// Test data that would typically only exist in a test file, used for exporting sample data outside the package.
var (
testJpegBytes = []byte{0xff, 0xd8, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x50}
testPngBytes = []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48}
testGifBytes = []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x00, 0x01, 0x00, 0x01, 0x84, 0x1f, 0x00, 0xff}
testWebpBytes = []byte{0x52, 0x49, 0x46, 0x46, 0x90, 0x49, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50}
testAacBytes = []byte{0xff, 0xf1, 0x50, 0x80, 0x1c, 0x3f, 0xfc, 0xda, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35}
testJpegBytes = []byte{0xff, 0xd8, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x50}
testPngBytes = []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48}
testGifBytes = []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x00, 0x01, 0x00, 0x01, 0x84, 0x1f, 0x00, 0xff}
testWebpBytes = []byte{0x52, 0x49, 0x46, 0x46, 0x90, 0x49, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50}
testAacBytes = []byte{0xff, 0xf1, 0x50, 0x80, 0x1c, 0x3f, 0xfc, 0xda, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35}
testLogoBytes, _ = Asset("_assets/tests/qr/status.png")
)
func SampleIdentityImages() []IdentityImage {
@ -31,3 +32,17 @@ func SampleIdentityImages() []IdentityImage {
},
}
}
func SampleIdentityImageForQRCode() []IdentityImage {
return []IdentityImage{
{
Name: LargeDimName,
Payload: testLogoBytes,
Width: 240,
Height: 300,
FileSize: 1024,
ResizeTarget: 240,
Clock: 0,
},
}
}

View File

@ -1,182 +0,0 @@
package qrcode
import (
"bytes"
"errors"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"net/url"
"os"
xdraw "golang.org/x/image/draw"
"github.com/status-im/status-go/multiaccounts"
)
const (
defaultPadding = 20
)
func GetImageDimensions(imgBytes []byte) (int, int, error) {
// Decode image bytes
img, _, err := image.Decode(bytes.NewReader(imgBytes))
if err != nil {
return 0, 0, err
}
// Get the image dimensions
bounds := img.Bounds()
width := bounds.Max.X - bounds.Min.X
height := bounds.Max.Y - bounds.Min.Y
return width, height, nil
}
func ToLogoImageFromBytes(imageBytes []byte, padding int) []byte {
img, _, err := image.Decode(bytes.NewReader(imageBytes))
if err != nil {
panic(err)
}
bounds := img.Bounds()
newBounds := image.Rect(bounds.Min.X-padding, bounds.Min.Y-padding, bounds.Max.X+padding, bounds.Max.Y+padding)
white := image.NewRGBA(newBounds)
draw.Draw(white, newBounds, &image.Uniform{C: color.White}, image.ZP, draw.Src)
// Create a circular mask
circle := image.NewRGBA(bounds)
draw.DrawMask(circle, bounds, img, image.ZP, &Circle{
X: bounds.Dx() / 2,
Y: bounds.Dy() / 2,
R: bounds.Dx() / 2,
}, image.ZP, draw.Over)
// Calculate the center point of the new image
centerX := (newBounds.Min.X + newBounds.Max.X) / 2
centerY := (newBounds.Min.Y + newBounds.Max.Y) / 2
// Draw the circular image in the center of the new image
draw.Draw(white, bounds.Add(image.Pt(centerX-bounds.Dx()/2, centerY-bounds.Dy()/2)), circle, image.ZP, draw.Over)
// Encode image to png format and save in a bytes
var resultImg bytes.Buffer
err = png.Encode(&resultImg, white)
if err != nil {
return nil
}
resultBytes := resultImg.Bytes()
return resultBytes
}
func SuperimposeImage(imageBytes []byte, qrFilepath []byte) []byte {
// Read the two images from bytes
img1, _, _ := image.Decode(bytes.NewReader(imageBytes))
img2, _, _ := image.Decode(bytes.NewReader(qrFilepath))
// Create a new image with the dimensions of the first image
result := image.NewRGBA(img1.Bounds())
// Draw the first image on the new image
draw.Draw(result, img1.Bounds(), img1, image.ZP, draw.Src)
// Get the dimensions of the second image
img2Bounds := img2.Bounds()
// Calculate the x and y coordinates to center the second image
x := (img1.Bounds().Dx() - img2Bounds.Dx()) / 2
y := (img1.Bounds().Dy() - img2Bounds.Dy()) / 2
// Draw the second image on top of the first image at the calculated coordinates
draw.Draw(result, img2Bounds.Add(image.Pt(x, y)), img2, image.ZP, draw.Over)
// Encode the final image to a desired format
var b bytes.Buffer
if err := png.Encode(&b, result); err != nil {
fmt.Println(err)
}
return b.Bytes()
}
func ResizeImage(imgBytes []byte, width, height int) ([]byte, error) {
// Decode image bytes
img, _, err := image.Decode(bytes.NewReader(imgBytes))
if err != nil {
return nil, err
}
// Create a new image with the desired dimensions
newImg := image.NewNRGBA(image.Rect(0, 0, width, height))
xdraw.BiLinear.Scale(newImg, newImg.Bounds(), img, img.Bounds(), draw.Over, nil)
// Encode the new image to bytes
var newImgBytes bytes.Buffer
if err = png.Encode(&newImgBytes, newImg); err != nil {
return nil, err
}
return newImgBytes.Bytes(), nil
}
func ImageToBytes(imagePath string) ([]byte, error) {
// Open the image file
file, err := os.Open(imagePath)
if err != nil {
return nil, err
}
defer file.Close()
// Decode the image
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
// Create a new buffer to hold the image data
var imgBuffer bytes.Buffer
// Encode the image to the desired format and save it in the buffer
err = png.Encode(&imgBuffer, img)
if err != nil {
return nil, err
}
// Return the image data as a byte slice
return imgBuffer.Bytes(), nil
}
func GetLogoImage(multiaccountsDB *multiaccounts.Database, params url.Values) ([]byte, error) {
var imageFolderBasePath = "../_assets/tests/"
var logoFileStaticPath = imageFolderBasePath + "status.png"
keyUids, ok := params["keyUid"]
if !ok || len(keyUids) == 0 {
return nil, errors.New("no keyUid")
}
imageNames, ok := params["imageName"]
if !ok || len(imageNames) == 0 {
return nil, errors.New("no imageName")
}
identityImageObjectFromDB, err := multiaccountsDB.GetIdentityImage(keyUids[0], imageNames[0])
if err != nil {
return nil, err
}
staticLogoFileBytes, _ := ImageToBytes(logoFileStaticPath)
if identityImageObjectFromDB == nil {
return ToLogoImageFromBytes(staticLogoFileBytes, GetPadding(staticLogoFileBytes)), nil
}
return ToLogoImageFromBytes(identityImageObjectFromDB.Payload, GetPadding(identityImageObjectFromDB.Payload)), nil
}
func GetPadding(imgBytes []byte) int {
size, _, err := GetImageDimensions(imgBytes)
if err != nil {
return defaultPadding
}
return size / 5
}
type Circle struct {
X, Y, R int
}
func (c *Circle) ColorModel() color.Model {
return color.AlphaModel
}
func (c *Circle) Bounds() image.Rectangle {
return image.Rect(c.X-c.R, c.Y-c.R, c.X+c.R, c.Y+c.R)
}
func (c *Circle) At(x, y int) color.Color {
xx, yy, rr := float64(x-c.X)+0.5, float64(y-c.Y)+0.5, float64(c.R)
if xx*xx+yy*yy < rr*rr {
return color.Alpha{255}
}
return color.Alpha{0}
}

View File

@ -3,15 +3,12 @@ package server
import (
"bytes"
"database/sql"
"encoding/base64"
"image"
"net/http"
"net/url"
"strconv"
"time"
qrcode "github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/standard"
"go.uber.org/zap"
"github.com/status-im/status-go/images"
@ -20,7 +17,6 @@ import (
"github.com/status-im/status-go/protocol/identity/colorhash"
"github.com/status-im/status-go/protocol/identity/identicon"
"github.com/status-im/status-go/protocol/identity/ring"
qrcodeutils "github.com/status-im/status-go/qrcode"
)
const (
@ -40,25 +36,6 @@ const (
type HandlerPatternMap map[string]http.HandlerFunc
type QROptions struct {
URL string `json:"url"`
ErrorCorrectionLevel string `json:"errorCorrectionLevel"`
Capacity string `json:"capacity"`
AllowProfileImage bool `json:"withLogo"`
}
type WriterCloserByteBuffer struct {
*bytes.Buffer
}
func (wc WriterCloserByteBuffer) Close() error {
return nil
}
func NewWriterCloserByteBuffer() *WriterCloserByteBuffer {
return &WriterCloserByteBuffer{bytes.NewBuffer([]byte{})}
}
func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
@ -412,62 +389,19 @@ func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) http.HandlerFun
func handleQRCodeGeneration(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
qrURLBase64Encoded, ok := params["qrurl"]
if !ok || len(qrURLBase64Encoded) == 0 {
logger.Error("no qr url provided")
return
}
qrURLBase64Decoded, err := base64.StdEncoding.DecodeString(qrURLBase64Encoded[0])
if err != nil {
logger.Error("error decoding string from base64", zap.Error(err))
}
level, ok := params["level"]
// Default error correction level
correctionLevel := qrcode.ErrorCorrectionMedium
if ok && len(level) == 1 {
switch level[0] {
case "4":
correctionLevel = qrcode.ErrorCorrectionHighest
case "1":
correctionLevel = qrcode.ErrorCorrectionLow
case "3":
correctionLevel = qrcode.ErrorCorrectionQuart
}
}
buf := NewWriterCloserByteBuffer()
qrc, err := qrcode.NewWith(string(qrURLBase64Decoded),
qrcode.WithEncodingMode(qrcode.EncModeAuto),
qrcode.WithErrorCorrectionLevel(correctionLevel),
)
if err != nil {
logger.Error("could not generate QRCode", zap.Error(err))
}
nw := standard.NewWithWriter(buf)
if err = qrc.Save(nw); err != nil {
logger.Error("could not save image", zap.Error(err))
}
payload := buf.Bytes()
logo, err := qrcodeutils.GetLogoImage(multiaccountsDB, params)
if err == nil {
qrWidth, qrHeight, _ := qrcodeutils.GetImageDimensions(payload)
logo, _ = qrcodeutils.ResizeImage(logo, qrWidth/5, qrHeight/5)
payload = qrcodeutils.SuperimposeImage(payload, logo)
}
size, ok := params["size"]
if ok && len(size) == 1 {
size, err := strconv.Atoi(size[0])
if err == nil {
payload, _ = qrcodeutils.ResizeImage(payload, size, size)
}
}
payload := generateQRBytes(params, logger, multiaccountsDB)
mime, err := images.GetProtobufImageMime(payload)
if err != nil {
logger.Error("could not generate image from payload", zap.Error(err))
}
w.Header().Set("Content-Type", mime)
w.Header().Set("Cache-Control", "no-store")
_, err = w.Write(payload)
if err != nil {
logger.Error("failed to write image", zap.Error(err))
}

306
server/qrops.go Normal file
View File

@ -0,0 +1,306 @@
package server
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"image"
"net/url"
"strconv"
"github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/standard"
"go.uber.org/zap"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/multiaccounts"
)
type WriterCloserByteBuffer struct {
*bytes.Buffer
}
func (wc WriterCloserByteBuffer) Close() error {
return nil
}
func NewWriterCloserByteBuffer() *WriterCloserByteBuffer {
return &WriterCloserByteBuffer{bytes.NewBuffer([]byte{})}
}
type QRConfig struct {
DecodedQRURL string
WithLogo bool
CorrectionLevel qrcode.EncodeOption
KeyUID string
ImageName string
Size int
Params url.Values
}
func NewQRConfig(params url.Values, logger *zap.Logger) (*QRConfig, error) {
config := &QRConfig{}
config.Params = params
err := config.setQrURL()
if err != nil {
logger.Error("[qrops-error] error in setting QRURL", zap.Error(err))
return nil, err
}
config.setAllowProfileImage()
config.setErrorCorrectionLevel()
err = config.setSize()
if err != nil {
logger.Error("[qrops-error] could not convert string to int for size param ", zap.Error(err))
return nil, err
}
if config.WithLogo {
err = config.setKeyUID()
if err != nil {
logger.Error(err.Error())
return nil, err
}
config.setImageName()
}
return config, nil
}
func (q *QRConfig) setQrURL() error {
qrURL, ok := q.Params["url"]
if !ok || len(qrURL) == 0 {
return errors.New("[qrops-error] no qr url provided")
}
decodedURL, err := base64.StdEncoding.DecodeString(qrURL[0])
if err != nil {
return err
}
q.DecodedQRURL = string(decodedURL)
return nil
}
func (q *QRConfig) setAllowProfileImage() {
allowProfileImage, ok := q.Params["allowProfileImage"]
if !ok || len(allowProfileImage) == 0 {
// we default to false when this flag was not provided
// so someone does not want to allowProfileImage on their QR Image
// fine then :)
q.WithLogo = false
}
LogoOnImage, err := strconv.ParseBool(allowProfileImage[0])
if err != nil {
// maybe for fun someone tries to send non-boolean values to this flag
// we also default to false in that case
q.WithLogo = false
}
// if we reach here its most probably true
q.WithLogo = LogoOnImage
}
func (q *QRConfig) setErrorCorrectionLevel() {
level, ok := q.Params["level"]
if !ok || len(level) == 0 {
// we default to MediumLevel of error correction when the level flag
// is not passed.
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
}
levelInt, err := strconv.Atoi(level[0])
if err != nil || levelInt < 0 {
// if there is any issue with string to int conversion
// we still default to MediumLevel of error correction
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
}
switch levelInt {
case 1:
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionLow)
case 2:
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
case 3:
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionQuart)
case 4:
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionHighest)
default:
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
}
}
func (q *QRConfig) setSize() error {
size, ok := q.Params["size"]
if ok {
imageToBeResized, err := strconv.Atoi(size[0])
if err != nil {
return err
}
if imageToBeResized <= 0 {
return errors.New("[qrops-error] Got an invalid size parameter, it should be greater than zero")
}
q.Size = imageToBeResized
}
return nil
}
func (q *QRConfig) setKeyUID() error {
keyUID, ok := q.Params["keyUid"]
// the keyUID was not passed, which is a requirement to get the multiaccount image,
// so we log this error
if !ok || len(keyUID) == 0 {
return errors.New("[qrops-error] A keyUID is required to put logo on image and it was not passed in the parameters")
}
q.KeyUID = keyUID[0]
return nil
}
func (q *QRConfig) setImageName() {
imageName, ok := q.Params["imageName"]
//if the imageName was not passed, we default to const images.LargeDimName
if !ok || len(imageName) == 0 {
q.ImageName = images.LargeDimName
}
q.ImageName = imageName[0]
}
func ToLogoImageFromBytes(imageBytes []byte, padding int) ([]byte, error) {
img, _, err := image.Decode(bytes.NewReader(imageBytes))
if err != nil {
return nil, fmt.Errorf("decoding image failed: %v", err)
}
paddedImg := images.AddPadding(img, padding)
circle := images.CreateCircle(img)
centeredImg := images.PlaceCircleInCenter(paddedImg, circle)
resultBytes, err := images.EncodePNG(centeredImg)
if err != nil {
return nil, fmt.Errorf("encoding PNG failed: %v", err)
}
return resultBytes, nil
}
func GetLogoImage(multiaccountsDB *multiaccounts.Database, keyUID string, imageName string) ([]byte, error) {
var (
padding int
LogoBytes []byte
)
staticImageData, err := images.Asset("_assets/tests/qr/status.png")
if err != nil { // Asset was not found.
return nil, err
}
identityImageObjectFromDB, err := multiaccountsDB.GetIdentityImage(keyUID, imageName)
if err != nil {
return nil, err
}
if identityImageObjectFromDB == nil {
padding = GetPadding(staticImageData)
LogoBytes, err = ToLogoImageFromBytes(staticImageData, padding)
} else {
padding = GetPadding(identityImageObjectFromDB.Payload)
LogoBytes, err = ToLogoImageFromBytes(identityImageObjectFromDB.Payload, padding)
}
return LogoBytes, err
}
func GetPadding(imgBytes []byte) int {
const (
defaultPadding = 20
)
size, _, err := images.GetImageDimensions(imgBytes)
if err != nil {
return defaultPadding
}
return size / 5
}
func generateQRBytes(params url.Values, logger *zap.Logger, multiaccountsDB *multiaccounts.Database) []byte {
qrGenerationConfig, err := NewQRConfig(params, logger)
if err != nil {
logger.Error("could not generate QRConfig please rectify the errors with input parameters", zap.Error(err))
return nil
}
qrc, err := qrcode.NewWith(qrGenerationConfig.DecodedQRURL,
qrcode.WithEncodingMode(qrcode.EncModeAuto),
qrGenerationConfig.CorrectionLevel,
)
if err != nil {
logger.Error("could not generate QRCode with provided options", zap.Error(err))
return nil
}
buf := NewWriterCloserByteBuffer()
nw := standard.NewWithWriter(buf)
err = qrc.Save(nw)
if err != nil {
logger.Error("could not save image", zap.Error(err))
return nil
}
payload := buf.Bytes()
if qrGenerationConfig.WithLogo {
logo, err := GetLogoImage(multiaccountsDB, qrGenerationConfig.KeyUID, qrGenerationConfig.ImageName)
if err != nil {
logger.Error("could not get logo image from multiaccountsDB", zap.Error(err))
return nil
}
qrWidth, qrHeight, err := images.GetImageDimensions(payload)
if err != nil {
logger.Error("could not get image dimensions from payload", zap.Error(err))
return nil
}
logo, err = images.ResizeImage(logo, qrWidth/5, qrHeight/5)
if err != nil {
logger.Error("could not resize logo image ", zap.Error(err))
return nil
}
payload = images.SuperimposeLogoOnQRImage(payload, logo)
}
if qrGenerationConfig.Size > 0 {
payload, err = images.ResizeImage(payload, qrGenerationConfig.Size, qrGenerationConfig.Size)
if err != nil {
logger.Error("could not resize final logo image ", zap.Error(err))
return nil
}
}
return payload
}

118
server/qrops_test.go Normal file
View File

@ -0,0 +1,118 @@
package server
import (
"encoding/base64"
"io/ioutil"
"net/url"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/multiaccounts"
)
type QROpsTestSuite struct {
suite.Suite
TestKeyComponents
TestLoggerComponents
server *MediaServer
serverNoPort *MediaServer
testStart time.Time
multiaccountsDB *multiaccounts.Database
}
var (
keyUID = "0xdeadbeef"
qrURL = "https://github.com/status-im/status-go/pull/3154"
)
func TestQROpsTestSuite(t *testing.T) {
suite.Run(t, new(QROpsTestSuite))
}
func (s *QROpsTestSuite) SetupTest() {
s.SetupKeyComponents(s.T())
s.SetupLoggerComponents()
mediaServer, err := NewMediaServer(nil, nil, nil)
s.Require().NoError(err)
s.server = mediaServer
err = s.server.SetPort(customPortForTests)
s.Require().NoError(err)
err = s.server.Start()
s.Require().NoError(err)
s.serverNoPort = &MediaServer{Server: Server{
hostname: DefaultIP.String(),
portManger: newPortManager(s.Logger, nil),
}}
go func() {
time.Sleep(waitTime)
s.serverNoPort.port = 80
}()
s.testStart = time.Now()
}
func seedTestDBWithIdentityImagesForQRCodeTests(s *QROpsTestSuite, db *multiaccounts.Database, keyUID string) {
iis := images.SampleIdentityImageForQRCode()
err := db.StoreIdentityImages(keyUID, iis, false)
s.Require().NoError(err)
}
// TestQROpsCodeWithoutSuperImposingLogo tests the QR code generation logic, it also allows us to debug in case
// things go wrong, here we compare generate a new QR code and compare its bytes with an already generated one.
func (s *QROpsTestSuite) TestQROpsCodeWithoutSuperImposingLogo() {
var params = url.Values{}
params.Set("url", base64.StdEncoding.EncodeToString([]byte(qrURL)))
params.Set("allowProfileImage", "false")
params.Set("level", "2")
params.Set("size", "200")
params.Set("imageName", "")
payload := generateQRBytes(params, s.Logger, s.multiaccountsDB)
expectedPayload, err := images.Asset("_assets/tests/qr/defaultQR.png")
s.Require().NoError(err)
s.Require().NotEmpty(payload)
require.Equal(s.T(), payload, expectedPayload)
}
func (s *QROpsTestSuite) TestQROpsCodeWithSuperImposingLogo() {
tmpfile, err := ioutil.TempFile("", "accounts-tests-")
s.Require().NoError(err)
db, err := multiaccounts.InitializeDB(tmpfile.Name())
s.Require().NoError(err)
seedTestDBWithIdentityImagesForQRCodeTests(s, db, keyUID)
var params = url.Values{}
params.Set("url", base64.StdEncoding.EncodeToString([]byte(qrURL)))
params.Set("allowProfileImage", "true")
params.Set("level", "2")
params.Set("size", "200")
params.Set("keyUid", keyUID)
params.Set("imageName", "large")
payload := generateQRBytes(params, s.Logger, db)
s.Require().NotEmpty(payload)
expectedPayload, err := images.Asset("_assets/tests/qr/QRWithLogo.png")
require.Equal(s.T(), payload, expectedPayload)
s.Require().NoError(err)
err = db.Close()
s.Require().NoError(err)
err = os.Remove(tmpfile.Name())
s.Require().NoError(err)
}

View File

@ -105,10 +105,20 @@ func (s *MediaServer) MakeStickerURL(stickerHash string) string {
return u.String()
}
func (s *MediaServer) MakeQRURL(qurul string) string {
func (s *MediaServer) MakeQRURL(qurul string,
allowProfileImage string,
level string,
size string,
keyUID string,
imageName string) string {
u := s.MakeBaseURL()
u.Path = generateQRCode
u.RawQuery = url.Values{"qurul": {qurul}}.Encode()
u.RawQuery = url.Values{"url": {qurul},
"level": {level},
"allowProfileImage": {allowProfileImage},
"size": {size},
"keyUid": {keyUID},
"imageName": {imageName}}.Encode()
return u.String()
}

View File

@ -1,14 +1,33 @@
package server
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/images"
)
const (
waitTime = 50 * time.Millisecond
waitTime = 50 * time.Millisecond
customPortForTests = 1337
defaultPortForTests = 80
)
var (
baseURL = "https://127.0.0.1"
baseURLWithCustomPort = fmt.Sprintf("%s:%d", baseURL, customPortForTests)
baseURLWithDefaultPort = fmt.Sprintf("%s:%d", baseURL, defaultPortForTests)
)
func TestServerURLSuite(t *testing.T) {
@ -21,6 +40,7 @@ type ServerURLSuite struct {
TestLoggerComponents
server *MediaServer
serverForQR *MediaServer
serverNoPort *MediaServer
testStart time.Time
}
@ -29,11 +49,19 @@ func (s *ServerURLSuite) SetupTest() {
s.SetupKeyComponents(s.T())
s.SetupLoggerComponents()
mediaServer, err := NewMediaServer(nil, nil, nil)
s.Require().NoError(err)
s.serverForQR = mediaServer
err = s.serverForQR.Start()
s.Require().NoError(err)
s.server = &MediaServer{Server: Server{
hostname: DefaultIP.String(),
portManger: newPortManager(s.Logger, nil),
}}
err := s.server.SetPort(1337)
err = s.server.SetPort(customPortForTests)
s.Require().NoError(err)
s.serverNoPort = &MediaServer{Server: Server{
@ -42,7 +70,7 @@ func (s *ServerURLSuite) SetupTest() {
}}
go func() {
time.Sleep(waitTime)
s.serverNoPort.port = 80
s.serverNoPort.port = defaultPortForTests
}()
s.testStart = time.Now()
@ -58,48 +86,130 @@ func (s *ServerURLSuite) testNoPort(expected string, actual string) {
}
func (s *ServerURLSuite) TestServer_MakeBaseURL() {
s.Require().Equal("https://127.0.0.1:1337", s.server.MakeBaseURL().String())
s.testNoPort("https://127.0.0.1:80", s.serverNoPort.MakeBaseURL().String())
s.Require().Equal(baseURLWithCustomPort, s.server.MakeBaseURL().String())
s.testNoPort(baseURLWithDefaultPort, s.serverNoPort.MakeBaseURL().String())
}
func (s *ServerURLSuite) TestServer_MakeImageServerURL() {
s.Require().Equal("https://127.0.0.1:1337/messages/", s.server.MakeImageServerURL())
s.testNoPort("https://127.0.0.1:80/messages/", s.serverNoPort.MakeImageServerURL())
s.Require().Equal(baseURLWithCustomPort+"/messages/", s.server.MakeImageServerURL())
s.testNoPort(baseURLWithDefaultPort+"/messages/", s.serverNoPort.MakeImageServerURL())
}
func (s *ServerURLSuite) TestServer_MakeIdenticonURL() {
s.Require().Equal(
"https://127.0.0.1:1337/messages/identicons?publicKey=0xdaff0d11decade",
baseURLWithCustomPort+"/messages/identicons?publicKey=0xdaff0d11decade",
s.server.MakeIdenticonURL("0xdaff0d11decade"))
s.testNoPort(
"https://127.0.0.1:80/messages/identicons?publicKey=0xdaff0d11decade",
baseURLWithDefaultPort+"/messages/identicons?publicKey=0xdaff0d11decade",
s.serverNoPort.MakeIdenticonURL("0xdaff0d11decade"))
}
func (s *ServerURLSuite) TestServer_MakeImageURL() {
s.Require().Equal(
"https://127.0.0.1:1337/messages/images?messageId=0x10aded70ffee",
baseURLWithCustomPort+"/messages/images?messageId=0x10aded70ffee",
s.server.MakeImageURL("0x10aded70ffee"))
s.testNoPort(
"https://127.0.0.1:80/messages/images?messageId=0x10aded70ffee",
baseURLWithDefaultPort+"/messages/images?messageId=0x10aded70ffee",
s.serverNoPort.MakeImageURL("0x10aded70ffee"))
}
func (s *ServerURLSuite) TestServer_MakeAudioURL() {
s.Require().Equal(
"https://127.0.0.1:1337/messages/audio?messageId=0xde1e7ebee71e",
baseURLWithCustomPort+"/messages/audio?messageId=0xde1e7ebee71e",
s.server.MakeAudioURL("0xde1e7ebee71e"))
s.testNoPort(
"https://127.0.0.1:80/messages/audio?messageId=0xde1e7ebee71e",
baseURLWithDefaultPort+"/messages/audio?messageId=0xde1e7ebee71e",
s.serverNoPort.MakeAudioURL("0xde1e7ebee71e"))
}
func (s *ServerURLSuite) TestServer_MakeStickerURL() {
s.Require().Equal(
"https://127.0.0.1:1337/ipfs?hash=0xdeadbeef4ac0",
baseURLWithCustomPort+"/ipfs?hash=0xdeadbeef4ac0",
s.server.MakeStickerURL("0xdeadbeef4ac0"))
s.testNoPort(
"https://127.0.0.1:80/ipfs?hash=0xdeadbeef4ac0",
baseURLWithDefaultPort+"/ipfs?hash=0xdeadbeef4ac0",
s.serverNoPort.MakeStickerURL("0xdeadbeef4ac0"))
}
// TestQRCodeGeneration tests if we provide all the correct parameters to the media server
// do we get a valid QR code or not as part of the response payload.
// we have stored a generated QR code in tests folder, and we compare their bytes.
func (s *ServerURLSuite) TestQRCodeGeneration() {
qrURL := "https://github.com/status-im/status-go/pull/3154"
generatedURL := base64.StdEncoding.EncodeToString([]byte(qrURL))
generatedURL = s.serverForQR.MakeQRURL(generatedURL, "false", "2", "200", "", "")
u, err := url.Parse(generatedURL)
if err != nil {
s.Require().NoError(err)
}
if u.Scheme == "" || u.Host == "" {
s.Require().Failf("generatedURL is not a valid URL: %s", generatedURL)
}
serverCert := s.serverForQR.cert
serverCertBytes := serverCert.Certificate[0]
certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCertBytes})
rootCAs, err := x509.SystemCertPool()
if err != nil {
s.Require().NoError(err)
}
_ = rootCAs.AppendCertsFromPEM(certPem)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true, //nolint:all // MUST BE FALSE
RootCAs: rootCAs,
},
}
client := &http.Client{Transport: tr}
req, err := http.NewRequest(http.MethodGet, generatedURL, nil)
if err != nil {
s.Require().NoError(err)
}
resp, err := client.Do(req)
if err != nil {
s.Require().NoError(err)
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
s.Require().Failf("Unexpected response status code: %d", fmt.Sprint(resp.StatusCode))
}
payload, err := ioutil.ReadAll(resp.Body)
if err != nil {
s.Require().NoError(err)
}
s.Require().NotEmpty(payload)
expectedPayload, err := images.Asset("_assets/tests/qr/defaultQR.png")
require.Equal(s.T(), payload, expectedPayload)
s.Require().NoError(err)
//(siddarthkay) un-comment code block below to generate the file in tests folder
//f, err := os.Create("image.png")
//if err != nil {
// s.Require().NoError(err)
//
//}
//defer f.Close()
//_, err = f.Write(payload)
//
//if err != nil {
// s.Require().NoError(err)
//}
}