Initial commit
Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
commit
ef9aca1f3b
26 changed files with 1668 additions and 0 deletions
254
src/cmd/coller/main.go
Normal file
254
src/cmd/coller/main.go
Normal file
|
@ -0,0 +1,254 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"git.riou.xyz/jriou/coller/internal"
|
||||
"golang.design/x/clipboard"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var (
|
||||
AppName = "coller"
|
||||
AppVersion string
|
||||
GoVersion string
|
||||
GitCommit string
|
||||
)
|
||||
|
||||
type NotePayload struct {
|
||||
Content string `json:"content"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
Expiration int `json:"expiration,omitempty"`
|
||||
DeleteAfterRead bool `json:"delete_after_read,omitempty"`
|
||||
}
|
||||
|
||||
type NoteResponse struct {
|
||||
ID string `json:"id"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func handleMain() int {
|
||||
var err error
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Printf("could not find home directory: %v\n", err)
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
|
||||
version := flag.Bool("version", false, "Print version and exit")
|
||||
quiet := flag.Bool("quiet", false, "Log errors only")
|
||||
verbose := flag.Bool("verbose", false, "Print more logs")
|
||||
debug := flag.Bool("debug", false, "Print even more logs")
|
||||
configFile := flag.String("config", filepath.Join(homeDir, ".config", AppName+".json"), "Configuration file")
|
||||
reconfigure := flag.Bool("reconfigure", false, "Re-create configuration file")
|
||||
url := flag.String("url", "", "URL of the coller API")
|
||||
password := flag.String("password", os.Getenv("COPIER_PASSWORD"), "Password to decrypt the note")
|
||||
askPassword := flag.Bool("ask-password", false, "Read password from input")
|
||||
noPassword := flag.Bool("no-password", false, "Allow notes without password")
|
||||
passwordLength := flag.Int("password-length", 16, "Length of the auto-generated password")
|
||||
fileName := flag.String("file", "", "Read content of the note from a file")
|
||||
expiration := flag.Int("expiration", 0, "Number of seconds before expiration")
|
||||
deleteAfterRead := flag.Bool("delete-after-read", false, "Delete the note after the first read")
|
||||
copier := flag.Bool("copier", false, "Print the copier command to decrypt the note")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *version {
|
||||
internal.ShowVersion(AppName, AppVersion, GitCommit, GoVersion)
|
||||
return internal.RC_OK
|
||||
}
|
||||
|
||||
var level slog.Level
|
||||
if *debug {
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
if *verbose {
|
||||
level = slog.LevelInfo
|
||||
}
|
||||
if *quiet {
|
||||
level = slog.LevelError
|
||||
}
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
|
||||
|
||||
if *url == "" {
|
||||
if _, err = os.Stat(*configFile); errors.Is(err, os.ErrNotExist) || *reconfigure {
|
||||
logger.Debug("creating config")
|
||||
config := CreateConfig()
|
||||
|
||||
logger.Debug("writing configuration file")
|
||||
err := WriteConfig(config, *configFile)
|
||||
if err != nil {
|
||||
return ReturnError(logger, "could not create configuration file", err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("reading configuration file", slog.Any("config", *configFile))
|
||||
var config Config
|
||||
err = internal.ReadConfig(*configFile, &config)
|
||||
if err != nil {
|
||||
return ReturnError(logger, "could not read configuration file", err)
|
||||
}
|
||||
|
||||
*url = config.URL
|
||||
}
|
||||
|
||||
var content []byte
|
||||
if *fileName != "" {
|
||||
logger.Debug("reading from file", slog.Any("file", *fileName))
|
||||
content, err = os.ReadFile(*fileName)
|
||||
if err != nil {
|
||||
return ReturnError(logger, "could not read from file", err)
|
||||
}
|
||||
} else {
|
||||
err = clipboard.Init()
|
||||
if err != nil {
|
||||
return ReturnError(logger, "could not initialize clipboard library", err)
|
||||
}
|
||||
content = clipboard.Read(clipboard.FmtText)
|
||||
}
|
||||
|
||||
if *askPassword {
|
||||
fmt.Print("Password: ")
|
||||
p, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return ReturnError(logger, "could not read password", err)
|
||||
}
|
||||
*password = string(p)
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
if !*noPassword && *password == "" {
|
||||
logger.Debug("generating random password")
|
||||
if *passwordLength < internal.MIN_PASSWORD_LENGTH || *passwordLength > internal.MAX_PASSWORD_LENGTH {
|
||||
return ReturnError(logger, "invalid password length for auto-generated password", fmt.Errorf("password length must be between %d and %d", internal.MIN_PASSWORD_LENGTH, internal.MAX_PASSWORD_LENGTH))
|
||||
}
|
||||
*password = internal.GenerateChars(*passwordLength)
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
return ReturnError(logger, "could not create empty note", nil)
|
||||
}
|
||||
|
||||
p := NotePayload{}
|
||||
if *expiration != 0 {
|
||||
p.Expiration = *expiration
|
||||
}
|
||||
if *deleteAfterRead {
|
||||
p.DeleteAfterRead = *deleteAfterRead
|
||||
}
|
||||
|
||||
if *password != "" {
|
||||
logger.Debug("validating password")
|
||||
if err = internal.ValidatePassword(*password); err != nil {
|
||||
return ReturnError(logger, "invalid password", nil)
|
||||
}
|
||||
logger.Debug("encrypting content")
|
||||
content, err = internal.Encrypt(content, *password)
|
||||
if err != nil {
|
||||
return ReturnError(logger, "could not encrypt note", err)
|
||||
}
|
||||
p.Encrypted = true
|
||||
}
|
||||
|
||||
logger.Debug("encoding content")
|
||||
encoded := internal.Encode(content)
|
||||
p.Content = encoded
|
||||
|
||||
payload, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return ReturnError(logger, "could not serialize note to json", err)
|
||||
}
|
||||
|
||||
apiRoute := *url + "/api/note"
|
||||
|
||||
logger.Debug("creating note", slog.Any("payload", payload), slog.Any("url", apiRoute))
|
||||
|
||||
r, err := http.Post(apiRoute, "application/json", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return ReturnError(logger, "could not create note", err)
|
||||
}
|
||||
|
||||
logger.Debug("reading response", slog.Any("response", r))
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return ReturnError(logger, "could not read response", err)
|
||||
}
|
||||
|
||||
jsonBody := &NoteResponse{}
|
||||
err = json.Unmarshal(body, jsonBody)
|
||||
if err != nil {
|
||||
return ReturnError(logger, "could not decode response", err)
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return ReturnError(logger, jsonBody.Message, fmt.Errorf("%s", jsonBody.Error))
|
||||
}
|
||||
|
||||
logger.Debug("finding note location")
|
||||
var location string
|
||||
noteURL := *url + "/" + jsonBody.ID
|
||||
if *password != "" {
|
||||
if *copier {
|
||||
location = fmt.Sprintf("copier -password %s %s", *password, noteURL)
|
||||
} else {
|
||||
location = fmt.Sprintf("%s/%s", noteURL, *password)
|
||||
}
|
||||
} else {
|
||||
location = fmt.Sprintf("%s", noteURL)
|
||||
}
|
||||
|
||||
logger.Debug("displaying note location")
|
||||
fmt.Printf("%s\n", location)
|
||||
|
||||
return internal.RC_OK
|
||||
}
|
||||
|
||||
func main() {
|
||||
os.Exit(handleMain())
|
||||
}
|
||||
|
||||
func CreateConfig() Config {
|
||||
var url string
|
||||
fmt.Print("Instance URL: ")
|
||||
fmt.Scan(&url)
|
||||
return Config{URL: url}
|
||||
}
|
||||
|
||||
func WriteConfig(config Config, fileName string) error {
|
||||
content, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(fileName, content, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReturnError(logger *slog.Logger, message string, err error) int {
|
||||
if err != nil {
|
||||
logger.Error(message, slog.Any("error", err))
|
||||
} else {
|
||||
logger.Error(message)
|
||||
}
|
||||
return internal.RC_ERROR
|
||||
}
|
89
src/cmd/collerd/main.go
Normal file
89
src/cmd/collerd/main.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.riou.xyz/jriou/coller/internal"
|
||||
"git.riou.xyz/jriou/coller/server"
|
||||
)
|
||||
|
||||
var (
|
||||
AppName = "collerd"
|
||||
AppVersion string
|
||||
GoVersion string
|
||||
GitCommit string
|
||||
)
|
||||
|
||||
func handleMain() int {
|
||||
|
||||
var err error
|
||||
config := server.NewConfig()
|
||||
|
||||
version := flag.Bool("version", false, "Print version and exit")
|
||||
quiet := flag.Bool("quiet", false, "Log errors only")
|
||||
verbose := flag.Bool("verbose", false, "Print more logs")
|
||||
debug := flag.Bool("debug", false, "Print even more logs")
|
||||
configFileName := flag.String("config", "", "Configuration file name")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *version {
|
||||
internal.ShowVersion(AppName, AppVersion, GitCommit, GoVersion)
|
||||
return internal.RC_OK
|
||||
}
|
||||
|
||||
var level slog.Level
|
||||
if *debug {
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
if *verbose {
|
||||
level = slog.LevelInfo
|
||||
}
|
||||
if *quiet {
|
||||
level = slog.LevelError
|
||||
}
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
|
||||
|
||||
if *configFileName != "" {
|
||||
err = internal.ReadConfig(*configFileName, config)
|
||||
if err != nil {
|
||||
slog.Error("cannot parse configuration file", slog.Any("error", err))
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
logger.Debug("configuration file parsed", slog.Any("file", *configFileName))
|
||||
}
|
||||
|
||||
if err = config.Check(); err != nil {
|
||||
logger.Error("invalid configuration", slog.Any("error", err))
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
|
||||
db, err := server.NewDatabase(logger, config)
|
||||
if err != nil {
|
||||
slog.Error("could not connect to the database", slog.Any("error", err))
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
|
||||
srv, err := server.NewServer(logger, db, config, AppVersion)
|
||||
if err != nil {
|
||||
logger.Error("could not create server", slog.Any("error", err))
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
|
||||
srv.SetIDLength(config.IDLength)
|
||||
srv.SetPasswordLength(config.PasswordLength)
|
||||
|
||||
err = srv.Start()
|
||||
if err != nil {
|
||||
logger.Error("could not start server", slog.Any("error", err))
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
|
||||
return internal.RC_OK
|
||||
}
|
||||
|
||||
func main() {
|
||||
os.Exit(handleMain())
|
||||
}
|
118
src/cmd/copier/main.go
Normal file
118
src/cmd/copier/main.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"git.riou.xyz/jriou/coller/internal"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var (
|
||||
AppName = "copier"
|
||||
AppVersion string
|
||||
GoVersion string
|
||||
GitCommit string
|
||||
)
|
||||
|
||||
func handleMain() int {
|
||||
|
||||
flag.Usage = usage
|
||||
|
||||
version := flag.Bool("version", false, "Print version and exit")
|
||||
quiet := flag.Bool("quiet", false, "Log errors only")
|
||||
verbose := flag.Bool("verbose", false, "Print more logs")
|
||||
debug := flag.Bool("debug", false, "Print even more logs")
|
||||
password := flag.String("password", os.Getenv("COPIER_PASSWORD"), "Password to decrypt the note")
|
||||
askPassword := flag.Bool("w", false, "Read password from input")
|
||||
fileName := flag.String("file", "", "Write content of the note to a file")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() != 1 {
|
||||
usage()
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
|
||||
url := flag.Args()[0]
|
||||
|
||||
if *version {
|
||||
internal.ShowVersion(AppName, AppVersion, GitCommit, GoVersion)
|
||||
return internal.RC_OK
|
||||
}
|
||||
|
||||
var level slog.Level
|
||||
if *debug {
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
if *verbose {
|
||||
level = slog.LevelInfo
|
||||
}
|
||||
if *quiet {
|
||||
level = slog.LevelError
|
||||
}
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
|
||||
|
||||
if *askPassword {
|
||||
fmt.Print("Password: ")
|
||||
p, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
logger.Error("could not read password", slog.Any("error", err))
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
*password = string(p)
|
||||
}
|
||||
|
||||
logger.Debug("parsing url", slog.Any("url", url))
|
||||
r, err := http.Get(url)
|
||||
if err != nil {
|
||||
logger.Error("could not retreive note", slog.Any("error", err))
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
|
||||
logger.Debug("decoding body")
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
logger.Error("could not read response", slog.Any("error", err))
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
|
||||
var content []byte
|
||||
if *password != "" {
|
||||
logger.Debug("decrypting note")
|
||||
content, err = internal.Decrypt(body, *password)
|
||||
if err != nil {
|
||||
logger.Error("could not decrypt paste", slog.Any("error", err))
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
} else {
|
||||
content = body
|
||||
}
|
||||
|
||||
if *fileName != "" {
|
||||
logger.Debug("writing output to file", slog.Any("file", *fileName))
|
||||
err = os.WriteFile(*fileName, content, 0644)
|
||||
if err != nil {
|
||||
logger.Error("could not write output to file", slog.Any("error", err))
|
||||
return internal.RC_ERROR
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("%s", content)
|
||||
}
|
||||
|
||||
return internal.RC_OK
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Printf("Usage: %s [OPTIONS] URL\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
os.Exit(handleMain())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue