diff --git a/.gitignore b/.gitignore index f792cac..190a162 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,8 @@ /desktop/dist /docs/node_modules /docs/deploy.sh +node_modules +dist +.vscode +lemon_push.log +_lemon_ \ No newline at end of file diff --git a/desktop/build_windows.bat b/desktop/build_windows.bat index 59e95a6..4c83a68 100644 --- a/desktop/build_windows.bat +++ b/desktop/build_windows.bat @@ -5,19 +5,19 @@ cd ./src set GOOS=windows set GOARCH=amd64 -go build -o ../dist/%APP_NAME%_%APP_VERSION%_%GOOS%_%GOARCH%.exe +go build -ldflags="-s -w" -o ../dist/%APP_NAME%_%APP_VERSION%_%GOOS%_%GOARCH%.exe && upx -9 ../dist/%APP_NAME%_%APP_VERSION%_%GOOS%_%GOARCH%.exe -:: 编译为 macOS 可执行文件 -set GOOS=darwin -set GOARCH=amd64 -go build -o ../dist/%APP_NAME%_%APP_VERSION%_%GOOS%_%GOARCH% +@REM :: 编译为 macOS 可执行文件 +@REM set GOOS=darwin +@REM set GOARCH=amd64 +@REM go build -o ../dist/%APP_NAME%_%APP_VERSION%_%GOOS%_%GOARCH% -:: 编译为 macOS Apple Silicon可执行文件 -set GOOS=darwin -set GOARCH=arm64 -go build -o ../dist/%APP_NAME%_%APP_VERSION%_%GOOS%_%GOARCH% +@REM :: 编译为 macOS Apple Silicon可执行文件 +@REM set GOOS=darwin +@REM set GOARCH=arm64 +@REM go build -o ../dist/%APP_NAME%_%APP_VERSION%_%GOOS%_%GOARCH% -:: 编译为 Linux 可执行文件 -set GOOS=linux -set GOARCH=amd64 -go build -o ../dist/%APP_NAME%_%APP_VERSION%_%GOOS%_%GOARCH% +@REM :: 编译为 Linux 可执行文件 +@REM set GOOS=linux +@REM set GOARCH=amd64 +@REM go build -o ../dist/%APP_NAME%_%APP_VERSION%_%GOOS%_%GOARCH% diff --git a/desktop/src/gen_cert.go b/desktop/src/gen_cert.go new file mode 100644 index 0000000..7c096e9 --- /dev/null +++ b/desktop/src/gen_cert.go @@ -0,0 +1,97 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io/ioutil" + "log" + "math/big" + "net" + "time" +) + +func gen() { + ca := &x509.Certificate{ + SerialNumber: big.NewInt(1653), + Subject: pkix.Name{ + Organization: []string{"Lemon Push"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 5}, + BasicConstraintsValid: true, + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + } + privCa, _ := rsa.GenerateKey(rand.Reader, 1024) + CreateCertificateFile("ca", ca, privCa, ca, nil) + server := &x509.Certificate{ + SerialNumber: big.NewInt(1658), + Subject: pkix.Name{ + Organization: []string{"SERVER"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + } + hosts := []string{"localhost", "127.0.0.1"} + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + server.IPAddresses = append(server.IPAddresses, ip) + } else { + server.DNSNames = append(server.DNSNames, h) + } + } + privSer, _ := rsa.GenerateKey(rand.Reader, 1024) + CreateCertificateFile("server", server, privSer, ca, privCa) + client := &x509.Certificate{ + SerialNumber: big.NewInt(1658), + Subject: pkix.Name{ + Organization: []string{"CLIENT"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 7}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + } + privCli, _ := rsa.GenerateKey(rand.Reader, 1024) + CreateCertificateFile("client", client, privCli, ca, privCa) +} + +func CreateCertificateFile(name string, cert *x509.Certificate, key *rsa.PrivateKey, caCert *x509.Certificate, caKey *rsa.PrivateKey) { + priv := key + pub := &priv.PublicKey + privPm := priv + if caKey != nil { + privPm = caKey + } + ca_b, err := x509.CreateCertificate(rand.Reader, cert, caCert, pub, privPm) + if err != nil { + log.Println("create failed", err) + return + } + ca_f := name + ".pem" + log.Println("write to pem", ca_f) + var certificate = &pem.Block{Type: "CERTIFICATE", + Headers: map[string]string{}, + Bytes: ca_b} + ca_b64 := pem.EncodeToMemory(certificate) + ioutil.WriteFile(ca_f, ca_b64, 0777) + + priv_f := name + ".key" + priv_b := x509.MarshalPKCS1PrivateKey(priv) + log.Println("write to key", priv_f) + ioutil.WriteFile(priv_f, priv_b, 0777) + var privateKey = &pem.Block{Type: "PRIVATE KEY", + Headers: map[string]string{}, + Bytes: priv_b} + priv_b64 := pem.EncodeToMemory(privateKey) + ioutil.WriteFile(priv_f, priv_b64, 0777) +} diff --git a/desktop/src/go.mod b/desktop/src/go.mod index d43f40f..65a98ce 100644 --- a/desktop/src/go.mod +++ b/desktop/src/go.mod @@ -2,12 +2,27 @@ module net.blt/lemon_push go 1.20 +require github.com/lxn/win v0.0.0-20210218163916-a377121e959e + +require ( + golang.org/x/mod v0.13.0 // indirect + golang.org/x/tools v0.14.0 // indirect + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect +) + require ( github.com/Han-Ya-Jun/qrcode2console v0.0.0-20190430081741-6890f5f0fdf5 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/disintegration/imaging v1.6.2 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect + github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 github.com/mdp/qrterminal/v3 v3.1.1 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect golang.org/x/image v0.13.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/desktop/src/go.sum b/desktop/src/go.sum index 4c9dc4e..75abd9b 100644 --- a/desktop/src/go.sum +++ b/desktop/src/go.sum @@ -2,15 +2,93 @@ github.com/Han-Ya-Jun/qrcode2console v0.0.0-20190430081741-6890f5f0fdf5 h1:EMggI github.com/Han-Ya-Jun/qrcode2console v0.0.0-20190430081741-6890f5f0fdf5/go.mod h1:onbao3S7v7VTWE5sGfmiYgOt1MqzSd2dMp/+o6rk1Wk= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw= +github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/mdp/qrterminal/v3 v3.1.1 h1:cIPwg3QU0OIm9+ce/lRfWXhPwEjOSKwk3HBwL3HBTyc= github.com/mdp/qrterminal/v3 v3.1.1/go.mod h1:5lJlXe7Jdr8wlPDdcsJttv1/knsRgzXASyr4dcGZqNU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg= golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/desktop/src/lemon_push.conf b/desktop/src/lemon_push.conf deleted file mode 100644 index de15403..0000000 --- a/desktop/src/lemon_push.conf +++ /dev/null @@ -1,3 +0,0 @@ -ip= -port=14756 -folder=./_lemon_ diff --git a/desktop/src/log/logger.go b/desktop/src/log/logger.go new file mode 100644 index 0000000..a18f959 --- /dev/null +++ b/desktop/src/log/logger.go @@ -0,0 +1,41 @@ +package log + +import ( + "io" + "log" + "os" +) + +var logger *log.Logger +var logFile *os.File + +func InitLog() { + logFile, err := os.OpenFile("lemon_push.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + log.Fatal(err) + } + + mw := io.MultiWriter(os.Stdout, logFile) + // log.SetOutput(mw) + logger = log.New(mw, "", log.LstdFlags) +} + +func GetLogger() *log.Logger { + if logger == nil { + InitLog() + } + return logger +} + +func CloseLogFile() { + if logFile != nil { + err := logFile.Close() + if err != nil { + log.Fatal(err) + } + } +} + +func init() { + InitLog() +} diff --git a/desktop/src/main.go b/desktop/src/main.go index 439ce2d..b77af4c 100644 --- a/desktop/src/main.go +++ b/desktop/src/main.go @@ -5,11 +5,14 @@ import ( "encoding/json" "fmt" "io" + "log" + "mime" "net" "net/http" "os" "os/exec" "path/filepath" + "reflect" "regexp" "runtime" "strconv" @@ -17,58 +20,101 @@ import ( "time" "github.com/atotto/clipboard" + "github.com/dop251/goja" "github.com/mdp/qrterminal/v3" + mylogger "net.blt/lemon_push/log" + "net.blt/lemon_push/utils" + "net.blt/lemon_push/utils/js" ) -var dt = time.Now() -var folder = "./_lemon_" +type LemonConfig struct { + Port string `config:"port"` + IP string `config:"ip"` + Folder string `config:"folder"` + SSL string `config:"ssl"` + ClippedHook string `config:"clippedHook"` +} + +var logger *log.Logger + +var config LemonConfig + +var jsRuntime goja.Runtime + +func init() { + mylogger.InitLog() + logger = mylogger.GetLogger() + config = init_config() + createFolderIfNotExists(config.Folder) + init_hook() +} func main() { + defer mylogger.CloseLogFile() // 确保在程序退出时关闭日志文件 + // webui + wd, _ := os.Getwd() + webuiDir := filepath.Join(wd, "webui") + fs := http.FileServer(http.Dir(webuiDir)) + http.Handle("/webui/", http.StripPrefix("/webui/", fs)) + http.HandleFunc("/set_clipboard", setClipboard) http.HandleFunc("/get_clipboard", getClipboard) http.HandleFunc("/download", download) http.HandleFunc("/upload", upload) - config, lerr := loadConfigFile("lemon_push.conf") - if lerr != nil { - fmt.Println("加载配置lemon_push.conf失败:", lerr) - return - } - port := ":" + config["port"] // 监听端口 - selectedIP := config["ip"] // ip地址 - folder = config["folder"] // 文件夹 - if folder != "" { - createFolderIfNotExists(folder) - } + http.HandleFunc("/list", list) + http.HandleFunc("/files", getFiles) - fmt.Println(dt.Format("2006-01-02 15:04:05"), " 服务端监听端口:", config["port"]) + fmt.Println(" 服务端监听端口:", config.Port) - if selectedIP == "" { + if config.IP == "" { localIPs := getLocalIP() - fmt.Println(dt.Format("2006-01-02 15:04:05"), " 本机IP列表:") + fmt.Println(" 本机IP列表:") for i, ip := range localIPs { fmt.Printf("%d. %s\n", i+1, ip) } for { - fmt.Print("输入序号选择一个IP地址(仅用于生成二维码): ") + fmt.Println("输入序号选择一个IP地址(仅用于生成二维码): ") reader := bufio.NewReader(os.Stdin) input, _ := reader.ReadString('\n') input = strings.TrimSpace(input) index, err := strconv.Atoi(input) if err == nil && index >= 1 && index <= len(localIPs) { - selectedIP = localIPs[index-1] + config.IP = localIPs[index-1] break } fmt.Println("无效的选择,请重新输入.") } } - fmt.Println(dt.Format("2006-01-02 15:04:05"), " 选择的IP地址:", selectedIP, " 请使用App扫码连接") - url := selectedIP + port + url := fmt.Sprintf("%s:%s", config.IP, config.Port) + // fixed bug: https://github.com/golang/go/issues/32350#issuecomment-1128475902 + _ = mime.AddExtensionType(".js", "text/javascript") + fmt.Println("选择的IP地址:", config.IP, " 请使用App扫码连接") qRCode2ConsoleWithUrl(url) - fmt.Println(dt.Format("2006-01-02 15:04:05"), " 服务端已启动") - err := http.ListenAndServe(port, nil) + fmt.Println("服务端已启动") + fmt.Printf("weui地址: http(s)://%s/webui", url) + + var serverType string + var err error + + if config.SSL == "on" { + // 判断证书是否存在不存在生成 + _, certErr := os.Stat("server.pem") + if os.IsNotExist(certErr) { + logger.Println(" 证书不存在,正在生成证书...") + gen() + } + err = http.ListenAndServeTLS(url, "server.pem", "server.key", nil) + serverType = "https" + } else { + err = http.ListenAndServe(url, nil) + serverType = "http" + } + if err != nil { - fmt.Println("Error starting HTTP server:", err) + logger.Panic("Error starting", serverType, "server:", err) + } else { + logger.Println(" 服务端已启动,使用", serverType) } } @@ -87,13 +133,13 @@ func getLocalIP() []string { var ips []string addrs, err := net.InterfaceAddrs() if err != nil { - fmt.Println(err) + logger.Println(err) } for _, address := range addrs { // 检查ip地址判断是否回环地址 if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { if ipnet.IP.To4() != nil { - // fmt.Println(dt.Format("2006-01-02 15:04:05"), " 本机IP:", ipnet.IP.String()) + // logger.Println( " 本机IP:", ipnet.IP.String()) ips = append(ips, ipnet.IP.String()) } } @@ -117,15 +163,17 @@ func openBrowser(url string) error { } func setClipboard(w http.ResponseWriter, r *http.Request) { + // 设置跨域 + setCORS(w) values := r.URL.Query() code := values.Get("text") clipboard.WriteAll(code) - fmt.Println("客户端 " + r.RemoteAddr + " 设置剪切板:" + code) + logger.Println("客户端 " + r.RemoteAddr + " 设置剪切板:" + code) p := regexp.MustCompile(`https?://[^\s]+/[^/]+`) if p.MatchString(code) { matches := p.FindAllString(code, -1) for _, match := range matches { - fmt.Printf("%s 启动浏览器打开链接:%s\n", dt.Format("2006-01-02 15:04:05"), match) + logger.Printf("%s 启动浏览器打开链接:%s\n", match) openBrowser(match) } } @@ -143,10 +191,11 @@ func setClipboard(w http.ResponseWriter, r *http.Request) { } func getClipboard(w http.ResponseWriter, r *http.Request) { + setCORS(w) w.Header().Set("Content-Type", "application/json") resp := make(map[string]string) text, _ := clipboard.ReadAll() - fmt.Println("客户端 " + r.RemoteAddr + " 获取剪切板:" + text) + logger.Println("客户端 " + r.RemoteAddr + " 获取剪切板:" + text) resp["data"] = text resp["code"] = "0" jsonResp, err := json.Marshal(resp) @@ -156,16 +205,84 @@ func getClipboard(w http.ResponseWriter, r *http.Request) { w.Write(jsonResp) } +func getFiles(w http.ResponseWriter, r *http.Request) { + setCORS(w) + resp := make(map[string]interface{}) + filenames, err := utils.Clipboard().Files() + + if err != nil { + logger.Println(err) + resp["code"] = "1" + resp["msg"] = err.Error() + } else { + resp["data"] = filenames + resp["code"] = "0" + } + + jsonResp, err := json.Marshal(resp) + if err != nil { + return + } + w.Write(jsonResp) +} + +var lastText string // 用于存储前一次的剪贴板内容 +var lastFiles []string // 用于存储前一次的剪贴板内容 + +// 监听剪贴板 +func monitorClipboard() { + + jsRuntime.RunString(js.GetScript(config.ClippedHook)) + + var hookFn func([]string, string) string + + for { + files, err := utils.Clipboard().Files() + if err != nil { + fmt.Println("无法获取剪贴板文件,尝试读取剪贴板文字", err) + time.Sleep(time.Second * 1) + } + + var text string + if len(files) == 0 { + text, _ = clipboard.ReadAll() + } + + if lastText == text && reflect.DeepEqual(lastFiles, files) { + time.Sleep(time.Second * 1) + continue + } + + logger.Println("剪切板内容:", text, files) + + err = jsRuntime.ExportTo(jsRuntime.Get("hook"), &hookFn) + if err != nil { + log.Fatal("无法导出 JavaScript 函数:", err) + time.Sleep(time.Second * 1) + continue + } + + jsResult := hookFn(files, text) + logger.Println("hook 函数返回:", jsResult) + + lastText = text + lastFiles = files + + time.Sleep(time.Second * 1) + } +} + func download(w http.ResponseWriter, r *http.Request) { + setCORS(w) values := r.URL.Query() fileName := values.Get("filename") - folderPath := folder + folderPath := config.Folder - fmt.Println("客户端 " + r.RemoteAddr + " 下载文件:" + fileName) + logger.Println("客户端 " + r.RemoteAddr + " 下载文件:" + fileName) separator := string(filepath.Separator) file, err := os.Open(folderPath + separator + fileName) if err != nil { - fmt.Println(err) + logger.Println(err) http.Error(w, "文件未找到", http.StatusNotFound) return } @@ -173,7 +290,7 @@ func download(w http.ResponseWriter, r *http.Request) { fileInfo, err := file.Stat() if err != nil { - fmt.Println(err) + logger.Println(err) http.Error(w, "文件信息无法获取", http.StatusInternalServerError) return } @@ -184,30 +301,31 @@ func download(w http.ResponseWriter, r *http.Request) { _, copyErr := io.Copy(w, file) if copyErr != nil { - fmt.Println(copyErr) + logger.Println(copyErr) http.Error(w, "文件无法下载", http.StatusInternalServerError) return } } func upload(w http.ResponseWriter, r *http.Request) { - fmt.Println("客户端 " + r.RemoteAddr + " 上传文件") + setCORS(w) + logger.Println("客户端 " + r.RemoteAddr + " 上传文件") r.ParseMultipartForm(32 << 20) file, handler, err := r.FormFile("file") if err != nil { - fmt.Println(err) + logger.Println(err) return } defer file.Close() - fmt.Println("文件名: " + handler.Filename) - fmt.Println("文件大小: ", handler.Size) - fmt.Println("MIME类型: " + handler.Header.Get("Content-Type")) + logger.Println("文件名: " + handler.Filename) + logger.Println("文件大小: ", handler.Size) + logger.Println("MIME类型: " + handler.Header.Get("Content-Type")) // 创建一个目标文件 separator := string(filepath.Separator) - targetFile, err := os.Create(folder + separator + handler.Filename) - fmt.Println("文件路径: " + targetFile.Name()) + targetFile, err := os.Create(config.Folder + separator + handler.Filename) + logger.Println("文件路径: " + targetFile.Name()) if err != nil { - fmt.Println(err) + logger.Println(err) return } defer targetFile.Close() @@ -215,7 +333,7 @@ func upload(w http.ResponseWriter, r *http.Request) { // 将上传的文件内容拷贝到目标文件 _, copyErr := io.Copy(targetFile, file) if copyErr != nil { - fmt.Println(copyErr) + logger.Println(copyErr) return } @@ -230,6 +348,19 @@ func upload(w http.ResponseWriter, r *http.Request) { w.Write(jsonResp) } +func list(w http.ResponseWriter, r *http.Request) { + setCORS(w) + resp := make(map[string]interface{}) + list, _ := getFilesInFolder(config.Folder) + resp["data"] = list + resp["code"] = "0" + jsonResp, err := json.Marshal(resp) + if err != nil { + return + } + w.Write(jsonResp) +} + func createFolderIfNotExists(folderPath string) error { _, err := os.Stat(folderPath) if os.IsNotExist(err) { @@ -237,50 +368,112 @@ func createFolderIfNotExists(folderPath string) error { if errDir != nil { return errDir } - fmt.Println("文件夹不存在,已创建:", folderPath) + logger.Println("文件夹不存在,已创建:", folderPath) } else { - fmt.Println("文件夹已存在:", folderPath) + logger.Println("文件夹已存在:", folderPath) } return nil } -func loadConfigFile(filename string) (map[string]string, error) { - execPath, err := os.Executable() +func getFilesInFolder(folderPath string) ([]string, error) { + var files []string + + // 读取文件夹 + err := filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + files = append(files, info.Name()) + } + return nil + }) + if err != nil { return nil, err } + + return files, nil +} + +// 设置跨域 +func setCORS(w http.ResponseWriter) { + w.Header().Set("Access-Control-Allow-Origin", "*") +} + +// 时间格式化 +func timeFormat() string { + return time.Now().Format("2006-01-02 15:04:05") +} + +func init_config() LemonConfig { + loadedConfig, lerr := loadConfigFile("lemon_push.conf") + if lerr != nil { + log.Fatal("加载配置lemon_push.conf失败:", lerr) + } + + return loadedConfig +} + +func init_hook() { + if config.ClippedHook != "" { + jsRuntime = *goja.New() + jsRuntime.Set("get", js.Get) + jsRuntime.Set("post", js.Post) + jsRuntime.Set("upload", js.Upload) + + go monitorClipboard() + } +} + +func loadConfigFile(filename string) (LemonConfig, error) { + execPath, err := os.Executable() + if err != nil { + return LemonConfig{}, err + } execDir := filepath.Dir(execPath) filePath := filepath.Join(execDir, filename) file, err := os.Open(filePath) if err != nil { // 文件不存在,使用默认配置并创建文件 - config := make(map[string]string) - config["port"] = "14756" - config["folder"] = "./_lemon_" - config["ip"] = "" + config := LemonConfig{ + Port: "14756", + IP: "0.0.0.0", + Folder: "./_lemon_", + SSL: "off", + } // 创建文件并写入默认配置 file, err = os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0644) if err != nil { - return nil, err + return LemonConfig{}, err } defer file.Close() writer := bufio.NewWriter(file) - for key, value := range config { - _, err := writer.WriteString(key + "=" + value + "\n") + + // 使用反射遍历结构体字段 + val := reflect.ValueOf(config) + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldName := typ.Field(i).Name + fieldValue := field.Interface() + _, err := writer.WriteString(fieldName + "=" + fieldValue.(string) + "\n") if err != nil { - return nil, err + return LemonConfig{}, err } } + writer.Flush() return config, nil } defer file.Close() - config := make(map[string]string) + config := LemonConfig{} scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() @@ -288,12 +481,26 @@ func loadConfigFile(filename string) (map[string]string, error) { if len(parts) == 2 { key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) - config[key] = value + // 根据键设置配置结构体的相应字段 + elem := reflect.ValueOf(&config).Elem() + + for i := 0; i < elem.NumField(); i++ { + field := elem.Type().Field(i) + if strings.EqualFold(key, field.Tag.Get("config")) { + // 获取字段值 + fieldValue := elem.Field(i) + if fieldValue.CanSet() { + fieldValue.SetString(value) + } + break + } + } + } } if err := scanner.Err(); err != nil { - return nil, err + return LemonConfig{}, err } return config, nil diff --git a/desktop/src/utils/clipboard.go b/desktop/src/utils/clipboard.go new file mode 100644 index 0000000..b657266 --- /dev/null +++ b/desktop/src/utils/clipboard.go @@ -0,0 +1,312 @@ +package utils + +import ( + "encoding/binary" + "errors" + "fmt" + "reflect" + "syscall" + "unsafe" + + "github.com/lxn/walk" + "github.com/lxn/win" + "golang.org/x/sys/windows" +) + +const ( + TypeText = "text" + TypeFile = "file" + TypeMedia = "media" + TypeBitmap = "bitmap" + TypeUnknown = "unknown" +) + +var clipboard ClipboardService +var Formats = []uint32{win.CF_HDROP, win.CF_DIBV5, win.CF_UNICODETEXT} + +// Clipboard returns an object that provides access to the system clipboard. +func Clipboard() *ClipboardService { + return &clipboard +} + +// ClipboardService provides access to the system clipboard. +type ClipboardService struct { + hwnd win.HWND + contentsChangedPublisher walk.EventPublisher +} + +// ContentsChanged returns an Event that you can attach to for handling +// clipboard content changes. +func (c *ClipboardService) ContentsChanged() *walk.Event { + return c.contentsChangedPublisher.Event() +} + +// Clear clears the contents of the clipboard. +func (c *ClipboardService) Clear() error { + return c.withOpenClipboard(func() error { + if !win.EmptyClipboard() { + return lastError("EmptyClipboard") + } + + return nil + }) +} + +// ContainsText returns whether the clipboard currently contains text data. +func (c *ClipboardService) ContainsText() (available bool, err error) { + err = c.withOpenClipboard(func() error { + available = win.IsClipboardFormatAvailable(win.CF_UNICODETEXT) + + return nil + }) + + return +} + +// ContentType returns the type of current data of the clipboard +func (c *ClipboardService) ContentType() (string, error) { + var format uint32 + err := c.withOpenClipboard(func() error { + for _, f := range Formats { + isAvaliable := win.IsClipboardFormatAvailable(f) + if isAvaliable { + format = f + return nil + } + } + return lastError("get content type of clipboard") + }) + if err != nil { + return "", err + } + switch format { + case win.CF_HDROP: + return TypeFile, nil + case win.CF_DIBV5: + return TypeBitmap, nil + case win.CF_UNICODETEXT: + return TypeText, nil + default: + return TypeUnknown, nil + } +} + +// Text returns the current text data of the clipboard. +func (c *ClipboardService) Text() (text string, err error) { + err = c.withOpenClipboard(func() error { + hMem := win.HGLOBAL(win.GetClipboardData(win.CF_UNICODETEXT)) + if hMem == 0 { + return lastError("GetClipboardData") + } + + p := win.GlobalLock(hMem) + if p == nil { + return lastError("GlobalLock()") + } + defer win.GlobalUnlock(hMem) + + text = win.UTF16PtrToString((*uint16)(p)) + + return nil + }) + + return +} + +func int32Abs(val int32) uint32 { + if val < 0 { + return uint32(-val) + } + return uint32(val) +} + +func (c *ClipboardService) Bitmap() (bmpBytes []byte, err error) { + err = c.withOpenClipboard(func() error { + hMem := win.HGLOBAL(win.GetClipboardData(win.CF_DIBV5)) + if hMem == 0 { + return lastError("GetClipboardData") + } + + p := win.GlobalLock(hMem) + if p == nil { + return lastError("GlobalLock()") + } + defer win.GlobalUnlock(hMem) + + header := (*win.BITMAPV5HEADER)(unsafe.Pointer(p)) + var biSizeImage uint32 + // BiSizeImage is 0 when use tencent TIM + if header.BiBitCount == 32 { + biSizeImage = 4 * int32Abs(header.BiWidth) * int32Abs(header.BiHeight) + } else { + biSizeImage = header.BiSizeImage + } + + var data []byte + sh := (*reflect.SliceHeader)(unsafe.Pointer(&data)) + sh.Data = uintptr(p) + sh.Cap = int(header.BiSize + biSizeImage) + sh.Len = int(header.BiSize + biSizeImage) + + // In this place, we omit AlphaMask to make sure the BiV5Header can be decoded by image/bmp + // https://github.com/golang/image/blob/35266b937fa69456d24ed72a04d75eb6857f7d52/bmp/reader.go#L177 + if header.BiCompression == 3 && header.BV4RedMask == 0xff0000 && header.BV4GreenMask == 0xff00 && header.BV4BlueMask == 0xff { + header.BiCompression = win.BI_RGB + + // always set alpha channel value as 0xFF to make image untransparent + // to fix screenshot from PicPick is transparent when converted to png + pixelStartAt := header.BiSize + for i := pixelStartAt + 3; i < uint32(len(data)); i += 4 { + data[i] = 0xff + } + } + + bmpFileSize := 14 + header.BiSize + biSizeImage + bmpBytes = make([]byte, bmpFileSize) + + binary.LittleEndian.PutUint16(bmpBytes[0:], 0x4d42) // start with 'BM' + binary.LittleEndian.PutUint32(bmpBytes[2:], bmpFileSize) + binary.LittleEndian.PutUint16(bmpBytes[6:], 0) + binary.LittleEndian.PutUint16(bmpBytes[8:], 0) + binary.LittleEndian.PutUint32(bmpBytes[10:], 14+header.BiSize) + copy(bmpBytes[14:], data[:]) + + return nil + }) + return +} + +func (c *ClipboardService) Files() (filenames []string, err error) { + err = c.withOpenClipboard(func() error { + hMem := win.HGLOBAL(win.GetClipboardData(win.CF_HDROP)) + if hMem == 0 { + return lastError("GetClipboardData") + } + p := win.GlobalLock(hMem) + if p == nil { + return lastError("GlobalLock()") + } + defer win.GlobalUnlock(hMem) + filesCount := win.DragQueryFile(win.HDROP(p), 0xFFFFFFFF, nil, 0) + filenames = make([]string, 0, filesCount) + buf := make([]uint16, win.MAX_PATH) + for i := uint(0); i < filesCount; i++ { + win.DragQueryFile(win.HDROP(p), i, &buf[0], win.MAX_PATH) + filenames = append(filenames, windows.UTF16ToString(buf)) + } + + return nil + }) + if err != nil { + return nil, err + } + return +} + +// SetText sets the current text data of the clipboard. +func (c *ClipboardService) SetText(s string) error { + return c.withOpenClipboard(func() error { + win.EmptyClipboard() + utf16, err := syscall.UTF16FromString(s) + if err != nil { + return err + } + + hMem := win.GlobalAlloc(win.GMEM_MOVEABLE, uintptr(len(utf16)*2)) + if hMem == 0 { + return lastError("GlobalAlloc") + } + + p := win.GlobalLock(hMem) + if p == nil { + return lastError("GlobalLock()") + } + + win.MoveMemory(p, unsafe.Pointer(&utf16[0]), uintptr(len(utf16)*2)) + + win.GlobalUnlock(hMem) + + if 0 == win.SetClipboardData(win.CF_UNICODETEXT, win.HANDLE(hMem)) { + // We need to free hMem. + defer win.GlobalFree(hMem) + + return lastError("SetClipboardData") + } + + // The system now owns the memory referred to by hMem. + return nil + }) +} + +type DROPFILES struct { + pFiles uintptr + pt uintptr + fNC bool + fWide bool + _ uint32 // padding +} + +// SetFiles sets the current file drop data of the clipboard. +func (c *ClipboardService) SetFiles(paths []string) error { + return c.withOpenClipboard(func() error { + win.EmptyClipboard() + // https://docs.microsoft.com/en-us/windows/win32/shell/clipboard#cf_hdrop + var utf16 []uint16 + for _, path := range paths { + _utf16, err := syscall.UTF16FromString(path) + if err != nil { + return err + } + utf16 = append(utf16, _utf16...) + } + utf16 = append(utf16, uint16(0)) + + const dropFilesSize = unsafe.Sizeof(DROPFILES{}) - 4 + + size := dropFilesSize + uintptr((len(utf16))*2+2) + + hMem := win.GlobalAlloc(win.GHND, size) + if hMem == 0 { + return lastError("GlobalAlloc") + } + + p := win.GlobalLock(hMem) + if p == nil { + return lastError("GlobalLock()") + } + + zeroMem := make([]byte, size) + win.MoveMemory(p, unsafe.Pointer(&zeroMem[0]), size) + + pD := (*DROPFILES)(p) + pD.pFiles = dropFilesSize + pD.fWide = false + pD.fNC = true + win.MoveMemory(unsafe.Pointer(uintptr(p)+dropFilesSize), unsafe.Pointer(&utf16[0]), uintptr(len(utf16)*2)) + + win.GlobalUnlock(hMem) + + if 0 == win.SetClipboardData(win.CF_HDROP, win.HANDLE(hMem)) { + // We need to free hMem. + defer win.GlobalFree(hMem) + + return lastError("SetClipboardData") + } + // The system now owns the memory referred to by hMem. + + return nil + }) +} + +func (c *ClipboardService) withOpenClipboard(f func() error) error { + if !win.OpenClipboard(c.hwnd) { + return lastError("OpenClipboard") + } + defer win.CloseClipboard() + + return f() +} + +func lastError(name string) error { + return errors.New(fmt.Sprintf("%s failed", name)) +} diff --git a/desktop/src/utils/js/function.go b/desktop/src/utils/js/function.go new file mode 100644 index 0000000..5201c9b --- /dev/null +++ b/desktop/src/utils/js/function.go @@ -0,0 +1,164 @@ +package js + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "mime/multipart" + "net/http" + "os" + "path/filepath" + + mylogger "net.blt/lemon_push/log" +) + +var logger *log.Logger + +func init() { + logger = mylogger.GetLogger() +} + +func Get(url string) string { + resp, err := http.Get(url) + if err != nil { + return fmt.Sprintf("Error: %s", err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Sprintf("Error reading response: %s", err) + } + + return string(body) +} + +func Post(url string, body string) string { + resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(body))) + if err != nil { + return fmt.Sprintf("Error: %s", err) + } + defer resp.Body.Close() + + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Sprintf("Error reading response: %s", err) + } + + return string(responseBody) +} + +type UploadOptions struct { + ContentType string + FilePath string + FieldName string +} + +// uploadFile uploads the file to the given url. +func Upload(url string, options UploadOptions) string { + // Open the file. + file, err := os.Open(options.FilePath) + if err != nil { + return fmt.Sprintf("Error opening file: %s", err) + } + defer file.Close() + + // Prepare a buffer to store the form data. + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // Create a form file field. + if options.FieldName == "" { + options.FieldName = "file" + } + + part, err := writer.CreateFormFile(options.FieldName, options.FilePath) + if err != nil { + return fmt.Sprintf("Error creating form file: %s", err) + } + + // Copy the file content to the form field. + _, err = io.Copy(part, file) + if err != nil { + return fmt.Sprintf("Error copying file content: %s", err) + } + + // Close the form data writer. + err = writer.Close() + if err != nil { + return fmt.Sprintf("Error closing form writer: %s", err) + } + + // Create the request. + req, err := http.NewRequest("POST", url, body) + if err != nil { + return fmt.Sprintf("Error creating request: %s", err) + } + + contentType := writer.FormDataContentType() + if options.ContentType != "" { + contentType = options.ContentType + } + + req.Header.Set("Content-Type", contentType) + + // Perform the request. + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Sprintf("Error performing request: %s", err) + } + defer resp.Body.Close() + + // Read the response body. + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Sprintf("Error reading response: %s", err) + } + + return string(responseBody) +} + +func GetScript(jspath string) string { + // 设置当前目录 + dir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + jsPath := filepath.Join(dir, jspath) + + script, err := ioutil.ReadFile(jsPath) + if err != nil { + logger.Println("无法读取 JavaScript 文件:", err) + exampleScript := ` + function hook(params) { + // bark + const url = 'https://api.day.app/your_key/' + params; + return get(url); + // else + // return post(url, body); + } + ` + // 创建文件并写入默认配置 + jsFile, err := os.OpenFile(jsPath, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + logger.Println("无法创建 JavaScript 文件:", err) + + } + defer jsFile.Close() + + writer := bufio.NewWriter(jsFile) + _, err = writer.WriteString(exampleScript) + if err != nil { + logger.Fatal("无法写入 JavaScript 文件:", err) + } + writer.Flush() + logger.Println("已创建 JavaScript 文件:", jsPath) + } + + return string(script) +} diff --git a/desktop/start.sh b/desktop/start.sh deleted file mode 100644 index a775b2c..0000000 --- a/desktop/start.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -nohup ./dist/lemon_push_v104_windows_amd64.exe >lemon_push.log 2>&1 & diff --git a/readme.md b/readme.md index 8322496..40219ba 100644 --- a/readme.md +++ b/readme.md @@ -1,37 +1,47 @@ -## 柠檬Push - [简体中文](readme.md) | [English](readme-en.md) - -同一WiFi环境下手机高效推送文本到电脑剪切板的工具,移动端支持Android、iOS,电脑端支持Windows、Mac、Linux平台 +## 柠檬 Push + +[简体中文](readme.md) | [English](readme-en.md) + +同一 WiFi 环境下手机高效推送文本到电脑剪切板的工具,移动端支持 Android、iOS,电脑端支持 Windows、Mac、Linux 平台 LemonPush is an efficient tool for pushing text from your mobile device to your computer's clipboard under the same WiFi environment. It supports Android and iOS on the mobile side, and Windows, Mac, and Linux platforms on the computer side. ## 配置教程 -电脑双击启动程序后会显示电脑IP,手机安装柠檬Push App后,点击设置电脑端显示IP,可能会出现多个IP,使用局域网所在网络的IP,一般192开头,扫码连接或填写电脑IP后点击推送剪切板即可获取剪切板并推送至电脑端 -程序首次运行会创建默认配置文件lemon_push.conf,如出现端口冲突可在配置文件修改端口号后重启程序 +电脑双击启动程序后会显示电脑 IP,手机安装柠檬 Push App 后,点击设置电脑端显示 IP,可能会出现多个 IP,使用局域网所在网络的 IP,一般 192 开头,扫码连接或填写电脑 IP 后点击推送剪切板即可获取剪切板并推送至电脑端 + +程序首次运行会创建默认配置文件 lemon_push.conf,如出现端口冲突可在配置文件修改端口号后重启程序 ## 接口说明 + ### 写入电脑剪切板 + `/set_clipboard?text=内容` -返回json +返回 json + ``` { "code":"0", "data":"ok" } ``` + ### 获取电脑剪切板 + `/get_clipboard` -返回json +返回 json + ``` { "code":"0", "data":"电脑剪切板内容" } ``` + ### 上传文件 + 文件保存在目录`./_lemon_` `/upload` @@ -49,14 +59,48 @@ LemonPush is an efficient tool for pushing text from your mobile device to your `curl --location --request GET 'http://localhost:14756/download?filename=__UNI__F0B72F8_0809143049.apk'` +### WEBUI + +cd wenui && start build_webui.bat + +请求示例 + +`http://localhost:14756/webui` + +### HOOK + +``` +// 新增配置项 +ClippedHook=hook.js + +// hook.js +function hook(params) { + // params 是剪贴板内容 + // bark + const url = `https://api.day.app/your_key/${params}` + return get(url) + // post(url,body) +} +``` + +function hook(params) { + // bark + const url = 'https://api.day.app/your_key/' + params; + get(url); + // post(url, body); +} + + ## 常见问题 -- 电脑无法接收手机剪切板,需要配置电脑防火墙(教程待补充) -- Mac电脑双击无法运行,需配置文件权限,运行命令`chmod u+x 程序文件名` -- 双击程序运行会展示控制台并输出日志,如不需要控制台,可后台运行,后台运行需在配置文件lemon.conf中填写电脑ip(ip默认为空,需首次在控制台运行生成一份默认配置文件) -Windows运行`Start-Process -WindowStyle hidden -FilePath "程序"`,Mac运行`nohup 程序 &` + +- 电脑无法接收手机剪切板,需要配置电脑防火墙(教程待补充) +- Mac 电脑双击无法运行,需配置文件权限,运行命令`chmod u+x 程序文件名` +- 双击程序运行会展示控制台并输出日志,如不需要控制台,可后台运行,后台运行需在配置文件 lemon.conf 中填写电脑 ip(ip 默认为空,需首次在控制台运行生成一份默认配置文件) + Windows 运行`Start-Process -WindowStyle hidden -FilePath "程序"`,Mac 运行`nohup 程序 &` ## 开发背景 -日常手机与电脑互发消息频率较多,使用微信或QQ来发消息步骤略显繁琐 + +日常手机与电脑互发消息频率较多,使用微信或 QQ 来发消息步骤略显繁琐 例如 @@ -74,19 +118,22 @@ Windows运行`Start-Process -WindowStyle hidden -FilePath "程序"`,Mac运行` 以上的痛点在手机厂商推出的多屏互联方案得以改善,但有所限制,如只支持部分手机或自家笔记本等 -使用柠檬Push可减少以上步骤,在柠檬Push上面开启打开即推送,复制文本,切换至柠檬 Push 则会立刻推送文本到电脑剪切板,如文本含有链接自动使用默认浏览器打开 +使用柠檬 Push 可减少以上步骤,在柠檬 Push 上面开启打开即推送,复制文本,切换至柠檬 Push 则会立刻推送文本到电脑剪切板,如文本含有链接自动使用默认浏览器打开 -提高效率核心是减少步骤、减少选择。发文本到电脑几乎必然是复制到剪切板,发链接到电脑几乎必然用浏览器打开,所以柠檬Push基于以上设定开发 +提高效率核心是减少步骤、减少选择。发文本到电脑几乎必然是复制到剪切板,发链接到电脑几乎必然用浏览器打开,所以柠檬 Push 基于以上设定开发 ## 开发技术 -电脑端将剪切板接口转为http服务,基于局域网的http服务实现信息交互,使用Go语言实现电脑端程序 + +电脑端将剪切板接口转为 http 服务,基于局域网的 http 服务实现信息交互,使用 Go 语言实现电脑端程序 ## 已知问题 + 受限于作者的开发水平,软件还有许多未完善的地方。如使用传输内容未加密会存在安全性问题,除了局域网内有人主动攻击,对于多数的场景下是安全的 请不要从第三方平台下载软件,本项目代码开源,第三方平台的软件下载使用可能会被加入恶意代码导致信息泄露 ## 建议反馈 + 欢迎在兔小巢建议反馈 [https://support.qq.com/products/405982](https://support.qq.com/products/405982) @@ -97,6 +144,6 @@ Windows运行`Start-Process -WindowStyle hidden -FilePath "程序"`,Mac运行` ## 支持开发者 -柠檬Push如你对有所帮助,欢迎star、PR、feedback、share、donate支持开发者 +柠檬 Push 如你对有所帮助,欢迎 star、PR、feedback、share、donate 支持开发者 - \ No newline at end of file + diff --git a/webui/README.md b/webui/README.md new file mode 100644 index 0000000..a228896 --- /dev/null +++ b/webui/README.md @@ -0,0 +1,20 @@ +[Lemon push webui](https://github.com/ishare20/lemonPush) + +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + + +
+ + + + +