Compare commits

...

38 Commits

Author SHA1 Message Date
  Maciek Borzecki 37e45ea847 go mod support 5 years ago
  Maciek Borzecki d76c6e3397 cmd/mconnect-connector: trivial packet receive/send loop 5 years ago
  Maciek Borzecki 34f26d176a protocol: update connection to TLS after sending identity 5 years ago
  Maciek Borzecki f2f4245ca8 protocol: send/receive helpers 5 years ago
  Maciek Borzecki 2cb0eb8639 protocol: certificate generator 5 years ago
  Maciek Borzecki c40ed84737 protocol: starting with listener 5 years ago
  Maciek Borzecki bd1497d632 protocol/packet: pair packet wrappers 5 years ago
  Maciek Borzecki 1c0400f044 cmd/mconnect-connector: helper tool for establishing connections 7 years ago
  Maciek Borzecki aedd1fa243 protocol: connection helper 7 years ago
  Maciek Borzecki b675280994 cmd/mconnect-discover: display tcp port 7 years ago
  Maciek Borzecki b447fdfb7d protocol/packet: add packet encoder and decoder 7 years ago
  Maciek Borzecki c5923774f4 protocol/packet: helper for creating identity packets 7 years ago
  Maciek Borzecki ad36298f60 cmd/mconnect-discover: add flags 7 years ago
  Maciek Borzecki c9b15623b9 utils/flags: some helpers for github.com/jessevdk/go-flags 7 years ago
  Maciek Borzecki b482b5d2a9 cmd/mconnect-discover: track discovered devices, nicer output 7 years ago
  Maciek Borzecki 322fb34aea logger: add level support 7 years ago
  Maciek Borzecki 2486560099 discovery: use protocol port wrappers 7 years ago
  Maciek Borzecki c6f33b9e4f protocol: add UDP and TCP ports 7 years ago
  Maciek Borzecki 3ca4c43a44 cmd/mconnect-discover: self announce 7 years ago
  Maciek Borzecki e50a5745e1 discovery: use new UDP port 7 years ago
  Maciek Borzecki 48e691d0b7 discovery: self announce 7 years ago
  Maciek Borzecki a6670d1fcc protocol/packet: json field mappings for identity packet 7 years ago
  Maciek Borzecki ba44186903 protocol/packet: encoder 7 years ago
  Maciek Borzecki 19f3aea2dd protocol/packet: json field names, settable body 7 years ago
  Maciek Borzecki a38a88406d protocol/packet: renme decoder test 7 years ago
  Maciek Borzecki f8a3638203 protocol/packet: renames 7 years ago
  Maciek Borzecki cc8e4a80b6 discovery: update listener to account for change unmarshaller 7 years ago
  Maciek Borzecki 831273b043 protocol/packet: refactor unmarshaller 7 years ago
  Maciek Borzecki e222ee0509 dirs: update path to user data dir 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
26 changed files with 1270 additions and 0 deletions
Split View
  1. +119
    -0
      cmd/mconnect-connector/main.go
  2. +103
    -0
      cmd/mconnect-discover/main.go
  3. +41
    -0
      dirs/dirs.go
  4. +29
    -0
      dirs/dirs_test.go
  5. +20
    -0
      dirs/export_test.go
  6. +42
    -0
      discovery/announce.go
  7. +78
    -0
      discovery/listener.go
  8. +17
    -0
      go.mod
  9. +33
    -0
      logger/context.go
  10. +51
    -0
      logger/logger.go
  11. +45
    -0
      logger/logrus.go
  12. +77
    -0
      protocol/cert.go
  13. +85
    -0
      protocol/connection.go
  14. +20
    -0
      protocol/listen.go
  15. +52
    -0
      protocol/packet/decoder.go
  16. +71
    -0
      protocol/packet/decoder_test.go
  17. +16
    -0
      protocol/packet/discovery.go
  18. +81
    -0
      protocol/packet/encoder.go
  19. +52
    -0
      protocol/packet/encoder_test.go
  20. +20
    -0
      protocol/packet/export_test.go
  21. +42
    -0
      protocol/packet/identity.go
  22. +39
    -0
      protocol/packet/identity_test.go
  23. +30
    -0
      protocol/packet/packet.go
  24. +36
    -0
      protocol/packet/pair.go
  25. +35
    -0
      protocol/ports.go
  26. +36
    -0
      utils/flags/flags.go

+ 119
- 0
cmd/mconnect-connector/main.go View File

@ -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)
}
}
}
}

+ 103
- 0
cmd/mconnect-discover/main.go View File

@ -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)
}
}
}

+ 41
- 0
dirs/dirs.go View File

@ -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")
}

+ 29
- 0
dirs/dirs_test.go View File

@ -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")
}

+ 20
- 0
dirs/export_test.go View File

@ -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
}
}

+ 42
- 0
discovery/announce.go View File

@ -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
}

+ 78
- 0
discovery/listener.go View File

@ -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
}

+ 17
- 0
go.mod View File

@ -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
)

+ 33
- 0
logger/context.go View File

@ -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)
}

+ 51
- 0
logger/logger.go View File

@ -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
)

+ 45
- 0
logger/logrus.go View File

@ -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))
}

+ 77
- 0
protocol/cert.go View File

@ -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
}

+ 85
- 0
protocol/connection.go View File

@ -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)
}

+ 20
- 0
protocol/listen.go View File

@ -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)
}

+ 52
- 0
protocol/packet/decoder.go View File

@ -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
}

+ 71
- 0
protocol/packet/decoder_test.go View File

@ -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)
}

+ 16
- 0
protocol/packet/discovery.go View File

@ -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() {
}

+ 81
- 0
protocol/packet/encoder.go View File

@ -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
}

+ 52
- 0
protocol/packet/encoder_test.go View File

@ -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)
}

+ 20
- 0
protocol/packet/export_test.go View File

@ -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
}
}

+ 42
- 0
protocol/packet/identity.go View File

@ -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)
}

+ 39
- 0
protocol/packet/identity_test.go View File

@ -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)
}

+ 30
- 0
protocol/packet/packet.go View File

@ -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,
}
}

+ 36
- 0
protocol/packet/pair.go View File

@ -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
}

+ 35
- 0
protocol/ports.go View File

@ -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"),
}
)

+ 36
- 0
utils/flags/flags.go View File

@ -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)
}

Loading…
Cancel
Save