├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── client └── client.go ├── lemon ├── cli.go ├── flag.go ├── flag_test.go ├── main.go └── main_test.go ├── main.go ├── pkg └── .gitkeep ├── server └── server.go └── sideci.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /lemonade 2 | /dist/ 3 | /pkg/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | go: 6 | - 1.9.x 7 | - 1.10.x 8 | - 1.11.x 9 | - tip 10 | 11 | before_script: 12 | - 'go get ./...' 13 | 14 | script: 15 | - 'go test -v -race ./...' 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Masataka Kuwabara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell git describe --tags) 2 | 3 | build: 4 | go build -ldflags "-X github.com/lemonade-command/lemonade/lemon.Version=$(VERSION)" 5 | 6 | install: 7 | go install -ldflags "-X github.com/lemonade-command/lemonade/lemon.Version=$(VERSION)" 8 | 9 | release: 10 | gox --arch 'amd64 386' --os 'windows linux darwin' --output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}/{{.Dir}}" -ldflags "-s -w -X github.com/lemonade-command/lemonade/lemon.Version=$(VERSION)" 11 | zip pkg/lemonade_windows_386.zip dist/lemonade_windows_386/lemonade.exe -j 12 | zip pkg/lemonade_windows_amd64.zip dist/lemonade_windows_amd64/lemonade.exe -j 13 | tar zcvf pkg/lemonade_linux_386.tar.gz -C dist/lemonade_linux_386/ lemonade 14 | tar zcvf pkg/lemonade_linux_amd64.tar.gz -C dist/lemonade_linux_amd64/ lemonade 15 | tar zcvf pkg/lemonade_darwin_386.tar.gz -C dist/lemonade_darwin_386/ lemonade 16 | tar zcvf pkg/lemonade_darwin_amd64.tar.gz -C dist/lemonade_darwin_amd64/ lemonade 17 | 18 | clean: 19 | rm -rf dist/ 20 | rm -f pkg/*.tar.gz pkg/*.zip 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Lemonade 3 | ======== 4 | 5 | remote...lemote...lemode......Lemonade!!! :lemon: :lemon: 6 | 7 | Lemonade is a remote utility tool. 8 | (copy, paste and open browser) over TCP. 9 | 10 | [![Build Status](https://travis-ci.org/lemonade-command/lemonade.svg?branch=master)](https://travis-ci.org/lemonade-command/lemonade) 11 | 12 | Maintainers wanted 13 | ========== 14 | 15 | See https://github.com/lemonade-command/lemonade/issues/25 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | ```sh 22 | go get -d github.com/lemonade-command/lemonade 23 | cd $GOPATH/src/github.com/lemonade-command/lemonade/ 24 | make install 25 | ``` 26 | 27 | Or download from [latest release](https://github.com/lemonade-command/lemonade/releases/latest) 28 | 29 | 30 | Example of use 31 | ---------------- 32 | 33 | ![Example](http://f.st-hatena.com/images/fotolife/P/Pocke/20150823/20150823173041.gif) 34 | 35 | For example, you use a Linux as a virtual machine on Windows host. 36 | You connect to Linux by SSH client(e.g. PuTTY). 37 | When you want to copy text of a file on Linux to Windows, what do you do? 38 | One solution is doing `cat file.txt` and drag displayed text. 39 | But this answer is NOT elegant! Because your hand leaves from the keyboard to use the mouse. 40 | 41 | Another solution is using the Lemonade. 42 | You input `cat file.txt | lemonade copy`. Then, lemonade copies text of the file to clipboard of the Windows! 43 | 44 | In addition to the above, lemonade supports pasting and opening URL. 45 | 46 | 47 | Usage 48 | -------- 49 | 50 | ```sh 51 | Usage: lemonade [options]... SUB_COMMAND [arg] 52 | Sub Commands: 53 | open [URL] Open URL by browser 54 | copy [text] Copy text. 55 | paste Paste text. 56 | server Start lemonade server. 57 | 58 | Options: 59 | --port=2489 TCP port number 60 | --line-ending Convert Line Ending(CR/CRLF) 61 | --allow="0.0.0.0/0,::/0" Allow IP Range [Server only] 62 | --host="localhost" Destination hostname [Client only] 63 | --no-fallback-messages Do not show fallback messages [Client only] 64 | --trans-loopback=true Translate loopback address [open subcommand only] 65 | --trans-localfile=true Translate local file path [open subcommand only] 66 | --help Show this message 67 | ``` 68 | 69 | 70 | ### On server (in the above, Windows) 71 | 72 | ```sh 73 | $ lemonade server 74 | ``` 75 | 76 | 77 | ### Client (in the above, Linux) 78 | 79 | 80 | ```sh 81 | # You want to copy a text 82 | $ cat file.txt | lemonade copy 83 | 84 | # You want to paste a text from the clipboard of Windows 85 | $ lemonade paste 86 | 87 | # You want to open an URL to a browser on Windows. 88 | $ lemonade open 'http://google.com' 89 | ``` 90 | 91 | 92 | Configuration 93 | --------------- 94 | 95 | You can override command line options by configuration file. 96 | There is configuration file at `~/.config/lemonade.toml`. 97 | 98 | ### Server 99 | 100 | ```toml 101 | port = 1234 102 | allow = '192.168.0.0/24' 103 | line-ending = 'crlf' 104 | ``` 105 | 106 | - `port` is a listening port of TCP. 107 | - `allow` is a comma separated list of a allowed IP address(with CIDR block). 108 | 109 | 110 | ### Client 111 | 112 | ```toml 113 | port = 1234 114 | host = '192.168.x.x' 115 | trans-loopback = true 116 | trans-localfile = true 117 | line-ending = 'crlf' 118 | ``` 119 | 120 | - `port` is a port of server. 121 | - `host` is a hostname of server. 122 | - `trans-loopback` is a flag of translation loopback address. 123 | - `trans-localfile` is a flag of translation localfile. 124 | 125 | Detail of `trans-loopback` and `trans-localfile` are described Advanced Usage. 126 | 127 | 128 | Advanced Usage 129 | ----------------- 130 | 131 | 132 | ### trans-loopback 133 | 134 | Default: true 135 | 136 | This option works with `open` command only. 137 | 138 | If this option is true, lemonade translates loopback address to address of client. 139 | 140 | For example, you input `lemonade open 'http://127.0.0.1:8000'`. 141 | If this option is false, server receives loopback address. 142 | But this isn't expected. 143 | Because, at server, loopback address is server itself. 144 | 145 | If this option is true, server receives IP address of client. 146 | So, server can open URL! 147 | 148 | 149 | ### trans-localfile 150 | 151 | Default: true 152 | 153 | This option works with `open` command only. 154 | 155 | If this option is true, lemonade translates path of local file to address of client. 156 | 157 | For example, you input `lemonade open ./file.txt`. 158 | If this option is false, server receives `./file.txt`. 159 | But this isn't expected. 160 | Because, at server, `./file.txt` doesn't exist. 161 | 162 | If this option is true, server receives IP address of client. And client serve the local file. 163 | So, server can open the local file! 164 | 165 | 166 | ### line-ending 167 | 168 | Default: "" (NONE) 169 | 170 | This options works with `copy` and `paste` command only. 171 | 172 | If this option is `lf` or `crlf`, lemonade converts the line ending of text to the specified. 173 | 174 | 175 | ### Alias 176 | 177 | You can use lemonade as a `xdg-open`, `pbcopy` and `pbpaste`. 178 | 179 | 180 | For example. 181 | 182 | ```sh 183 | $ ln -s /path/to/lemonade /usr/bin/xdg-open 184 | $ xdg-open 'http://example.com' # Same as lemonade open 'http://example.com' 185 | ``` 186 | 187 | 188 | ### Secure TCP Connection 189 | 190 | Lemonade doesn't provide encryption and authorization. 191 | However we can use `SSH Port forwarding` for the purpose. 192 | 193 | lemonade server 194 | 195 | ```sh 196 | $ lemonade server -allow 127.0.0.1 & 197 | $ ssh -R 2489:127.0.0.1:2489 user@host 198 | ``` 199 | 200 | See: 201 | 202 | - [SSH/OpenSSH/PortForwarding - Community Help Wiki](https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding) 203 | - [WOW! and security? · Issue #14 · lemonade-command/lemonade](https://github.com/lemonade-command/lemonade/issues/14#) 204 | 205 | 206 | 207 | Links 208 | ------- 209 | 210 | - https://speakerdeck.com/pocke/remote-utility-tool-lemonade 211 | - [リモートのPCのブラウザやクリップボードを操作するツール Lemonade を作った - pockestrap](http://pocke.hatenablog.com/entry/2015/07/04/235118) 212 | - [リモートユーティリティーツール、Lemonade v0.2.0 をリリースした - pockestrap](http://pocke.hatenablog.com/entry/2015/08/23/221543) 213 | - [lemonade v1.0.0をリリースした - pockestrap](http://pocke.hatenablog.com/entry/2016/04/19/233423) 214 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "mime/multipart" 10 | "net/http" 11 | "os" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/atotto/clipboard" 16 | log "github.com/inconshreveable/log15" 17 | "github.com/lemonade-command/lemonade/lemon" 18 | ) 19 | 20 | type client struct { 21 | host string 22 | port int 23 | addr string 24 | lineEnding string 25 | noFallbackMessages bool 26 | logger log.Logger 27 | } 28 | 29 | func New(c *lemon.CLI, logger log.Logger) *client { 30 | return &client{ 31 | host: c.Host, 32 | port: c.Port, 33 | addr: fmt.Sprintf("http://%s:%d", c.Host, c.Port), 34 | lineEnding: c.LineEnding, 35 | noFallbackMessages: c.NoFallbackMessages, 36 | logger: logger, 37 | } 38 | } 39 | 40 | func (c *client) Copy(text string) error { 41 | c.logger.Debug("Sending: " + text) 42 | url := fmt.Sprintf("%s/copy", c.addr) 43 | _, err := http.Post(url, "text/plain", strings.NewReader(text)) 44 | if err != nil { 45 | clipboard.WriteAll(text) 46 | return err 47 | } 48 | return nil 49 | } 50 | 51 | func (c *client) Paste() (string, error) { 52 | url := fmt.Sprintf("%s/paste", c.addr) 53 | r, err := http.Get(url) 54 | if err != nil { 55 | c.logger.Error("http error.", "err", err.Error()) 56 | return "", err 57 | } 58 | 59 | defer r.Body.Close() 60 | body, err := ioutil.ReadAll(r.Body) 61 | if err != nil { 62 | c.logger.Error("http read body error.", "err", err.Error()) 63 | return "", err 64 | } 65 | 66 | return lemon.ConvertLineEnding(string(body), c.lineEnding), nil 67 | } 68 | 69 | func fileExists(fname string) bool { 70 | _, err := os.Stat(fname) 71 | return err == nil 72 | } 73 | 74 | func (c *client) postFile(name string, url string) (*http.Response, error) { 75 | bodyBuf := bytes.NewBufferString("") 76 | bodyWriter := multipart.NewWriter(bodyBuf) 77 | 78 | fileWriter, err := bodyWriter.CreateFormFile("uploadFile", name) 79 | if err != nil { 80 | c.logger.Error("Writing to buffer", "name", name) 81 | return nil, err 82 | } 83 | 84 | file, err := os.Open(name) 85 | if err != nil { 86 | c.logger.Error("cant Opening file", "name", name) 87 | return nil, err 88 | } 89 | 90 | _, err = io.Copy(fileWriter, file) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | contentType := bodyWriter.FormDataContentType() 96 | bodyWriter.Close() 97 | return http.Post(url, contentType, bodyBuf) 98 | } 99 | 100 | func (c *client) uploadFile(name string) error { 101 | url := fmt.Sprintf("%s/upload?open=true", c.addr) 102 | _, err := c.postFile(name, url) 103 | if err != nil { 104 | return err 105 | } 106 | return nil 107 | } 108 | 109 | func (c *client) Open(uri string, transLocalfile bool, transLoopback bool) error { 110 | if transLocalfile && fileExists(uri) { 111 | return c.uploadFile(uri) 112 | } 113 | 114 | url := fmt.Sprintf("%s/open?uri=%s&transLoopback=%s&base64=true", c.addr, base64.URLEncoding.EncodeToString([]byte(uri)), strconv.FormatBool(transLoopback)) 115 | c.logger.Info("Opening: " + uri) 116 | 117 | _, err := http.Get(url) 118 | if err != nil { 119 | c.logger.Error("http error.", "err", err.Error()) 120 | return err 121 | } 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /lemon/cli.go: -------------------------------------------------------------------------------- 1 | package lemon 2 | 3 | import "io" 4 | 5 | type CommandType int 6 | 7 | // Commands 8 | const ( 9 | OPEN CommandType = iota + 1 10 | COPY 11 | PASTE 12 | SERVER 13 | ) 14 | 15 | const ( 16 | Success = 0 17 | FlagParseError = iota + 10 18 | RPCError 19 | Help 20 | ) 21 | 22 | type CommandStyle int 23 | 24 | const ( 25 | ALIAS CommandStyle = iota + 1 26 | SUBCOMMAND 27 | ) 28 | 29 | type CLI struct { 30 | In io.Reader 31 | Out, Err io.Writer 32 | 33 | Type CommandType 34 | DataSource string 35 | 36 | // options 37 | Port int 38 | Allow string 39 | Host string 40 | TransLoopback bool 41 | TransLocalfile bool 42 | LineEnding string 43 | LogLevel int 44 | 45 | Help bool 46 | 47 | NoFallbackMessages bool 48 | } 49 | -------------------------------------------------------------------------------- /lemon/flag.go: -------------------------------------------------------------------------------- 1 | package lemon 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "regexp" 8 | 9 | "github.com/mitchellh/go-homedir" 10 | "github.com/monochromegane/conflag" 11 | ) 12 | 13 | func (c *CLI) FlagParse(args []string, skip bool) error { 14 | style, err := c.getCommandType(args) 15 | if err != nil { 16 | return err 17 | } 18 | if style == SUBCOMMAND { 19 | args = args[:len(args)-1] 20 | } 21 | 22 | return c.parse(args, skip) 23 | } 24 | 25 | func (c *CLI) getCommandType(args []string) (s CommandStyle, err error) { 26 | s = ALIAS 27 | switch { 28 | case regexp.MustCompile(`/?xdg-open$`).MatchString(args[0]): 29 | c.Type = OPEN 30 | return 31 | case regexp.MustCompile(`/?pbpaste$`).MatchString(args[0]): 32 | c.Type = PASTE 33 | return 34 | case regexp.MustCompile(`/?pbcopy$`).MatchString(args[0]): 35 | c.Type = COPY 36 | return 37 | } 38 | 39 | del := func(i int) { 40 | copy(args[i+1:], args[i+2:]) 41 | args[len(args)-1] = "" 42 | } 43 | 44 | s = SUBCOMMAND 45 | for i, v := range args[1:] { 46 | switch v { 47 | case "open": 48 | c.Type = OPEN 49 | del(i) 50 | return 51 | case "paste": 52 | c.Type = PASTE 53 | del(i) 54 | return 55 | case "copy": 56 | c.Type = COPY 57 | del(i) 58 | return 59 | case "server": 60 | c.Type = SERVER 61 | del(i) 62 | return 63 | } 64 | } 65 | 66 | return s, fmt.Errorf("Unknown SubCommand\n\n" + Usage) 67 | } 68 | 69 | func (c *CLI) flags() *flag.FlagSet { 70 | flags := flag.NewFlagSet("lemonade", flag.ContinueOnError) 71 | flags.IntVar(&c.Port, "port", 2489, "TCP port number") 72 | flags.StringVar(&c.Allow, "allow", "0.0.0.0/0,::/0", "Allow IP range") 73 | flags.StringVar(&c.Host, "host", "localhost", "Destination host name.") 74 | flags.BoolVar(&c.Help, "help", false, "Show this message") 75 | flags.BoolVar(&c.TransLoopback, "trans-loopback", true, "Translate loopback address") 76 | flags.BoolVar(&c.TransLocalfile, "trans-localfile", true, "Translate local file") 77 | flags.StringVar(&c.LineEnding, "line-ending", "", "Convert Line Endings (CR/CRLF)") 78 | flags.BoolVar(&c.NoFallbackMessages, "no-fallback-messages", false, "Do not show fallback messages") 79 | flags.IntVar(&c.LogLevel, "log-level", 1, "Log level") 80 | return flags 81 | } 82 | 83 | func (c *CLI) parse(args []string, skip bool) error { 84 | flags := c.flags() 85 | 86 | confPath, err := homedir.Expand("~/.config/lemonade.toml") 87 | if err == nil && !skip { 88 | if confArgs, err := conflag.ArgsFrom(confPath); err == nil { 89 | flags.Parse(confArgs) 90 | } 91 | } 92 | 93 | var arg string 94 | err = flags.Parse(args[1:]) 95 | if err != nil { 96 | return err 97 | } 98 | if c.Type == PASTE || c.Type == SERVER { 99 | return nil 100 | } 101 | 102 | for 0 < flags.NArg() { 103 | arg = flags.Arg(0) 104 | err := flags.Parse(flags.Args()[1:]) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | } 110 | 111 | if c.Help { 112 | return nil 113 | } 114 | 115 | if arg != "" { 116 | c.DataSource = arg 117 | } else { 118 | b, err := ioutil.ReadAll(c.In) 119 | if err != nil { 120 | return err 121 | } 122 | c.DataSource = string(b) 123 | } 124 | 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /lemon/flag_test.go: -------------------------------------------------------------------------------- 1 | package lemon 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestCLIParse(t *testing.T) { 10 | assert := func(args []string, expected CLI) { 11 | expected.In = os.Stdin 12 | c := &CLI{In: os.Stdin} 13 | c.FlagParse(args, true) 14 | 15 | if !reflect.DeepEqual(expected, *c) { 16 | t.Errorf("Expected:\n %+v, but got\n %+v", expected, c) 17 | } 18 | } 19 | 20 | defaultPort := 2489 21 | defaultHost := "localhost" 22 | defaultAllow := "0.0.0.0/0,::/0" 23 | defaultLogLevel := 1 24 | 25 | assert([]string{"xdg-open", "http://example.com"}, CLI{ 26 | Type: OPEN, 27 | Host: defaultHost, 28 | Port: defaultPort, 29 | Allow: defaultAllow, 30 | DataSource: "http://example.com", 31 | TransLoopback: true, 32 | TransLocalfile: true, 33 | LogLevel: defaultLogLevel, 34 | }) 35 | 36 | assert([]string{"/usr/bin/xdg-open", "http://example.com"}, CLI{ 37 | Type: OPEN, 38 | Host: defaultHost, 39 | Port: defaultPort, 40 | Allow: defaultAllow, 41 | DataSource: "http://example.com", 42 | TransLoopback: true, 43 | TransLocalfile: true, 44 | LogLevel: defaultLogLevel, 45 | }) 46 | 47 | assert([]string{"xdg-open"}, CLI{ 48 | Type: OPEN, 49 | Host: defaultHost, 50 | Port: defaultPort, 51 | Allow: defaultAllow, 52 | TransLoopback: true, 53 | TransLocalfile: true, 54 | LogLevel: defaultLogLevel, 55 | }) 56 | 57 | assert([]string{"pbpaste", "--port", "1124"}, CLI{ 58 | Type: PASTE, 59 | Host: defaultHost, 60 | Port: 1124, 61 | Allow: defaultAllow, 62 | TransLoopback: true, 63 | TransLocalfile: true, 64 | LogLevel: defaultLogLevel, 65 | }) 66 | 67 | assert([]string{"/usr/bin/pbpaste", "--port", "1124"}, CLI{ 68 | Type: PASTE, 69 | Host: defaultHost, 70 | Port: 1124, 71 | Allow: defaultAllow, 72 | TransLoopback: true, 73 | TransLocalfile: true, 74 | LogLevel: defaultLogLevel, 75 | }) 76 | 77 | assert([]string{"pbcopy", "hogefuga"}, CLI{ 78 | Type: COPY, 79 | Host: defaultHost, 80 | Port: defaultPort, 81 | Allow: defaultAllow, 82 | DataSource: "hogefuga", 83 | TransLoopback: true, 84 | TransLocalfile: true, 85 | LogLevel: defaultLogLevel, 86 | }) 87 | 88 | assert([]string{"/usr/bin/pbcopy", "hogefuga"}, CLI{ 89 | Type: COPY, 90 | Host: defaultHost, 91 | Port: defaultPort, 92 | Allow: defaultAllow, 93 | DataSource: "hogefuga", 94 | TransLoopback: true, 95 | TransLocalfile: true, 96 | LogLevel: defaultLogLevel, 97 | }) 98 | 99 | assert([]string{"lemonade", "--host", "192.168.0.1", "--port", "1124", "open", "http://example.com"}, CLI{ 100 | Type: OPEN, 101 | Host: "192.168.0.1", 102 | Port: 1124, 103 | Allow: defaultAllow, 104 | DataSource: "http://example.com", 105 | TransLoopback: true, 106 | TransLocalfile: true, 107 | LogLevel: defaultLogLevel, 108 | }) 109 | 110 | assert([]string{"lemonade", "copy", "hogefuga"}, CLI{ 111 | Type: COPY, 112 | Host: defaultHost, 113 | Port: defaultPort, 114 | Allow: defaultAllow, 115 | DataSource: "hogefuga", 116 | TransLoopback: true, 117 | TransLocalfile: true, 118 | LogLevel: defaultLogLevel, 119 | }) 120 | 121 | assert([]string{"lemonade", "paste"}, CLI{ 122 | Type: PASTE, 123 | Host: defaultHost, 124 | Port: defaultPort, 125 | Allow: defaultAllow, 126 | TransLoopback: true, 127 | TransLocalfile: true, 128 | LogLevel: defaultLogLevel, 129 | }) 130 | 131 | assert([]string{"lemonade", "--allow", "192.168.0.0/24", "server", "--port", "1124"}, CLI{ 132 | Type: SERVER, 133 | Host: defaultHost, 134 | Port: 1124, 135 | Allow: "192.168.0.0/24", 136 | TransLoopback: true, 137 | TransLocalfile: true, 138 | LogLevel: defaultLogLevel, 139 | }) 140 | 141 | assert([]string{"lemonade", "open", "--trans-loopback=false"}, CLI{ 142 | Type: OPEN, 143 | Host: defaultHost, 144 | Port: defaultPort, 145 | Allow: defaultAllow, 146 | TransLoopback: false, 147 | TransLocalfile: true, 148 | LogLevel: defaultLogLevel, 149 | }) 150 | 151 | assert([]string{"lemonade", "open", "--trans-loopback=true"}, CLI{ 152 | Type: OPEN, 153 | Host: defaultHost, 154 | Port: defaultPort, 155 | Allow: defaultAllow, 156 | TransLoopback: true, 157 | TransLocalfile: true, 158 | LogLevel: defaultLogLevel, 159 | }) 160 | 161 | assert([]string{"lemonade", "open", "--trans-localfile=false"}, CLI{ 162 | Type: OPEN, 163 | Host: defaultHost, 164 | Port: defaultPort, 165 | Allow: defaultAllow, 166 | TransLoopback: true, 167 | TransLocalfile: false, 168 | LogLevel: defaultLogLevel, 169 | }) 170 | 171 | assert([]string{"lemonade", "open", "--trans-localfile=true"}, CLI{ 172 | Type: OPEN, 173 | Host: defaultHost, 174 | Port: defaultPort, 175 | Allow: defaultAllow, 176 | TransLoopback: true, 177 | TransLocalfile: true, 178 | LogLevel: defaultLogLevel, 179 | }) 180 | 181 | assert([]string{"lemonade", "copy", "--no-fallback-messages", "hogefuga"}, CLI{ 182 | Type: COPY, 183 | Host: defaultHost, 184 | Port: defaultPort, 185 | Allow: defaultAllow, 186 | DataSource: "hogefuga", 187 | TransLoopback: true, 188 | TransLocalfile: true, 189 | NoFallbackMessages: true, 190 | LogLevel: defaultLogLevel, 191 | }) 192 | 193 | assert([]string{"lemonade", "paste", "--no-fallback-messages"}, CLI{ 194 | Type: PASTE, 195 | Host: defaultHost, 196 | Port: defaultPort, 197 | Allow: defaultAllow, 198 | TransLoopback: true, 199 | TransLocalfile: true, 200 | NoFallbackMessages: true, 201 | LogLevel: defaultLogLevel, 202 | }) 203 | } 204 | -------------------------------------------------------------------------------- /lemon/main.go: -------------------------------------------------------------------------------- 1 | package lemon 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var Version string 10 | var Usage = fmt.Sprintf(`Usage: lemonade [options]... SUB_COMMAND [arg] 11 | Sub Commands: 12 | open [URL] Open URL by browser 13 | copy [text] Copy text. 14 | paste Paste text. 15 | server Start lemonade server. 16 | 17 | Options: 18 | --port=2489 TCP port number 19 | --line-ending Convert Line Ending (CR/CRLF) 20 | --allow="0.0.0.0/0,::/0" Allow IP Range [Server only] 21 | --host="localhost" Destination hostname [Client only] 22 | --no-fallback-messages Do not show fallback messages [Client only] 23 | --trans-loopback=true Translate loopback address [open subcommand only] 24 | --trans-localfile=true Translate local file path [open subcommand only] 25 | --log-level=1 Log level [4 = Critical, 0 = Debug] 26 | --help Show this message 27 | 28 | 29 | Version: 30 | %s`, Version) 31 | 32 | func ConvertLineEnding(text, option string) string { 33 | switch option { 34 | case "lf", "LF": 35 | text = strings.Replace(text, "\r\n", "\n", -1) 36 | return strings.Replace(text, "\r", "\n", -1) 37 | case "crlf", "CRLF": 38 | text = regexp.MustCompile(`\r(.)|\r$`).ReplaceAllString(text, "\r\n$1") 39 | text = regexp.MustCompile(`([^\r])\n|^\n`).ReplaceAllString(text, "$1\r\n") 40 | return text 41 | default: 42 | return text 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lemon/main_test.go: -------------------------------------------------------------------------------- 1 | package lemon 2 | 3 | import "testing" 4 | 5 | func TestConvertLineEnding(t *testing.T) { 6 | assert := func(text, option, expected string) { 7 | if got := ConvertLineEnding(text, option); got != expected { 8 | t.Errorf("Expected: %+v, got %+v", []byte(expected), []byte(got)) 9 | } 10 | } 11 | 12 | assert("aaa\r\nbbb", "lf", "aaa\nbbb") 13 | assert("aaa\rbbb", "lf", "aaa\nbbb") 14 | assert("aaa\nbbb", "lf", "aaa\nbbb") 15 | 16 | assert("aaa\r\nbbb", "crlf", "aaa\r\nbbb") 17 | assert("aaa\rbbb", "crlf", "aaa\r\nbbb") 18 | assert("aaa\nbbb", "crlf", "aaa\r\nbbb") 19 | 20 | assert("a\r", "crlf", "a\r\n") 21 | assert("\na", "crlf", "\r\na") 22 | } 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | log "github.com/inconshreveable/log15" 8 | 9 | "github.com/lemonade-command/lemonade/client" 10 | "github.com/lemonade-command/lemonade/lemon" 11 | "github.com/lemonade-command/lemonade/server" 12 | ) 13 | 14 | var logLevelMap = map[int]log.Lvl{ 15 | 0: log.LvlDebug, 16 | 1: log.LvlInfo, 17 | 2: log.LvlWarn, 18 | 3: log.LvlError, 19 | 4: log.LvlCrit, 20 | } 21 | 22 | func main() { 23 | 24 | cli := &lemon.CLI{ 25 | In: os.Stdin, 26 | Out: os.Stdout, 27 | Err: os.Stderr, 28 | } 29 | os.Exit(Do(cli, os.Args)) 30 | } 31 | 32 | func Do(c *lemon.CLI, args []string) int { 33 | logger := log.New() 34 | logger.SetHandler(log.LvlFilterHandler(log.LvlError, log.StdoutHandler)) 35 | 36 | if err := c.FlagParse(args, false); err != nil { 37 | writeError(c, err) 38 | return lemon.FlagParseError 39 | } 40 | 41 | logLevel := logLevelMap[c.LogLevel] 42 | logger.SetHandler(log.LvlFilterHandler(logLevel, log.StdoutHandler)) 43 | 44 | if c.Help { 45 | fmt.Fprint(c.Err, lemon.Usage) 46 | return lemon.Help 47 | } 48 | 49 | lc := client.New(c, logger) 50 | var err error 51 | 52 | switch c.Type { 53 | case lemon.OPEN: 54 | logger.Debug("Opening URL") 55 | err = lc.Open(c.DataSource, c.TransLocalfile, c.TransLoopback) 56 | case lemon.COPY: 57 | logger.Debug("Copying text") 58 | err = lc.Copy(c.DataSource) 59 | case lemon.PASTE: 60 | logger.Debug("Pasting text") 61 | var text string 62 | text, err = lc.Paste() 63 | c.Out.Write([]byte(text)) 64 | case lemon.SERVER: 65 | logger.Debug("Starting Server") 66 | err = server.Serve(c, logger) 67 | default: 68 | panic("Unreachable code") 69 | } 70 | 71 | if err != nil { 72 | writeError(c, err) 73 | return lemon.RPCError 74 | } 75 | return lemon.Success 76 | } 77 | 78 | func writeError(c *lemon.CLI, err error) { 79 | fmt.Fprintln(c.Err, err.Error()) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxi/lemonade/fe39bb0a25b31dbaae17ac278bda7498d465e305/pkg/.gitkeep -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | 13 | "github.com/atotto/clipboard" 14 | log "github.com/inconshreveable/log15" 15 | "github.com/lemonade-command/lemonade/lemon" 16 | "github.com/pocke/go-iprange" 17 | "github.com/skratchdot/open-golang/open" 18 | ) 19 | 20 | var logger log.Logger 21 | var lineEnding string 22 | var ra *iprange.Range 23 | var port int 24 | var path = "./files" 25 | 26 | func handleCopy(w http.ResponseWriter, r *http.Request) { 27 | if r.Method != http.MethodPost { 28 | http.Error(w, "Copy only support post", 404) 29 | return 30 | } 31 | 32 | // Read body 33 | b, err := ioutil.ReadAll(r.Body) 34 | defer r.Body.Close() 35 | if err != nil { 36 | http.Error(w, err.Error(), 500) 37 | return 38 | } 39 | text := lemon.ConvertLineEnding(string(b), lineEnding) 40 | logger.Debug("Copy:", "text", text) 41 | clipboard.WriteAll(text) 42 | } 43 | 44 | func handlePaste(w http.ResponseWriter, r *http.Request) { 45 | if r.Method != http.MethodGet { 46 | http.Error(w, "Paste only support get", 404) 47 | return 48 | } 49 | 50 | t, err := clipboard.ReadAll() 51 | if err == nil { 52 | io.WriteString(w, t) 53 | } 54 | logger.Debug("Paste: ", "text", t) 55 | } 56 | 57 | func translateLoopbackIP(uri string, remoteIP string) string { 58 | parsed, err := url.Parse(uri) 59 | if err != nil { 60 | return uri 61 | } 62 | 63 | host, port, err := net.SplitHostPort(parsed.Host) 64 | 65 | ip := net.ParseIP(host) 66 | if ip == nil || !ip.IsLoopback() { 67 | return uri 68 | } 69 | 70 | if len(port) == 0 { 71 | parsed.Host = remoteIP 72 | } else { 73 | parsed.Host = fmt.Sprintf("%s:%s", remoteIP, port) 74 | } 75 | 76 | return parsed.String() 77 | } 78 | 79 | func handleOpen(w http.ResponseWriter, r *http.Request) { 80 | if r.Method != http.MethodGet { 81 | http.Error(w, "Open only support get", 404) 82 | return 83 | } 84 | 85 | q := r.URL.Query() 86 | uri := q.Get("uri") 87 | isBase64 := q.Get("base64") 88 | if isBase64 == "true" { 89 | decodeURI, err := base64.URLEncoding.DecodeString(uri) 90 | if err != nil { 91 | logger.Error("base64 decode error", "uri", uri) 92 | return 93 | } 94 | uri = string(decodeURI) 95 | } 96 | 97 | transLoopback := q.Get("transLoopback") 98 | if transLoopback == "true" { 99 | remoteIP, _, _ := net.SplitHostPort(r.RemoteAddr) 100 | uri = translateLoopbackIP(uri, remoteIP) 101 | } 102 | 103 | logger.Info("Open: ", "uri", uri) 104 | open.Run(uri) 105 | } 106 | 107 | func handleUpload(w http.ResponseWriter, r *http.Request) { 108 | if r.Method != http.MethodPost { 109 | http.Error(w, "Upload only support post", 404) 110 | return 111 | } 112 | 113 | r.ParseMultipartForm(10 << 20) 114 | file, handler, err := r.FormFile("uploadFile") 115 | if err != nil { 116 | http.Error(w, "Error Retrieving the File", 500) 117 | logger.Error("Error Retrieving the File", "err", err) 118 | return 119 | } 120 | defer file.Close() 121 | 122 | fileBytes, err := ioutil.ReadAll(file) 123 | if err != nil { 124 | http.Error(w, "Error Read the File", 500) 125 | logger.Error("Error Read the File", "err", err) 126 | return 127 | } 128 | 129 | ioutil.WriteFile(path+"/"+handler.Filename, fileBytes, os.ModePerm) 130 | 131 | q := r.URL.Query() 132 | isOpen := q.Get("open") 133 | if isOpen == "true" { 134 | uri := fmt.Sprintf("http://127.0.0.1:%d/files/%s", port, handler.Filename) 135 | logger.Info("Open: ", "uri", uri) 136 | open.Run(uri) 137 | } 138 | } 139 | 140 | func middleware(next http.Handler) http.Handler { 141 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 142 | if r.Method != http.MethodGet && r.Method != http.MethodPost { 143 | http.Error(w, "Not support method.", 404) 144 | return 145 | } 146 | 147 | remoteIP, _, err := net.SplitHostPort(r.RemoteAddr) 148 | if err != nil { 149 | http.Error(w, "RemoteAddr error.", 500) 150 | return 151 | } 152 | if !ra.IncludeStr(remoteIP) { 153 | http.Error(w, "Not allow ip.", 503) 154 | logger.Info("not in allow ip. from: ", remoteIP) 155 | return 156 | } 157 | next.ServeHTTP(w, r) 158 | }) 159 | } 160 | 161 | func Serve(c *lemon.CLI, _logger log.Logger) error { 162 | logger = _logger 163 | lineEnding = c.LineEnding 164 | port = c.Port 165 | 166 | var err error 167 | ra, err = iprange.New(c.Allow) 168 | if err != nil { 169 | logger.Error("allowIp error") 170 | return err 171 | } 172 | 173 | os.MkdirAll(path, os.ModePerm) 174 | http.Handle("/files/", http.StripPrefix("/files/", http.FileServer(http.Dir(path)))) 175 | http.Handle("/copy", middleware(http.HandlerFunc(handleCopy))) 176 | http.Handle("/paste", middleware(http.HandlerFunc(handlePaste))) 177 | http.Handle("/open", middleware(http.HandlerFunc(handleOpen))) 178 | http.Handle("/upload", middleware(http.HandlerFunc(handleUpload))) 179 | err = http.ListenAndServe(fmt.Sprintf(":%d", c.Port), nil) 180 | if err != nil { 181 | return err 182 | } 183 | return nil 184 | } 185 | -------------------------------------------------------------------------------- /sideci.yml: -------------------------------------------------------------------------------- 1 | linter: 2 | gometalinter: 3 | options: 4 | disable: 5 | - golint 6 | --------------------------------------------------------------------------------