Author | SHA1 | Message | Date |
---|---|---|---|
Maciek Borzecki | 37e45ea847 |
go mod support
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
5 years ago |
Maciek Borzecki | d76c6e3397 |
cmd/mconnect-connector: trivial packet receive/send loop
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
5 years ago |
Maciek Borzecki | 34f26d176a |
protocol: update connection to TLS after sending identity
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
5 years ago |
Maciek Borzecki | f2f4245ca8 |
protocol: send/receive helpers
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
5 years ago |
Maciek Borzecki | 2cb0eb8639 |
protocol: certificate generator
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
5 years ago |
Maciek Borzecki | c40ed84737 |
protocol: starting with listener
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
5 years ago |
Maciek Borzecki | bd1497d632 |
protocol/packet: pair packet wrappers
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
5 years ago |
Maciek Borzecki | 1c0400f044 |
cmd/mconnect-connector: helper tool for establishing connections
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | aedd1fa243 |
protocol: connection helper
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | b675280994 |
cmd/mconnect-discover: display tcp port
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | b447fdfb7d |
protocol/packet: add packet encoder and decoder
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | c5923774f4 |
protocol/packet: helper for creating identity packets
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | ad36298f60 |
cmd/mconnect-discover: add flags
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | c9b15623b9 |
utils/flags: some helpers for github.com/jessevdk/go-flags
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | b482b5d2a9 |
cmd/mconnect-discover: track discovered devices, nicer output
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | 322fb34aea |
logger: add level support
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | 2486560099 |
discovery: use protocol port wrappers
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | c6f33b9e4f |
protocol: add UDP and TCP ports
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | 3ca4c43a44 |
cmd/mconnect-discover: self announce
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | e50a5745e1 |
discovery: use new UDP port
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | 48e691d0b7 |
discovery: self announce
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | a6670d1fcc |
protocol/packet: json field mappings for identity packet
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | ba44186903 |
protocol/packet: encoder
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | 19f3aea2dd |
protocol/packet: json field names, settable body
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | a38a88406d |
protocol/packet: renme decoder test
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | f8a3638203 |
protocol/packet: renames
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | cc8e4a80b6 |
discovery: update listener to account for change unmarshaller
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | 831273b043 |
protocol/packet: refactor unmarshaller
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | e222ee0509 |
dirs: update path to user data dir
Signed-off-by: Maciek Borzecki <maciek.borzecki@gmail.com> |
7 years ago |
Maciek Borzecki | 26580713f0 | cmd/mconnect-discover: adjust to changes in discovery | 7 years ago |
Maciek Borzecki | a5c696c2da | discovery: rework discovery handling | 7 years ago |
Maciek Borzecki | 64c7ebfac6 | discovery: more verbose logging | 7 years ago |
Maciek Borzecki | e8b7b03e66 | protocol/packet: packet wrappers | 7 years ago |
Maciek Borzecki | 8a41e74076 | dirs: helpers for finding user directories | 7 years ago |
Maciek Borzecki | cd497dae9c | global: add license header | 7 years ago |
Maciek Borzecki | 7d9eac9c0b | cmd/mconnect-discover: simple tool to perform discovery | 7 years ago |
Maciek Borzecki | bb1b9870d8 | discovery: device discovery | 7 years ago |
Maciek Borzecki | 706ac9a0ab | logger: simple logger | 7 years ago |
@ -0,0 +1,119 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package main | |||
import ( | |||
"context" | |||
"os" | |||
"os/user" | |||
"github.com/jessevdk/go-flags" | |||
"github.com/bboozzoo/mconnect/logger" | |||
"github.com/bboozzoo/mconnect/protocol" | |||
"github.com/bboozzoo/mconnect/protocol/packet" | |||
uflags "github.com/bboozzoo/mconnect/utils/flags" | |||
) | |||
var ( | |||
Stderr = os.Stderr | |||
Stdout = os.Stdout | |||
) | |||
func main() { | |||
var opts struct { | |||
Debug bool `short:"d" long:"debug" description:"Show debugging information"` | |||
Address string `short:"a" long:"address" description:"Address of remote device"` | |||
} | |||
_, err := flags.ParseArgs(&opts, os.Args) | |||
if err != nil { | |||
uflags.HandleFlagsError(err) | |||
} | |||
ctx := context.Background() | |||
ctx = logger.WithContext(ctx, logger.New()) | |||
log := logger.FromContext(ctx) | |||
log.SetLevel(logger.ErrorLevel) | |||
if opts.Debug { | |||
log.SetLevel(logger.DebugLevel) | |||
} | |||
hostname, err := os.Hostname() | |||
if err != nil { | |||
log.Errorf("cannot obtain hostname: %v", err) | |||
os.Exit(1) | |||
} | |||
u, err := user.Current() | |||
if err != nil { | |||
log.Errorf("cannot obtain current user: %v", err) | |||
os.Exit(1) | |||
} | |||
entity := u.Name + "@" + hostname | |||
deviceCert, err := protocol.GenerateDeviceCertificate(entity) | |||
if err != nil { | |||
log.Errorf("cannot generate device certificate for entity %q: %v", | |||
entity, err) | |||
os.Exit(1) | |||
} | |||
conf := protocol.Configuration{ | |||
Identity: &packet.Identity{ | |||
DeviceId: "mconnect-" + hostname, | |||
DeviceName: hostname, | |||
DeviceType: "computer", | |||
ProtocolVersion: 7, | |||
TcpPort: 1716, | |||
}, | |||
Cert: deviceCert.TLSCertificate(), | |||
} | |||
conn, err := protocol.Dial(ctx, opts.Address, &conf) | |||
if err != nil { | |||
log.Errorf("connection failed: %v", err) | |||
os.Exit(1) | |||
} | |||
defer conn.Close() | |||
for { | |||
var response *packet.Packet | |||
p, err := conn.Receive() | |||
if err != nil { | |||
log.Errorf("failed to receive packet: %v", err) | |||
os.Exit(1) | |||
} | |||
log.Infof("got packet: %+v", p) | |||
log.Infof("packet type: %q", p.Type) | |||
switch p.Type { | |||
case "kdeconnect.pair": | |||
log.Infof("pair request") | |||
pair, err := p.AsPair() | |||
if err != nil { | |||
log.Errorf("cannot decode pair packet: %v", err) | |||
continue | |||
} | |||
if pair.Pair { | |||
response = packet.NewPair() | |||
} | |||
} | |||
if response != nil { | |||
log.Debugf("sending response: %v", response) | |||
if err := conn.Send(*response); err != nil { | |||
log.Errorf("cannot send a response packet: %v", err) | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,103 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package main | |||
import ( | |||
"context" | |||
"fmt" | |||
"os" | |||
"time" | |||
"github.com/jessevdk/go-flags" | |||
"github.com/bboozzoo/mconnect/discovery" | |||
"github.com/bboozzoo/mconnect/logger" | |||
"github.com/bboozzoo/mconnect/protocol/packet" | |||
uflags "github.com/bboozzoo/mconnect/utils/flags" | |||
) | |||
var ( | |||
Stderr = os.Stderr | |||
Stdout = os.Stdout | |||
) | |||
func main() { | |||
var opts struct { | |||
Debug bool `short:"d" long:"debug" description:"Show debugging information"` | |||
} | |||
_, err := flags.ParseArgs(&opts, os.Args) | |||
if err != nil { | |||
uflags.HandleFlagsError(err) | |||
} | |||
ctx := context.Background() | |||
ctx = logger.WithContext(ctx, logger.New()) | |||
log := logger.FromContext(ctx) | |||
log.SetLevel(logger.ErrorLevel) | |||
if opts.Debug { | |||
log.SetLevel(logger.DebugLevel) | |||
} | |||
log.Infof("setting up listener") | |||
l, err := discovery.NewListener() | |||
if err != nil { | |||
fmt.Fprintf(Stderr, "error: failed to setup listener: %v\n", | |||
err) | |||
os.Exit(1) | |||
} | |||
hostname, err := os.Hostname() | |||
if err != nil { | |||
fmt.Fprintf(Stderr, "error: failed to obtain hostname: %v\n", | |||
err) | |||
os.Exit(1) | |||
} | |||
go func() { | |||
for { | |||
err := discovery.Announce(ctx, packet.Identity{ | |||
DeviceId: "mconnect-" + hostname, | |||
DeviceName: hostname, | |||
DeviceType: "computer", | |||
ProtocolVersion: 7, | |||
TcpPort: 1716, | |||
}) | |||
if err != nil { | |||
log.Errorf("failed to self announce: %v", err) | |||
} | |||
time.Sleep(5 * time.Second) | |||
} | |||
}() | |||
devices := map[string]*discovery.Discovery{} | |||
for { | |||
log.Info("receive wait") | |||
d, err := l.Receive(ctx) | |||
if err != nil { | |||
log.Warning("failed to receive identity packet: %v", err) | |||
continue | |||
} | |||
log.Infof("discovered a device at %s packet: %v", | |||
d.From, d.Identity) | |||
if _, ok := devices[d.Identity.DeviceId]; !ok { | |||
devices[d.Identity.DeviceId] = d | |||
fmt.Fprintf(Stdout, " * %q (ID: %v) %v:%v\n", | |||
d.Identity.DeviceName, | |||
d.Identity.DeviceId, | |||
d.From.IP, d.Identity.TcpPort) | |||
} | |||
} | |||
} |
@ -0,0 +1,41 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package dirs | |||
import ( | |||
"os/user" | |||
"path" | |||
) | |||
var userHome string | |||
func UserHome() string { | |||
if userHome == "" { | |||
user, _ := user.Current() | |||
if user != nil { | |||
userHome = user.HomeDir | |||
} | |||
} | |||
return userHome | |||
} | |||
func UserCache() string { | |||
return path.Join(UserHome(), ".cache", "mconnect") | |||
} | |||
func UserConfig() string { | |||
return path.Join(UserHome(), ".config", "mconnect") | |||
} | |||
func UserData() string { | |||
return path.Join(UserHome(), ".local", "share", "mconnect") | |||
} |
@ -0,0 +1,29 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package dirs_test | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/bboozzoo/mconnect/dirs" | |||
) | |||
func TestUserDirs(t *testing.T) { | |||
restore := dirs.MockUserHome("/home/foo") | |||
defer restore() | |||
assert.Equal(t, dirs.UserCache(), "/home/foo/.cache/mconnect") | |||
assert.Equal(t, dirs.UserConfig(), "/home/foo/.config/mconnect") | |||
assert.Equal(t, dirs.UserData(), "/home/foo/.local/share/mconnect") | |||
} |
@ -0,0 +1,20 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package dirs | |||
func MockUserHome(home string) (restore func()) { | |||
old := userHome | |||
userHome = home | |||
return func() { | |||
userHome = old | |||
} | |||
} |
@ -0,0 +1,42 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package discovery | |||
import ( | |||
"context" | |||
"net" | |||
"github.com/pkg/errors" | |||
"github.com/bboozzoo/mconnect/protocol" | |||
"github.com/bboozzoo/mconnect/protocol/packet" | |||
) | |||
func Announce(ctx context.Context, identity packet.Identity) error { | |||
p := packet.New("kdeconnect.identity", identity) | |||
data, err := packet.Marshal(p) | |||
if err != nil { | |||
return errors.Wrapf(err, "failed to marshal packet") | |||
} | |||
c, err := net.DialUDP("udp", nil, protocol.UDPDiscoveryAddr) | |||
if err != nil { | |||
return errors.Wrapf(err, "failed to open UDP socket") | |||
} | |||
defer c.Close() | |||
_, err = c.Write(data) | |||
if err != nil { | |||
return errors.Wrapf(err, "failed to send identity packet") | |||
} | |||
return nil | |||
} |
@ -0,0 +1,78 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package discovery | |||
import ( | |||
"context" | |||
"net" | |||
"github.com/pkg/errors" | |||
"github.com/bboozzoo/mconnect/logger" | |||
"github.com/bboozzoo/mconnect/protocol" | |||
"github.com/bboozzoo/mconnect/protocol/packet" | |||
) | |||
type Listener struct { | |||
conn *net.UDPConn | |||
} | |||
func NewListener() (*Listener, error) { | |||
conn, err := net.ListenUDP("udp", protocol.UDPDiscoveryAddr) | |||
if err != nil { | |||
return nil, err | |||
} | |||
listener := &Listener{ | |||
conn: conn, | |||
} | |||
return listener, nil | |||
} | |||
// Discovery conveys the received discovery information | |||
type Discovery struct { | |||
// Packet is the original packet received | |||
Packet *packet.Packet | |||
// Identity is the parsed identity data | |||
Identity *packet.Identity | |||
// From is the address the packet was received from | |||
From *net.UDPAddr | |||
} | |||
// Receive blocks waiting to receive a discovery packet. Once received, it will | |||
// parse the packet and return a result. | |||
func (l *Listener) Receive(ctx context.Context) (*Discovery, error) { | |||
log := logger.FromContext(ctx) | |||
buf := make([]byte, 4096) | |||
count, addr, err := l.conn.ReadFromUDP(buf) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "failed to receive a packet") | |||
} | |||
log.Debugf("got %v bytes from %v", count, addr) | |||
log.Debugf("data:\n%s", string(buf)) | |||
var p packet.Packet | |||
if err := packet.Unmarshal(buf, &p); err != nil { | |||
return nil, errors.Wrap(err, "failed to parse packet") | |||
} | |||
identity, err := p.AsIdentity() | |||
if err != nil { | |||
return nil, errors.Wrap(err, "failed to parse as identity packet") | |||
} | |||
discovery := &Discovery{ | |||
Packet: &p, | |||
Identity: identity, | |||
From: addr, | |||
} | |||
return discovery, nil | |||
} |
@ -0,0 +1,17 @@ | |||
module github.com/bboozzoo/mconnect | |||
go 1.13 | |||
require ( | |||
github.com/Sirupsen/logrus v1.0.4 | |||
github.com/godbus/dbus v4.1.0+incompatible | |||
github.com/jessevdk/go-flags v1.3.0 | |||
github.com/onsi/ginkgo v1.11.0 // indirect | |||
github.com/onsi/gomega v1.8.1 // indirect | |||
github.com/pkg/errors v0.8.0 | |||
github.com/sirupsen/logrus v1.4.2 // indirect | |||
github.com/stretchr/testify v1.2.2 | |||
golang.org/x/crypto v0.0.0-20180119165957-a66000089151 // indirect | |||
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect | |||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect | |||
) |
@ -0,0 +1,33 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package logger | |||
import ( | |||
"context" | |||
) | |||
type loggerKeyType int | |||
const ( | |||
loggerContextKey loggerKeyType = 1 | |||
) | |||
func FromContext(ctx context.Context) Logger { | |||
if logger, _ := ctx.Value(loggerContextKey).(Logger); logger != nil { | |||
return logger | |||
} | |||
return New() | |||
} | |||
func WithContext(ctx context.Context, logger Logger) context.Context { | |||
return context.WithValue(ctx, loggerContextKey, logger) | |||
} |
@ -0,0 +1,51 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package logger | |||
type Logger interface { | |||
Debug(args ...interface{}) | |||
Debugf(format string, args ...interface{}) | |||
Debugln(args ...interface{}) | |||
Info(args ...interface{}) | |||
Infof(format string, args ...interface{}) | |||
Infoln(args ...interface{}) | |||
Error(args ...interface{}) | |||
Errorf(format string, args ...interface{}) | |||
Errorln(args ...interface{}) | |||
Warning(args ...interface{}) | |||
Warningf(format string, args ...interface{}) | |||
Warningln(args ...interface{}) | |||
Panic(args ...interface{}) | |||
Panicf(format string, args ...interface{}) | |||
Panicln(args ...interface{}) | |||
Print(args ...interface{}) | |||
Printf(format string, args ...interface{}) | |||
Println(args ...interface{}) | |||
SetLevel(level Level) | |||
} | |||
type Level int | |||
const ( | |||
PanicLevel Level = iota | |||
FatalLevel | |||
ErrorLevel | |||
WarnLevel | |||
InfoLevel | |||
DebugLevel | |||
) |
@ -0,0 +1,45 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package logger | |||
import ( | |||
"os" | |||
"github.com/Sirupsen/logrus" | |||
) | |||
type logger struct { | |||
logrus.Logger | |||
} | |||
var log *logger | |||
func init() { | |||
log = &logger{ | |||
Logger: logrus.Logger{ | |||
Out: os.Stderr, | |||
Formatter: &logrus.TextFormatter{ | |||
FullTimestamp: true, | |||
}, | |||
Hooks: make(logrus.LevelHooks), | |||
Level: logrus.DebugLevel, | |||
}, | |||
} | |||
} | |||
func New() Logger { | |||
return log | |||
} | |||
func (l *logger) SetLevel(level Level) { | |||
l.Logger.SetLevel(logrus.Level(level)) | |||
} |
@ -0,0 +1,77 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package protocol | |||
import ( | |||
"crypto/ecdsa" | |||
"crypto/elliptic" | |||
"crypto/rand" | |||
"crypto/tls" | |||
"crypto/x509" | |||
"crypto/x509/pkix" | |||
"math/big" | |||
"time" | |||
) | |||
type DeviceCertificate struct { | |||
key *ecdsa.PrivateKey | |||
cert []byte | |||
} | |||
func (d *DeviceCertificate) TLSCertificate() *tls.Certificate { | |||
return &tls.Certificate{ | |||
PrivateKey: d.key, | |||
Certificate: [][]byte{d.cert}, | |||
} | |||
} | |||
// GenerateDeviceCertificate returns a device certificate | |||
func GenerateDeviceCertificate(entity string) (*DeviceCertificate, error) { | |||
limit := big.Int{} | |||
limit.Lsh(big.NewInt(1), 128) | |||
serial, err := rand.Int(rand.Reader, &limit) | |||
if err != nil { | |||
return nil, err | |||
} | |||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) | |||
if err != nil { | |||
return nil, err | |||
} | |||
startTime := time.Now() | |||
// 10 years from now | |||
expireTime := startTime.AddDate(10, 0, 0) | |||
template := x509.Certificate{ | |||
SerialNumber: serial, | |||
Subject: pkix.Name{ | |||
CommonName: entity, | |||
Organization: []string{"mconnect"}, | |||
OrganizationalUnit: []string{"mconnect"}, | |||
}, | |||
NotBefore: startTime, | |||
NotAfter: expireTime, | |||
BasicConstraintsValid: true, | |||
} | |||
selfSign := template | |||
cert, err := x509.CreateCertificate(rand.Reader, &template, &selfSign, | |||
&priv.PublicKey, priv) | |||
if err != nil { | |||
return nil, err | |||
} | |||
devcert := &DeviceCertificate{ | |||
key: priv, | |||
cert: cert, | |||
} | |||
return devcert, nil | |||
} |
@ -0,0 +1,85 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package protocol | |||
import ( | |||
"context" | |||
"crypto/tls" | |||
"net" | |||
"github.com/pkg/errors" | |||
"github.com/bboozzoo/mconnect/logger" | |||
"github.com/bboozzoo/mconnect/protocol/packet" | |||
) | |||
type Connection struct { | |||
conn *tls.Conn | |||
} | |||
type Configuration struct { | |||
Cert *tls.Certificate | |||
Identity *packet.Identity | |||
} | |||
func Dial(ctx context.Context, where string, conf *Configuration) (*Connection, error) { | |||
log := logger.FromContext(ctx) | |||
dialer := net.Dialer{} | |||
conn, err := dialer.DialContext(ctx, "tcp", where) | |||
if err != nil { | |||
return nil, errors.Wrapf(err, "failed to dial %s", where) | |||
} | |||
log.Debugf("connected to %v", conn.RemoteAddr()) | |||
e := packet.NewEncoder(conn) | |||
if err := e.Encode(packet.NewIdentity(conf.Identity)); err != nil { | |||
return nil, errors.Wrapf(err, "failed to send identity") | |||
} | |||
log.Debugf("identity sent") | |||
// upgrade to secure connection | |||
tlsConf := tls.Config{ | |||
InsecureSkipVerify: true, | |||
Certificates: []tls.Certificate{*conf.Cert}, | |||
} | |||
tlsConn := tls.Server(conn, &tlsConf) | |||
if err := tlsConn.Handshake(); err != nil { | |||
log.Errorf("TLS handshake failed: %v", err) | |||
return nil, err | |||
} | |||
return &Connection{conn: tlsConn}, nil | |||
} | |||
func (c *Connection) Close() error { | |||
if c.conn != nil { | |||
c.conn.Close() | |||
} | |||
return nil | |||
} | |||
func (c *Connection) Receive() (*packet.Packet, error) { | |||
d := packet.NewDecoder(c.conn) | |||
var p packet.Packet | |||
if err := d.Decode(&p); err != nil { | |||
return nil, err | |||
} | |||
return &p, nil | |||
} | |||
func (c *Connection) Send(p packet.Packet) error { | |||
e := packet.NewEncoder(c.conn) | |||
return e.Encode(&p) | |||
} |
@ -0,0 +1,20 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package protocol | |||
import ( | |||
"net" | |||
) | |||
func Listener(addr string) (net.Listener, error) { | |||
return net.Listen("tcp", addr) | |||
} |
@ -0,0 +1,52 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package packet | |||
import ( | |||
"bytes" | |||
"encoding/json" | |||
"fmt" | |||
"io" | |||
"github.com/pkg/errors" | |||
) | |||
func Unmarshal(data []byte, p *Packet) error { | |||
return NewDecoder(bytes.NewBuffer(data)).Decode(p) | |||
} | |||
type Decoder struct { | |||
r io.Reader | |||
j *json.Decoder | |||
} | |||
func NewDecoder(r io.Reader) *Decoder { | |||
return &Decoder{ | |||
r: r, | |||
j: json.NewDecoder(r), | |||
} | |||
} | |||
func (d *Decoder) Decode(p *Packet) error { | |||
if p == nil { | |||
return fmt.Errorf("no packet") | |||
} | |||
if err := d.j.Decode(p); err != nil { | |||
return errors.Wrap(err, "failed to decode body") | |||
} | |||
if p.Id == uint64(0) || p.Type == "" || p.Body == nil { | |||
return fmt.Errorf("packet incomplete, missing id, type or body") | |||
} | |||
return nil | |||
} |
@ -0,0 +1,71 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package packet_test | |||
import ( | |||
"strings" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/bboozzoo/mconnect/protocol/packet" | |||
) | |||
func TestUnmarshal(t *testing.T) { | |||
var p packet.Packet | |||
err := packet.Unmarshal([]byte(`foobar`), &p) | |||
assert.Error(t, err) | |||
p = packet.Packet{} | |||
err = packet.Unmarshal([]byte(`{}`), &p) | |||
assert.Error(t, err) | |||
p = packet.Packet{} | |||
err = packet.Unmarshal([]byte(`{"id": 123, "type": "foo","body":{}}`), &p) | |||
assert.NoError(t, err) | |||
assert.Equal(t, p, packet.Packet{ | |||
Id: uint64(123), | |||
Type: "foo", | |||
Body: []byte("{}"), | |||
}) | |||
} | |||
func TestDecoder(t *testing.T) { | |||
input := ` | |||
{"id": 123, "type": "foo","body":{}} | |||
{"id": 456, "type": "bar","body":{"123": 123}} | |||
{"id": 678, "type": "baz" | |||
` | |||
d := packet.NewDecoder(strings.NewReader(input)) | |||
p := packet.Packet{} | |||
err := d.Decode(&p) | |||
assert.NoError(t, err) | |||
assert.Equal(t, p, packet.Packet{ | |||
Id: uint64(123), | |||
Type: "foo", | |||
Body: []byte("{}"), | |||
}) | |||
p = packet.Packet{} | |||
err = d.Decode(&p) | |||
assert.NoError(t, err) | |||
assert.Equal(t, p, packet.Packet{ | |||
Id: uint64(456), | |||
Type: "bar", | |||
Body: []byte(`{"123": 123}`), | |||
}) | |||
p = packet.Packet{} | |||
err = d.Decode(&p) | |||
assert.Error(t, err) | |||
} |
@ -0,0 +1,16 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package packet | |||
func NewDiscovery() { | |||
} |
@ -0,0 +1,81 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package packet | |||
import ( | |||
"bytes" | |||
"encoding/json" | |||
"io" | |||
"time" | |||
"github.com/pkg/errors" | |||
) | |||
var getId = func() uint64 { | |||
return uint64(time.Now().UnixNano() / 1000) | |||
} | |||
func Marshal(p *Packet) ([]byte, error) { | |||
b := &bytes.Buffer{} | |||
enc := NewEncoder(b) | |||
if err := enc.Encode(p); err != nil { | |||
return nil, err | |||
} | |||
return b.Bytes(), nil | |||
} | |||
type Encoder struct { | |||
w io.Writer | |||
j *json.Encoder | |||
} | |||
func NewEncoder(w io.Writer) *Encoder { | |||
return &Encoder{ | |||
w: w, | |||
j: json.NewEncoder(w), | |||
} | |||
} | |||
type auxPacket struct { | |||
Packet | |||
Body interface{} `json:"body"` | |||
} | |||
func (e *Encoder) Encode(p *Packet) error { | |||
if p == nil { | |||
return errors.New("no packet") | |||
} | |||
if p.Type == "" { | |||
return errors.New("packet type not set") | |||
} | |||
id := p.Id | |||
if id == 0 { | |||
id = getId() | |||
} | |||
body := p.auxBody | |||
// encodes packet and appends a newline character | |||
err := e.j.Encode(auxPacket{ | |||
Packet: Packet{ | |||
Id: id, | |||
Type: p.Type, | |||
}, | |||
Body: body, | |||
}) | |||
if err != nil { | |||
return errors.Wrap(err, "failed to encode body") | |||
} | |||
return nil | |||
} |
@ -0,0 +1,52 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package packet_test | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/bboozzoo/mconnect/protocol/packet" | |||
) | |||
func TestMarshal(t *testing.T) { | |||
data, err := packet.Marshal(nil) | |||
assert.Error(t, err) | |||
data, err = packet.Marshal(&packet.Packet{}) | |||
assert.Error(t, err) | |||
exp := `{"id":123,"type":"foo","body":null}` + "\n" | |||
p := packet.New("foo", nil) | |||
p.Id = 123 | |||
data, err = packet.Marshal(p) | |||
assert.NoError(t, err) | |||
assert.Equal(t, []byte(exp), data) | |||
exp = `{"id":123,"type":"foo","body":{}}` + "\n" | |||
p = packet.New("foo", map[int]int{}) | |||
p.Id = 123 | |||
data, err = packet.Marshal(p) | |||
assert.NoError(t, err) | |||
assert.Equal(t, []byte(exp), data) | |||
restore := packet.MockGetId(func() uint64 { | |||
return uint64(889911) | |||
}) | |||
defer restore() | |||
exp = `{"id":889911,"type":"foo","body":{}}` + "\n" | |||
data, err = packet.Marshal(packet.New("foo", map[int]int{})) | |||
assert.NoError(t, err) | |||
assert.Equal(t, []byte(exp), data) | |||
} |
@ -0,0 +1,20 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package packet | |||
func MockGetId(newGetId func() uint64) (restore func()) { | |||
old := getId | |||
getId = newGetId | |||
return func() { | |||
getId = old | |||
} | |||
} |
@ -0,0 +1,42 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package packet | |||
import ( | |||
"encoding/json" | |||
"fmt" | |||
) | |||
type Identity struct { | |||
DeviceId string `json:"deviceId"` | |||
DeviceName string `json:"deviceName"` | |||
DeviceType string `json:"deviceType"` | |||
ProtocolVersion uint `json:"protocolVersion"` | |||
IncomingCapabilities []string `json:"incomingCapabilities"` | |||
OutgoingCapabilities []string `json:"outgoingCapabilities"` | |||
TcpPort uint `json:"tcpPort"` | |||
} | |||
func (p *Packet) AsIdentity() (*Identity, error) { | |||
if p.Type != "kdeconnect.identity" { | |||
return nil, fmt.Errorf("not an identity packet, unexpected type %q", p.Type) | |||
} | |||
var identity Identity | |||
if err := json.Unmarshal(p.Body, &identity); err != nil { | |||
return nil, err | |||
} | |||
return &identity, nil | |||
} | |||
func NewIdentity(identity *Identity) *Packet { | |||
return New("kdeconnect.identity", identity) | |||
} |
@ -0,0 +1,39 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package packet_test | |||
import ( | |||
"encoding/json" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/bboozzoo/mconnect/protocol/packet" | |||
) | |||
func TestPacketAsIdentity(t *testing.T) { | |||
p := packet.Packet{ | |||
Type: "kdeconnect.ping", | |||
Body: json.RawMessage(`foo`), | |||
} | |||
i, err := p.AsIdentity() | |||
assert.Nil(t, i) | |||
assert.Error(t, err) | |||
p = packet.Packet{ | |||
Type: "kdeconnect.identity", | |||
Body: json.RawMessage(`{}`), | |||
} | |||
i, err = p.AsIdentity() | |||
assert.NotNil(t, i) | |||
assert.NoError(t, err) | |||
} |
@ -0,0 +1,30 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package packet | |||
import ( | |||
"encoding/json" | |||
) | |||
type Packet struct { | |||
Id uint64 `json:"id"` | |||
Type string `json:"type"` | |||
Body json.RawMessage `json:"body"` | |||
auxBody interface{} | |||
} | |||
func New(typ string, body interface{}) *Packet { | |||
return &Packet{ | |||
Type: typ, | |||
auxBody: body, | |||
} | |||
} |
@ -0,0 +1,36 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package packet | |||
import ( | |||
"encoding/json" | |||
"fmt" | |||
) | |||
type Pair struct { | |||
Pair bool `json:"pair"` | |||
} | |||
func NewPair() *Packet { | |||
return New("kdeconnect.pair", Pair{Pair: true}) | |||
} | |||
func (p *Packet) AsPair() (*Pair, error) { | |||
if p.Type != "kdeconnect.pair" { | |||
return nil, fmt.Errorf("not a pair packet, unexpected type %q", p.Type) | |||
} | |||
var pair Pair | |||
if err := json.Unmarshal(p.Body, &pair); err != nil { | |||
return nil, err | |||
} | |||
return &pair, nil | |||
} |
@ -0,0 +1,35 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package protocol | |||
import ( | |||
"net" | |||
) | |||
const ( | |||
// UDPPort is the UDP port used for discovery | |||
UDPPort = 1716 | |||
// UDPPortOld is the UDP port used by older versions of the protocol | |||
UDPPortOld = 1714 | |||
// TCPPortMin is the minimum TCP port number | |||
TCPPortMin = 1716 | |||
PayloadTransferPortMin = 1739 | |||
) | |||
var ( | |||
// UDPDiscoveryAddr is the UDP address used for discovery | |||
UDPDiscoveryAddr = &net.UDPAddr{ | |||
Port: UDPPort, | |||
IP: net.ParseIP("255.255.255.255"), | |||
} | |||
) |
@ -0,0 +1,36 @@ | |||
// Licensed under the Apache License, Version 2.0 (the "License"); | |||
// you may not use this file except in compliance with the License. | |||
// You may obtain a copy of the License at | |||
// | |||
// http://www.apache.org/licenses/LICENSE-2.0 | |||
// | |||
// Unless required by applicable law or agreed to in writing, software | |||
// distributed under the License is distributed on an "AS IS" BASIS, | |||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
// See the License for the specific language governing permissions and | |||
// limitations under the License. | |||
package flags | |||
import ( | |||
"fmt" | |||
"os" | |||
"github.com/jessevdk/go-flags" | |||
) | |||
func IsErrHelp(err error) bool { | |||
ferr, ok := err.(*flags.Error) | |||
return ok && ferr.Type == flags.ErrHelp | |||
} | |||
func HandleFlagsError(err error) { | |||
if err == nil { | |||
panic(fmt.Sprintf("expected an error, got %v", err)) | |||
} | |||
if IsErrHelp(err) { | |||
os.Exit(0) | |||
} | |||
os.Exit(1) | |||
} |