Skip to content

Commit

Permalink
Allow backwards compatibility of Votifier votes
Browse files Browse the repository at this point in the history
  • Loading branch information
PassTheMayo committed May 20, 2024
1 parent f7fba5c commit 4d1a60a
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 137 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ go 1.18

require github.com/jessevdk/go-flags v1.5.0

require golang.org/x/sys v0.15.0 // indirect
require golang.org/x/sys v0.20.0 // indirect
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1 change: 1 addition & 0 deletions options/vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "time"

// Vote is the options used by the SendVote() function
type Vote struct {
// Deprecated: This property no longer affects how the vote is sent or processed.
RequireVersion int
PublicKey string
ServiceName string
Expand Down
265 changes: 131 additions & 134 deletions vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ import (

var (
// ErrPublicKeyRequired means that the server is using Votifier 1 but the PublicKey option is missing
ErrPublicKeyRequired = errors.New("vote: PublicKey is a required option but the value is empty")
ErrPublicKeyRequired = errors.New("vote: server negotiated Votifier 1, but PublicKey option is empty")
// ErrInvalidPublicKey means the public key provided cannot be parsed
ErrInvalidPublicKey = errors.New("vote: invalid public key value")
// ErrPublicKeyRequired means that the server is using Votifier 2 but the Token option is missing
ErrTokenRequired = errors.New("vote: Token is a required option but the value is empty")
ErrTokenRequired = errors.New("vote: server negotiated Votifier 2, but Token option is empty")
)

type voteMessage struct {
Expand Down Expand Up @@ -87,8 +87,9 @@ func sendVote(host string, port uint16, opts options.Vote) error {
}

var (
challenge string
version string
challenge string
version string
majorVersion string
)

// Handshake packet
Expand All @@ -102,159 +103,155 @@ func sendVote(host string, port uint16, opts options.Vote) error {

dataSegments := strings.Split(string(data[:len(data)-1]), " ")
version = dataSegments[1]
majorVersion = strings.Split(version, ".")[0]

if len(dataSegments) > 2 {
challenge = dataSegments[2]
}
}

switch strings.Split(version, ".")[0] {
case "1":
{
if opts.RequireVersion != 0 && opts.RequireVersion != 1 {
return fmt.Errorf("vote: version mismatch (server is running Votifier 1, expected to send version %d vote)", opts.RequireVersion)
}
if majorVersion != "2" && majorVersion != "1" {
return fmt.Errorf("vote: unknown Votifier version: %s", version)
}

if len(opts.PublicKey) < 1 {
return ErrPublicKeyRequired
}
if majorVersion == "2" && len(opts.Token) > 0 {
if err := sendVotifier2Vote(r, conn, host, port, challenge, opts); err != nil {
return err
}
} else if len(opts.PublicKey) > 0 {
if err := sendVotifier1Vote(conn, opts); err != nil {
return err
}
} else {
return fmt.Errorf("vote: version mismatch (server is expecting a Votifier %s packet, but options are missing to allow sending of this vote packet or any prior backwards-compatible versions)", version)
}

if len(opts.IPAddress) < 1 {
opts.IPAddress = "127.0.0.1"
}
return nil
}

// Vote packet
// https://github.com/NuVotifier/NuVotifier/wiki/Technical-QA#protocol-v1-deprecated
{
block, _ := pem.Decode([]byte(fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", opts.PublicKey)))
func sendVotifier1Vote(conn net.Conn, opts options.Vote) error {
if len(opts.IPAddress) < 1 {
opts.IPAddress = "127.0.0.1"
}

if block == nil {
return ErrInvalidPublicKey
}
// Vote packet
// https://github.com/NuVotifier/NuVotifier/wiki/Technical-QA#protocol-v1-deprecated
{
block, _ := pem.Decode([]byte(fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", opts.PublicKey)))

key, err := x509.ParsePKIXPublicKey(block.Bytes)
if block == nil {
return ErrInvalidPublicKey
}

if err != nil {
return err
}
key, err := x509.ParsePKIXPublicKey(block.Bytes)

publicKey, ok := key.(*rsa.PublicKey)
if err != nil {
return err
}

if !ok {
return fmt.Errorf("vote: parsed invalid key type: %T", key)
}
publicKey, ok := key.(*rsa.PublicKey)

payload := fmt.Sprintf(
"VOTE\n%s\n%s\n%s\n%s",
opts.ServiceName,
opts.Username,
opts.IPAddress,
opts.Timestamp.Format(time.RFC3339),
)
if !ok {
return fmt.Errorf("vote: parsed invalid key type: %T", key)
}

encryptedPayload, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(payload))
payload := fmt.Sprintf(
"VOTE\n%s\n%s\n%s\n%s",
opts.ServiceName,
opts.Username,
opts.IPAddress,
opts.Timestamp.Format(time.RFC3339),
)

if err != nil {
return err
}
encryptedPayload, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(payload))

if _, err = conn.Write(encryptedPayload); err != nil {
return err
}
}
if err != nil {
return err
}

break
if _, err = conn.Write(encryptedPayload); err != nil {
return err
}
}

return nil
}

func sendVotifier2Vote(r *bufio.Reader, conn net.Conn, host string, port uint16, challenge string, opts options.Vote) error {
// Vote packet
// https://github.com/NuVotifier/NuVotifier/wiki/Technical-QA#protocol-v2
{
buf := &bytes.Buffer{}

payload := votePayload{
ServiceName: opts.ServiceName,
Username: opts.Username,
Address: fmt.Sprintf("%s:%d", host, port),
Timestamp: opts.Timestamp.UnixMilli(),
Challenge: challenge,
UUID: opts.UUID,
}

payloadData, err := json.Marshal(payload)

if err != nil {
return err
}
case "2":
{
if opts.RequireVersion != 0 && opts.RequireVersion != 2 {
return fmt.Errorf("vote: version mismatch (server is running Votifier 2, expected to send version %d vote)", opts.RequireVersion)
}

if len(opts.Token) < 1 {
return ErrTokenRequired
}

// Vote packet
// https://github.com/NuVotifier/NuVotifier/wiki/Technical-QA#protocol-v2
{
buf := &bytes.Buffer{}

payload := votePayload{
ServiceName: opts.ServiceName,
Username: opts.Username,
Address: fmt.Sprintf("%s:%d", host, port),
Timestamp: opts.Timestamp.UnixMilli(),
Challenge: challenge,
UUID: opts.UUID,
}

payloadData, err := json.Marshal(payload)

if err != nil {
return err
}

hash := hmac.New(sha256.New, []byte(opts.Token))
hash.Write(payloadData)

message := voteMessage{
Payload: string(payloadData),
Signature: base64.StdEncoding.EncodeToString(hash.Sum(nil)),
}

messageData, err := json.Marshal(message)

if err != nil {
return err
}

if err := binary.Write(buf, binary.BigEndian, uint16(0x733A)); err != nil {
return err
}

if err := binary.Write(buf, binary.BigEndian, uint16(len(messageData))); err != nil {
return err
}

if _, err := buf.Write(messageData); err != nil {
return err
}

if _, err := io.Copy(conn, buf); err != nil {
return err
}
}

// Response packet
// https://github.com/NuVotifier/NuVotifier/wiki/Technical-QA#protocol-v2
{
data, err := r.ReadBytes('\n')

if err != nil {
return err
}

response := voteResponse{}

if err = json.Unmarshal(data[:len(data)-1], &response); err != nil {
return err
}

switch response.Status {
case "ok":
break
case "error":
return fmt.Errorf("vote: server returned error: %s", response.Error)
default:
return fmt.Errorf("vote: received unexpected server response (expected=<nil>, received=%s)", response.Status)
}
}

hash := hmac.New(sha256.New, []byte(opts.Token))
hash.Write(payloadData)

message := voteMessage{
Payload: string(payloadData),
Signature: base64.StdEncoding.EncodeToString(hash.Sum(nil)),
}

messageData, err := json.Marshal(message)

if err != nil {
return err
}

if err := binary.Write(buf, binary.BigEndian, uint16(0x733A)); err != nil {
return err
}

if err := binary.Write(buf, binary.BigEndian, uint16(len(messageData))); err != nil {
return err
}

if _, err := buf.Write(messageData); err != nil {
return err
}

if _, err := io.Copy(conn, buf); err != nil {
return err
}
}

// Response packet
// https://github.com/NuVotifier/NuVotifier/wiki/Technical-QA#protocol-v2
{
data, err := r.ReadBytes('\n')

if err != nil {
return err
}

response := voteResponse{}

if err = json.Unmarshal(data[:len(data)-1], &response); err != nil {
return err
}

switch response.Status {
case "ok":
break
case "error":
return fmt.Errorf("vote: server returned error: %s", response.Error)
default:
return fmt.Errorf("vote: received unexpected server response (expected=<nil>, received=%s)", response.Status)
}
default:
return fmt.Errorf("vote: unknown Votifier version: %s", version)
}

return nil
Expand Down

0 comments on commit 4d1a60a

Please sign in to comment.