Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: per-user configs #78

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,11 @@ func (c *Client) FileInfo(id string) (*server.File, error) {
// join operation. This method will first attempt to securely connect over HTTPS, and (if that fails)
// fall back to skipping certificate verification. In the latter case, it will download and return
// the server certificate so the client can pin them.
func (c *Client) ServerInfo() (*server.Info, error) {
func (c *Client) ServerInfo(username string) (*server.Info, error) {
var err error

// First attempt to retrieve info with secure HTTP client
info, err := c.retrieveInfo(util.WithTimeout(util.NewHTTPClient()))
info, err := c.retrieveInfo(util.WithTimeout(util.NewHTTPClient()), username)
if err != nil {
// If this is not a cert error, fail immediately; there is nothing we can do
if !errors.As(err, &x509.UnknownAuthorityError{}) {
Expand All @@ -237,7 +237,7 @@ func (c *Client) ServerInfo() (*server.Info, error) {

// Then attempt to retrieve ignoring bad certs; this is okay, we pin the cert if it's bad
// and warn the user about this.
info, err = c.retrieveInfo(util.WithTimeout(util.NewHTTPClientWithInsecureTransport()))
info, err = c.retrieveInfo(util.WithTimeout(util.NewHTTPClientWithInsecureTransport()), username)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -281,13 +281,15 @@ func (c *Client) Verify(cert *x509.Certificate, key *crypto.Key) error {

func (c *Client) addAuthHeader(req *http.Request, key *crypto.Key) error {
if key == nil {
key = c.config.Key
if userConfig, ok := c.config.Users[c.config.User]; ok {
key = userConfig.Key
}
}
if key == nil {
return nil // No auth configured
}

auth, err := crypto.GenerateAuthHMAC(key.Bytes, req.Method, req.URL.Path, useDefaultAuthTTL) // RequestURI is empty!
auth, err := crypto.GenerateAuthHMAC(c.config.User, key.Bytes, req.Method, req.URL.Path) // RequestURI is empty!
if err != nil {
return err
}
Expand Down Expand Up @@ -321,8 +323,8 @@ func (c *Client) parseFileInfoResponse(resp *http.Response) (*server.File, error
}, nil
}

func (c *Client) retrieveInfo(client *http.Client) (*server.Info, error) {
resp, err := client.Get(fmt.Sprintf("%s/info", config.ExpandServerAddr(c.config.ServerAddr)))
func (c *Client) retrieveInfo(client *http.Client, username string) (*server.Info, error) {
resp, err := client.Get(fmt.Sprintf("%s/info?u=%s", config.ExpandServerAddr(c.config.ServerAddr), username))
if err != nil {
return nil, err
}
Expand Down
5 changes: 2 additions & 3 deletions clipboard/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ func (c *Clipboard) Stat(id string) (*File, error) {
var cf File
if err := json.NewDecoder(mf).Decode(&cf); err != nil {
log.Printf("error reading meta file for %s: %s", id, err.Error())
cf.Expires = int64(c.config.FileExpireAfterDefault.Seconds())
}
cf.ID = id
cf.Size = stat.Size()
Expand All @@ -192,7 +191,7 @@ func (c *Clipboard) Allow() bool {
// The method observes the per-file size limit as defined in the config, as well as the total clipboard
// size limit. If a limit is reached, it will return util.ErrLimitReached. When the target file is a FIFO
// pipe (see MakePipe) and the consumer prematurely interrupts reading, ErrBrokenPipe may be returned.
func (c *Clipboard) WriteFile(id string, meta *File, rc io.ReadCloser) error {
func (c *Clipboard) WriteFile(id string, meta *File, rc io.ReadCloser, fileSizeLimit int64) error {
file, metafile, err := c.getFilenames(id)
if err != nil {
return err
Expand All @@ -215,7 +214,7 @@ func (c *Clipboard) WriteFile(id string, meta *File, rc io.ReadCloser) error {
}
defer f.Close()

fileSizeLimiter := util.NewLimiter(c.config.FileSizeLimit)
fileSizeLimiter := util.NewLimiter(fileSizeLimit)
limitWriter := util.NewLimitWriter(f, fileSizeLimiter, c.sizeLimiter)

if _, err := io.Copy(limitWriter, rc); err != nil {
Expand Down
15 changes: 2 additions & 13 deletions cmd/copy_paste.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/urfave/cli/v2"
"heckel.io/pcopy/client"
"heckel.io/pcopy/config"
"heckel.io/pcopy/crypto"
"heckel.io/pcopy/server"
"heckel.io/pcopy/util"
"io"
Expand Down Expand Up @@ -53,9 +52,7 @@ Examples:
echo hi | pcp -l work: # Copies 'hi' to the 'work' clipboard and print links
echo ho | pcp work:bla # Copies 'ho' to the 'work' clipboard as 'bla'
pcp : img1/ img2/ # Creates ZIP from two folders and copies it to the default clipboard
yes | pcp --stream # Stream contents to the other end via FIFO device

To override or specify the remote server key, you may pass the PCOPY_KEY variable.`,
yes | pcp --stream # Stream contents to the other end via FIFO device`,
}

var cmdPaste = &cli.Command{
Expand Down Expand Up @@ -86,9 +83,7 @@ Examples:
ppaste bar > bar.txt # Reads 'bar' from the default clipboard to file 'bar.txt'
ppaste work: # Reads from the 'work' clipboard and prints its contents
ppaste work:ho > ho.txt # Reads 'ho' from the 'work' clipboard to file 'ho.txt'
ppaste : images/ # Extracts ZIP from default clipboard to folder images/

To override or specify the remote server key, you may pass the PCOPY_KEY variable.`,
ppaste : images/ # Extracts ZIP from default clipboard to folder images/`,
}

func execCopy(c *cli.Context) error {
Expand Down Expand Up @@ -278,12 +273,6 @@ func parseClientArgs(c *cli.Context) (*config.Config, string, []string, error) {
progressOutput(c.App.ErrWriter, processed, total, done)
}
}
if os.Getenv(config.EnvKey) != "" {
conf.Key, err = crypto.DecodeKey(os.Getenv(config.EnvKey))
if err != nil {
return nil, "", nil, err
}
}

return conf, id, files, nil
}
Expand Down
53 changes: 29 additions & 24 deletions cmd/join.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var cmdJoin = &cli.Command{
&cli.BoolFlag{Name: "force", Aliases: []string{"f"}, Usage: "overwrite config if it already exists"},
&cli.BoolFlag{Name: "auto", Aliases: []string{"a"}, Usage: "automatically choose clipboard alias"},
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Usage: "do not print instructions"},
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, DefaultText: string(config.DefaultUser), Usage: "join as a different user"},
},
Description: `Connects to a remote clipboard with the server address SERVER. CLIPBOARD is the local alias
that can be used to identify it (default is 'default'). This command is interactive and
Expand All @@ -38,20 +39,25 @@ If the remote server's certificate is self-signed, its certificate will be downl
~/.config/pcopy/$CLIPBOARD.crt (or /etc/pcopy/$CLIPBOARD.crt) and pinned for future connections.

Examples:
pcopy join pcopy.example.com # Joins remote clipboard as local alias 'default'
pcopy join pcopy.work.com work # Joins remote clipboard with local alias 'work'`,
pcopy join pcopy.example.com # Joins clipboard as local alias 'default'
pcopy join pcopy.work.com work # Joins clipboard with local alias 'work'
pcopy join --user phil nopaste.net # Joins clipboard as user 'phil'`,
}

func execJoin(c *cli.Context) error {
force := c.Bool("force")
auto := c.Bool("auto")
quiet := c.Bool("quiet")
username := c.String("user")
if c.NArg() < 1 {
return errors.New("missing server address, see --help for usage details")
}
if force && auto {
return errors.New("cannot use both --auto and --force")
}
if username == "" {
username = config.DefaultUser
}

clipboard := config.DefaultClipboard
rawServerAddr := c.Args().Get(0)
Expand All @@ -67,11 +73,11 @@ func execJoin(c *cli.Context) error {
}

// Read basic info from server
info, err := readServerInfo(c, rawServerAddr)
info, err := readServerInfo(c, rawServerAddr, username)
if err != nil {
return err
}
pclient, err := client.NewClient(&config.Config{ServerAddr: info.ServerAddr})
pclient, err := client.NewClient(&config.Config{ServerAddr: info.ServerAddr, User: username})
if err != nil {
return err
}
Expand All @@ -80,30 +86,25 @@ func execJoin(c *cli.Context) error {
var key *crypto.Key

if info.Salt != nil {
envKey := os.Getenv(config.EnvKey)
if envKey != "" {
key, err = crypto.DecodeKey(envKey)
if err != nil {
return err
}
} else {
password, err := readPassword(c)
if err != nil {
return err
}
key = crypto.DeriveKey(password, info.Salt)
err = pclient.Verify(info.Cert, key)
if err != nil {
return fmt.Errorf("failed to join clipboard: %s", err.Error())
}
password, err := readPassword(c)
if err != nil {
return err
}
key = crypto.DeriveKey(password, info.Salt)
err = pclient.Verify(info.Cert, key)
if err != nil {
return fmt.Errorf("failed to join clipboard: %s", err.Error())
}
}

// Write config file
conf := config.New()
conf.ServerAddr = config.CollapseServerAddr(info.ServerAddr)
conf.DefaultID = info.DefaultID
conf.Key = key // May be nil, but that's ok
conf.User = username
conf.Users[username] = &config.User{
Key: key, // May be nil, but that's ok
}
if err := conf.WriteFile(configFile); err != nil {
return err
}
Expand Down Expand Up @@ -136,8 +137,12 @@ type serverInfoResult struct {
// readServerInfo is doing a parallel lookup for all potential server addresses. For instance, "nopaste.net"
// is expanded to ["https://nopaste.net:2586", "https://nopaste.net:443"] so we check both addresses in
// parallel and return the first one that returns, or return an error with all errors.
func readServerInfo(c *cli.Context, rawServerAddr string) (*server.Info, error) {
fmt.Fprintf(c.App.ErrWriter, "Joining clipboard at %s ... ", rawServerAddr)
func readServerInfo(c *cli.Context, rawServerAddr string, username string) (*server.Info, error) {
if username != config.DefaultUser {
fmt.Fprintf(c.App.ErrWriter, "Joining clipboard at %s ... ", rawServerAddr)
} else {
fmt.Fprintf(c.App.ErrWriter, "Joining clipboard at %s as user %s ... ", rawServerAddr, username)
}

resultChan := make(chan *serverInfoResult)
serverAddrs := config.ExpandServerAddrsGuess(rawServerAddr)
Expand All @@ -146,7 +151,7 @@ func readServerInfo(c *cli.Context, rawServerAddr string) (*server.Info, error)
for _, serverAddr := range serverAddrs {
go func(serverAddr string) {
pclient, _ := client.NewClient(&config.Config{ServerAddr: serverAddr})
serverInfo, err := pclient.ServerInfo()
serverInfo, err := pclient.ServerInfo(username)
if err != nil {
resultChan <- &serverInfoResult{addr: serverAddr, err: err}
return
Expand Down
15 changes: 3 additions & 12 deletions cmd/serve.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package cmd

import (
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/pcopy/config"
"heckel.io/pcopy/crypto"
"heckel.io/pcopy/server"
"log"
"os"
Expand Down Expand Up @@ -34,10 +34,7 @@ To generate a new config file, you may want to use the 'pcopy setup' command.

Examples:
pcopy serve # Starts server in the foreground
pcopy serve --listen-https :9999 # Starts server with alternate port
PCOPY_KEY=.. pcopy serve # Starts server with alternate key (see 'pcopy keygen')

To override or specify the remote server key, you may pass the PCOPY_KEY variable.`,
pcopy serve --listen-https :9999 # Starts server with alternate port`,
}

func execServe(c *cli.Context) error {
Expand All @@ -62,6 +59,7 @@ func execServe(c *cli.Context) error {
if len(configs) == 0 {
return cli.Exit("No valid config files found. Exiting", 1)
}
fmt.Printf("%#v\n", configs[0].Users)
return server.Serve(configs...)
}

Expand Down Expand Up @@ -127,12 +125,5 @@ func maybeOverrideOptions(conf *config.Config, listenHTTPS, listenHTTP, serverAd
if certFile != "" {
conf.CertFile = certFile
}
if os.Getenv(config.EnvKey) != "" {
var err error
conf.Key, err = crypto.DecodeKey(os.Getenv(config.EnvKey))
if err != nil {
return nil, err
}
}
return conf, nil
}
2 changes: 1 addition & 1 deletion cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func (w *wizard) askPassword() {
fmt.Fprintln(w.context.App.ErrWriter)
fmt.Fprintln(w.context.App.ErrWriter)
if string(password) != "" {
w.config.Key, err = crypto.GenerateKey(password)
w.config.Users[config.DefaultUser].Key, err = crypto.GenerateKey(password)
if err != nil {
w.fail(err)
}
Expand Down
8 changes: 8 additions & 0 deletions config/config.conf
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,11 @@
# Default: rw ro
#
# FileModesAllowed rw ro

User Phil
Key pNNRAc3smnuZGg==:Qld+V009aezUQNJI6qfiQDabPhHbwacA0Zjts/nvQYs=
FileExpireAfter 1y

User Bob
Key ...
FileSizeLimit 20M
4 changes: 3 additions & 1 deletion config/config.conf.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
# Format: SALT:KEY (both base64 encoded)
# Default: None
#
{{if .Key}}Key {{encodeKey .Key}}{{else}}# Key{{end}}
{{if .Users.default.Key}}Key {{encodeKey .Users.default.Key}}{{else}}# Key{{end}}

# Path to the private key for the matching certificate. If not set, the config file path (with
# a .key extension) is assumed to be the path to the private key, e.g. server.key (if the config
Expand Down Expand Up @@ -157,3 +157,5 @@
#
{{$fileModesAllowedStr := stringsJoin .FileModesAllowed " " -}}
{{if or (eq "rw ro" $fileModesAllowedStr) (not .FileModesAllowed)}}# FileModesAllowed rw ro{{else}}FileModesAllowed {{$fileModesAllowedStr}}{{end}}

# User
Loading