Compare commits

...

8 commits

Author SHA1 Message Date
8445a7546e Allow registering device without requiring the creation of the config file.
Some checks failed
Fly Deploy / Deploy app (push) Has been cancelled
Signed-off-by: jmug <u.g.a.mariano@gmail.com>
2025-05-10 14:26:20 -07:00
c041a5feee Update to go 1.24 and add some logging.
Signed-off-by: Mariano Uvalle <u.g.a.mariano@gmail.com>
2025-05-10 13:59:51 -07:00
7ddf1162c7 flake lock
Signed-off-by: jmug <u.g.a.mariano@gmail.com>
2024-12-19 13:47:57 -08:00
a8a11ed86e Ignore direnv cache
Signed-off-by: jmug <u.g.a.mariano@gmail.com>
2024-12-19 13:47:52 -08:00
39f7e63c72 Add flake dev shell
Signed-off-by: jmug <u.g.a.mariano@gmail.com>
2024-12-19 13:44:37 -08:00
5bb0a76d61
Merge pull request #3 from AYM1607/AYM1607-patch-1
Update sqlite package
2024-01-24 15:31:57 -08:00
d79d141986 Update go sum 2024-01-24 23:31:20 +00:00
8a56431b8f
Update go.mod 2024-01-24 15:28:08 -08:00
17 changed files with 296 additions and 36 deletions

99
.dockerignore Normal file
View file

@ -0,0 +1,99 @@
# flyctl launch added from .gitignore
# Created by https://www.toptal.com/developers/gitignore/api/vim,linux,macos,go
# Edit at https://www.toptal.com/developers/gitignore?templates=vim,linux,macos,go
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
**/*.exe
**/*.exe~
**/*.dll
**/*.so
**/*.dylib
# Test binary, built with `go test -c`
**/*.test
# Output of the go coverage tool, specifically when used with LiteIDE
**/*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
**/go.work
### Linux ###
**/*~
# temporary files which can be created if a process still has a handle open of a deleted file
**/.fuse_hidden*
# KDE directory preferences
**/.directory
# Linux trash folder which might appear on any partition or disk
**/.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
**/.nfs*
### macOS ###
# General
**/.DS_Store
**/.AppleDouble
**/.LSOverride
# Icon must end with two \r
**/Icon
# Thumbnails
**/._*
# Files that might appear in the root of a volume
**/.DocumentRevisions-V100
**/.fseventsd
**/.Spotlight-V100
**/.TemporaryItems
**/.Trashes
**/.VolumeIcon.icns
**/.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
**/.AppleDB
**/.AppleDesktop
**/Network Trash Folder
**/Temporary Items
**/.apdisk
### macOS Patch ###
# iCloud generated files
**/*.icloud
### Vim ###
# Swap
**/[._]*.s[a-v][a-z]
!**/*.svg # comment out if you don't need vector files
**/[._]*.sw[a-p]
**/[._]s[a-rt-v][a-z]
**/[._]ss[a-gi-z]
**/[._]sw[a-p]
# Session
**/Session.vim
**/Sessionx.vim
# Temporary
**/.netrwhist
# Auto-generated tag files
**/tags
# Persistent undo
**/[._]*.un~
# End of https://www.toptal.com/developers/gitignore/api/vim,linux,macos,go
**/.direnv
fly.toml

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

2
.gitignore vendored
View file

@ -93,3 +93,5 @@ tags
[._]*.un~ [._]*.un~
# End of https://www.toptal.com/developers/gitignore/api/vim,linux,macos,go # End of https://www.toptal.com/developers/gitignore/api/vim,linux,macos,go
.direnv

16
cmd/admin/main.go Normal file
View file

@ -0,0 +1,16 @@
package main
import (
"encoding/base64"
"fmt"
"github.com/AYM1607/ccclip/pkg/crypto"
)
func main() {
privK := crypto.NewPrivateKey()
privStr := base64.StdEncoding.EncodeToString(privK.Bytes())
pubStr := base64.StdEncoding.EncodeToString(privK.PublicKey().Bytes())
fmt.Printf("Priv: %s\nPub: %s\n", privStr, pubStr)
}

View file

@ -7,7 +7,7 @@ import (
var apiclient *client.Client var apiclient *client.Client
func init() { func init() {
apiclient = client.New("https://api.ccclip.io") apiclient = client.New("https://clipboard.jmug.me")
} }
func main() { func main() {

View file

@ -12,6 +12,7 @@ import (
func init() { func init() {
rootCmd.AddCommand(registerDeviceCommand) rootCmd.AddCommand(registerDeviceCommand)
registerDeviceCommand.Flags().StringVarP(&email, "email", "e", "", "email is your login identifier, ignored if your config file has has an email")
} }
var registerDeviceCommand = &cobra.Command{ var registerDeviceCommand = &cobra.Command{
@ -23,8 +24,11 @@ var registerDeviceCommand = &cobra.Command{
return err return err
} }
if cc.Email == "" { if cc.Email != "" {
return errors.New("you don't have an account configured for thist device") email = cc.Email
}
if email == "" {
return errors.New("provide an email through your config file or with --email")
} }
if cc.DeviceId != "" { if cc.DeviceId != "" {
@ -35,7 +39,7 @@ var registerDeviceCommand = &cobra.Command{
pbk := pvk.PublicKey() pbk := pvk.PublicKey()
password := input.ReadPassword() password := input.ReadPassword()
res, err := apiclient.RegisterDevice(cc.Email, password, pbk.Bytes()) res, err := apiclient.RegisterDevice(email, password, pbk.Bytes())
if err != nil { if err != nil {
return err return err
} }
@ -43,6 +47,9 @@ var registerDeviceCommand = &cobra.Command{
// Write the key files first, if those fail to write then we should not // Write the key files first, if those fail to write then we should not
// save the device Id. // save the device Id.
cc.DeviceId = res.DeviceID cc.DeviceId = res.DeviceID
if cc.Email == "" {
cc.Email = email
}
err = configfile.SavePrivateKey(pvk) err = configfile.SavePrivateKey(pvk)
if err != nil { if err != nil {
return err return err

View file

@ -1,12 +1,7 @@
FROM golang:1.21-alpine AS builder FROM golang:1.24-alpine AS builder
# Ensure we have a c compiler. # Ensure we have a c compiler.
RUN apk add --no-cache build-base ca-certificates fuse3 sqlite RUN apk add --no-cache build-base ca-certificates fuse3 sqlite
# Install LiteFS for distribute SQLite
COPY --from=flyio/litefs:0.5 /usr/local/bin/litefs /usr/local/bin/litefs
COPY cmd/server/litefs.yml /etc/litefs.yml
WORKDIR /src WORKDIR /src
COPY go.mod . COPY go.mod .
COPY go.sum . COPY go.sum .
@ -14,6 +9,6 @@ RUN go mod download
COPY . . COPY . .
RUN go build -ldflags='-s -w' -tags 'linux' -trimpath -o /dist/app ./cmd/server RUN go build -ldflags='-s -w' -tags 'linux' -trimpath -o /dist/app ./cmd/server
EXPOSE 3000 EXPOSE 8080
ENTRYPOINT ["litefs", "mount"] ENTRYPOINT ["/dist/app"]

81
flake.lock generated Normal file
View file

@ -0,0 +1,81 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gonixpkgs": {
"locked": {
"lastModified": 1746518791,
"narHash": "sha256-MiJ11L7w18S2G5ftcoYtcrrS0JFqBaj9d5rwJFpC5Wk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1cb1c02a6b1b7cf67e3d7731cbbf327a53da9679",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1cb1c02a6b1b7cf67e3d7731cbbf327a53da9679",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1734629298,
"narHash": "sha256-KOzg4I+sRJW6rFaBzVAXKKCi2/uhDgC8YWmImKG6cs8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fac643b357f5f203cc1fe07f3f070b7534d61334",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"gonixpkgs": "gonixpkgs",
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

32
flake.nix Normal file
View file

@ -0,0 +1,32 @@
{
description = "A basic flake with a shell";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/release-24.11";
gonixpkgs.url = "github:NixOS/nixpkgs/1cb1c02a6b1b7cf67e3d7731cbbf327a53da9679";
systems.url = "github:nix-systems/default";
flake-utils = {
url = "github:numtide/flake-utils";
inputs.systems.follows = "systems";
};
};
outputs =
{ nixpkgs, gonixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
gopkgs = gonixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
packages = with gopkgs; [
pkgs.flyctl
go
gotools
gopls
];
};
}
);
}

View file

@ -1,30 +1,30 @@
# fly.toml app configuration file generated for dark-paper-8180 on 2023-11-10T08:23:22Z # fly.toml app configuration file generated for ccclip on 2025-05-10T00:45:48-07:00
# #
# See https://fly.io/docs/reference/configuration/ for information about how to use this file. # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
# #
app = "dark-paper-8180" app = 'ccclip'
primary_region = "sea" primary_region = 'sea'
[build] [build]
dockerfile = "./cmd/server/Dockerfile" dockerfile = './cmd/server/Dockerfile'
[env] [env]
CCCLIP_PORT = "3000" CCCLIP_DATABASE_LOCATION = '/database/ccclip.db'
CCCLIP_DATABASE_LOCATION = "/litefs/ccclip.db"
[mounts] [[mounts]]
source = "litefs" source = 'database'
destination = "/var/lib/litefs" destination = '/database'
[http_service] [http_service]
internal_port = 8080 internal_port = 8080
force_https = true force_https = true
auto_stop_machines = true auto_stop_machines = 'stop'
auto_start_machines = true auto_start_machines = true
min_machines_running = 1 processes = ['app']
processes = ["app"]
[vm] [[vm]]
size = "shared-cpu-1x" size = 'shared-cpu-1x'
memory = "1gb" memory = '512mb'
cpu_kind = 'shared'
cpus = 1

6
go.mod
View file

@ -1,11 +1,13 @@
module github.com/AYM1607/ccclip module github.com/AYM1607/ccclip
go 1.21 go 1.24
toolchain go1.24.3
require ( require (
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-sqlite3 v1.14.18 github.com/mattn/go-sqlite3 v1.14.19
github.com/oklog/ulid/v2 v2.1.0 github.com/oklog/ulid/v2 v2.1.0
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
golang.org/x/crypto v0.14.0 golang.org/x/crypto v0.14.0

4
go.sum
View file

@ -5,8 +5,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=

View file

@ -22,7 +22,7 @@ type ConfigFile struct {
var Path string var Path string
func EnsureAndGet() (ConfigFile, error) { func EnsureAndGet() (ConfigFile, error) {
err := os.MkdirAll(Path, os.FileMode(int(0660))) err := os.MkdirAll(Path, os.FileMode(int(0770)))
if err != nil { if err != nil {
return ConfigFile{}, fmt.Errorf("could not create config directory: %w", err) return ConfigFile{}, fmt.Errorf("could not create config directory: %w", err)
} }
@ -38,7 +38,7 @@ func EnsureAndGet() (ConfigFile, error) {
} }
func Write(c ConfigFile) error { func Write(c ConfigFile) error {
err := os.MkdirAll(Path, os.FileMode(int(0660))) err := os.MkdirAll(Path, os.FileMode(int(0770)))
if err != nil { if err != nil {
return err return err
} }
@ -55,12 +55,12 @@ func Write(c ConfigFile) error {
func LoadPrivateKey() (*ecdh.PrivateKey, error) { func LoadPrivateKey() (*ecdh.PrivateKey, error) {
fp := path.Join(Path, PrivateKeyFileName) fp := path.Join(Path, PrivateKeyFileName)
return crypto.LoadPrivateKeyFromFile(fp) return crypto.LoadPrivateKeyFromFile(fp), nil
} }
func LoadPublicKey() (*ecdh.PublicKey, error) { func LoadPublicKey() (*ecdh.PublicKey, error) {
fp := path.Join(Path, PublicKeyFileName) fp := path.Join(Path, PublicKeyFileName)
return crypto.LoadPublicKeyFromFile(fp) return crypto.LoadPublicKeyFromFile(fp), nil
} }
func SavePrivateKey(k *ecdh.PrivateKey) error { func SavePrivateKey(k *ecdh.PrivateKey) error {

View file

@ -5,6 +5,7 @@ import (
"crypto/ecdh" "crypto/ecdh"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"time" "time"
@ -110,6 +111,8 @@ func (c *Client) SetClipboard(plaintext []byte, deviceId string, pvk *ecdh.Priva
} }
hres, err := http.Post(c.url+"/setClipboard", "application/json", bytes.NewReader(authReqJson)) hres, err := http.Post(c.url+"/setClipboard", "application/json", bytes.NewReader(authReqJson))
hresBytes, _ := io.ReadAll(hres.Body)
fmt.Println(string(hresBytes))
if err != nil { if err != nil {
return err return err
} }
@ -194,6 +197,8 @@ func (c *Client) getDevices(deviceId string, pvk *ecdh.PrivateKey) ([]*api.Devic
hresBody, err := io.ReadAll(hres.Body) hresBody, err := io.ReadAll(hres.Body)
defer hres.Body.Close() defer hres.Body.Close()
hresBytes, _ := io.ReadAll(hres.Body)
fmt.Println(string(hresBytes))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -8,7 +8,7 @@ import (
"github.com/AYM1607/ccclip/pkg/crypto" "github.com/AYM1607/ccclip/pkg/crypto"
) )
const serverPublicKeyB64 = "JTyaIVDHe1Nwqmd4NFlkvqj+MZOVp5s3JZP+T3QuoT8=" const serverPublicKeyB64 = "Dg6HYJ8aoQOOzGqOCw4J7tnT+QHkokjfdeWM8ktwnks="
var serverPublicKey *ecdh.PublicKey var serverPublicKey *ecdh.PublicKey

View file

@ -0,0 +1,9 @@
package server
import "log"
var logger *log.Logger
func init() {
logger = log.Default()
}

View file

@ -166,9 +166,11 @@ type GetUserDevicesResponse struct {
} }
func (c *controller) handleGetUserDevices(w http.ResponseWriter, r *http.Request) { func (c *controller) handleGetUserDevices(w http.ResponseWriter, r *http.Request) {
logger.Println("handling get user devices")
var authReq AuthenticatedPayload var authReq AuthenticatedPayload
err := json.NewDecoder(r.Body).Decode(&authReq) err := json.NewDecoder(r.Body).Decode(&authReq)
if err != nil { if err != nil {
logger.Printf("decoding request: %s\n", err)
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
@ -177,18 +179,21 @@ func (c *controller) handleGetUserDevices(w http.ResponseWriter, r *http.Request
// TODO: verify the request fingerprint. Right now we're just trusting that // TODO: verify the request fingerprint. Right now we're just trusting that
// if it decrypts successfully then we can trust it. // if it decrypts successfully then we can trust it.
if err != nil { if err != nil {
logger.Printf("decrypting payload: %s\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
user, err := c.store.GetDeviceUser(authReq.DeviceID) user, err := c.store.GetDeviceUser(authReq.DeviceID)
if err != nil { if err != nil {
logger.Printf("getting user for device: %s\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
devices, err := c.store.GetUserDevices(user.ID) devices, err := c.store.GetUserDevices(user.ID)
if err != nil { if err != nil {
logger.Printf("getting devices for user: %s\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
@ -197,6 +202,7 @@ func (c *controller) handleGetUserDevices(w http.ResponseWriter, r *http.Request
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(res) err = json.NewEncoder(w).Encode(res)
if err != nil { if err != nil {
logger.Printf("encoding response: %s\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
@ -208,9 +214,11 @@ type SetClipboardRequest struct {
} }
func (c *controller) handleSetClipboard(w http.ResponseWriter, r *http.Request) { func (c *controller) handleSetClipboard(w http.ResponseWriter, r *http.Request) {
log.Default().Println("handling set clipboard")
var authReq AuthenticatedPayload var authReq AuthenticatedPayload
err := json.NewDecoder(r.Body).Decode(&authReq) err := json.NewDecoder(r.Body).Decode(&authReq)
if err != nil { if err != nil {
log.Default().Printf("decoding request: %s\n", err)
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
@ -219,18 +227,21 @@ func (c *controller) handleSetClipboard(w http.ResponseWriter, r *http.Request)
// TODO: verify the request fingerprint. Right now we're just trusting that // TODO: verify the request fingerprint. Right now we're just trusting that
// if it decrypts successfully then we can trust it. // if it decrypts successfully then we can trust it.
if err != nil { if err != nil {
log.Default().Printf("decrypting authenticated payload: %s\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
user, err := c.store.GetDeviceUser(authReq.DeviceID) user, err := c.store.GetDeviceUser(authReq.DeviceID)
if err != nil { if err != nil {
log.Default().Printf("getting user device: %s\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
err = c.store.PutClipboard(user.ID, req.Clipboard) err = c.store.PutClipboard(user.ID, req.Clipboard)
if err != nil { if err != nil {
log.Default().Printf("putting keyboard: %s\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }