This commit is contained in:
commit
a258da3678
10 changed files with 609 additions and 0 deletions
17
.drone.yml
Normal file
17
.drone.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: kubernetes
|
||||
name: default
|
||||
|
||||
services:
|
||||
- name: echo-server
|
||||
image: ghcr.io/will-scargill/echo:latest
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang:1.18
|
||||
commands:
|
||||
- curl -L https://vh7.uk/9kiw > waitfor.sh && chmod +x waitfor.sh
|
||||
- ./waitfor.sh 127.0.0.1:16000 -t 60
|
||||
- go test -v ./...
|
||||
|
139
.gitignore
vendored
Normal file
139
.gitignore
vendored
Normal file
|
@ -0,0 +1,139 @@
|
|||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
### Intellij+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Intellij+all Patch ###
|
||||
# Ignore everything but code style settings and run configurations
|
||||
# that are supposed to be shared within teams.
|
||||
|
||||
.idea/*
|
||||
|
||||
!.idea/codeStyles
|
||||
!.idea/runConfigurations
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# Support for Project snippet scope
|
||||
.vscode/*.code-snippets
|
||||
|
||||
# Ignore code-workspaces
|
||||
*.code-workspace
|
78
crypto/aes.go
Normal file
78
crypto/aes.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func packCipherData(cipherText, iv []byte) ([]byte, error) {
|
||||
return json.Marshal([]string{
|
||||
base64.StdEncoding.EncodeToString(cipherText),
|
||||
base64.StdEncoding.EncodeToString(iv),
|
||||
})
|
||||
}
|
||||
|
||||
func unpackCipherData(data []byte) ([]byte, []byte, error) {
|
||||
var cipherData []string
|
||||
err := json.Unmarshal(data, &cipherData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(cipherData) != 2 {
|
||||
return nil, nil, fmt.Errorf("invalid cipher data")
|
||||
}
|
||||
|
||||
cipherText, err := base64.StdEncoding.DecodeString(cipherData[0])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
iv, err := base64.StdEncoding.DecodeString(cipherData[1])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return cipherText, iv, nil
|
||||
}
|
||||
|
||||
func EncryptAesCbc(aes cipher.Block, plainTextBytes []byte) ([]byte, error) {
|
||||
iv := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encrypter := cipher.NewCBCEncrypter(aes, iv)
|
||||
|
||||
plainTextBytes, err := pkcs7Pad(plainTextBytes, encrypter.BlockSize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cipherText := make([]byte, len(plainTextBytes))
|
||||
encrypter.CryptBlocks(cipherText, plainTextBytes)
|
||||
|
||||
return packCipherData(cipherText, iv)
|
||||
}
|
||||
|
||||
func DecryptAesCbc(aes cipher.Block, data []byte) ([]byte, error) {
|
||||
encrypted, iv, err := unpackCipherData(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptor := cipher.NewCBCDecrypter(aes, iv)
|
||||
|
||||
decryptedBytes := make([]byte, len(encrypted))
|
||||
decryptor.CryptBlocks(decryptedBytes, encrypted)
|
||||
|
||||
decryptedBytes, err = pkcs7Unpad(decryptedBytes, decryptor.BlockSize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decryptedBytes, nil
|
||||
}
|
44
crypto/padding.go
Normal file
44
crypto/padding.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ref: https://golang-examples.tumblr.com/post/98350728789/pkcs7-padding
|
||||
// Appends padding.
|
||||
func pkcs7Pad(data []byte, blocklen int) ([]byte, error) {
|
||||
if blocklen <= 0 {
|
||||
return nil, fmt.Errorf("Invalid block length %d", blocklen)
|
||||
}
|
||||
padlen := 1
|
||||
for ((len(data) + padlen) % blocklen) != 0 {
|
||||
padlen = padlen + 1
|
||||
}
|
||||
|
||||
pad := bytes.Repeat([]byte{byte(padlen)}, padlen)
|
||||
return append(data, pad...), nil
|
||||
}
|
||||
|
||||
// Returns slice of the original data without padding.
|
||||
func pkcs7Unpad(data []byte, blocklen int) ([]byte, error) {
|
||||
if blocklen <= 0 {
|
||||
return nil, fmt.Errorf("Invalid block length %d", blocklen)
|
||||
}
|
||||
if len(data)%blocklen != 0 || len(data) == 0 {
|
||||
return nil, fmt.Errorf("Invalid data length %d", len(data))
|
||||
}
|
||||
padlen := int(data[len(data)-1])
|
||||
if padlen > blocklen || padlen == 0 {
|
||||
return nil, fmt.Errorf("Invalid padding")
|
||||
}
|
||||
// check padding
|
||||
pad := data[len(data)-padlen:]
|
||||
for i := 0; i < padlen; i++ {
|
||||
if pad[i] != byte(padlen) {
|
||||
return nil, fmt.Errorf("Invalid padding")
|
||||
}
|
||||
}
|
||||
|
||||
return data[:len(data)-padlen], nil
|
||||
}
|
24
crypto/rsa.go
Normal file
24
crypto/rsa.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
)
|
||||
|
||||
func RsaEncrypt(pubKey []byte, text []byte) (string, error) {
|
||||
block, _ := pem.Decode(pubKey)
|
||||
key, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
k := key.(*rsa.PublicKey)
|
||||
ciphertext, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, k, text, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
|||
module git.vh7.uk/jakew/echo-go
|
||||
|
||||
go 1.18
|
0
go.sum
Normal file
0
go.sum
Normal file
232
main.go
Normal file
232
main.go
Normal file
|
@ -0,0 +1,232 @@
|
|||
package echo_go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"git.vh7.uk/jakew/echo-go/crypto"
|
||||
)
|
||||
|
||||
const EchoVersion = "3.17"
|
||||
|
||||
func randomHex(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func (c *Client) SendPlain(msgType MessageType, data *string, subType *string, metadata []string) error {
|
||||
metadataBytes, err := json.Marshal(&metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg := RawMessage{
|
||||
UserId: c.UserId,
|
||||
MessageType: msgType,
|
||||
SubType: subType,
|
||||
Data: data,
|
||||
Metadata: string(metadataBytes),
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//log.Printf("sending message %v", msg)
|
||||
_, err = c.Con.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) Send(msgType MessageType, data *string, subType *string, metadata []string) error {
|
||||
metadataBytes, err := json.Marshal(&metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg := RawMessage{
|
||||
UserId: c.UserId,
|
||||
MessageType: msgType,
|
||||
SubType: subType,
|
||||
Data: data,
|
||||
Metadata: string(metadataBytes),
|
||||
}
|
||||
|
||||
plainBytes, err := json.Marshal(&msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aesCipher, err := aes.NewCipher([]byte(c.SessionKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encryptedBytes, err := crypto.EncryptAesCbc(aesCipher, plainBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.Con.Write(encryptedBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) ReceivePlain() ([]RawMessage, error) {
|
||||
data := make([]byte, 20480)
|
||||
_, err := c.Con.Read(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messages := []RawMessage{}
|
||||
|
||||
for _, message := range strings.Split(string(bytes.Trim(data, "\x00")), "}") {
|
||||
if strings.TrimSpace(message) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var msg RawMessage
|
||||
err = json.Unmarshal([]byte(message+"}"), &msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (c *Client) Receive() ([]RawMessage, error) {
|
||||
data := make([]byte, 20480)
|
||||
_, err := c.Con.Read(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messages := []RawMessage{}
|
||||
|
||||
for _, message := range strings.Split(string(bytes.Trim(data, "\x00")), "]") {
|
||||
if strings.TrimSpace(message) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
aesCipher, err := aes.NewCipher([]byte(c.SessionKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decrypted, err := crypto.DecryptAesCbc(aesCipher, []byte(message+"]"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var msg RawMessage
|
||||
err = json.Unmarshal(decrypted, &msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (c *Client) handshakeLoop(password string) error {
|
||||
log.Println("sending server info request")
|
||||
err := c.SendPlain(ReqServerInfo, nil, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encrypted := false
|
||||
|
||||
for {
|
||||
var msgs []RawMessage
|
||||
var err error
|
||||
if encrypted {
|
||||
msgs, err = c.Receive()
|
||||
} else {
|
||||
msgs, err = c.ReceivePlain()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, msg := range msgs {
|
||||
switch msg.MessageType {
|
||||
case ResServerInfo:
|
||||
ciphertext, err := crypto.RsaEncrypt([]byte(*msg.Data), []byte(c.SessionKey))
|
||||
err = c.SendPlain(ReqClientSecret, &ciphertext, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encrypted = true
|
||||
case ResClientSecret:
|
||||
data, err := json.Marshal([]string{
|
||||
c.Username,
|
||||
password,
|
||||
EchoVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dataStr := string(data)
|
||||
err = c.Send(ReqConnection, &dataStr, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case ResConnectionAccepted:
|
||||
log.Println("handshake accepted")
|
||||
return nil
|
||||
case ResConnectionDenied:
|
||||
return fmt.Errorf("handshake failed - %v", *msg.Data)
|
||||
default:
|
||||
return fmt.Errorf("unexpected handshake message type %v", msg.MessageType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Disconnect() {
|
||||
log.Println("gracefully disconnecting")
|
||||
_ = c.Send(ReqDisconnect, nil, nil, nil)
|
||||
_ = c.Con.Close()
|
||||
}
|
||||
|
||||
func New(addr string, username string) (*Client, error) {
|
||||
userId, err := randomHex(32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionKey, err := randomHex(8)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("connecting to %v", addr)
|
||||
|
||||
con, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := Client{
|
||||
Con: con,
|
||||
UserId: userId,
|
||||
SessionKey: sessionKey,
|
||||
Username: username,
|
||||
}
|
||||
|
||||
return &client, nil
|
||||
}
|
17
main_test.go
Normal file
17
main_test.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package echo_go
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCanConnect(t *testing.T) {
|
||||
client, err := New("127.0.0.1:16000", "EchoGoTest")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
err = client.handshakeLoop("mypassword")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run handshake loop: %v", err)
|
||||
}
|
||||
|
||||
client.Disconnect()
|
||||
}
|
55
types.go
Normal file
55
types.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package echo_go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
type MessageType string
|
||||
|
||||
const (
|
||||
ReqServerInfo MessageType = "serverInfoRequest"
|
||||
ReqClientSecret = "clientSecret"
|
||||
ReqConnection = "connectionRequest"
|
||||
ReqDisconnect = "disconnect"
|
||||
ReqInfo = "requestInfo"
|
||||
ReqUserMessage = "userMessage"
|
||||
ReqChangeChannel = "changeChannel"
|
||||
ReqHistory = "historyRequest"
|
||||
ReqLeaveChannel = "leaveChannel"
|
||||
|
||||
ResServerInfo = "serverInfo"
|
||||
ResClientSecret = "gotSecret"
|
||||
ResConnectionAccepted = "CRAccepted"
|
||||
ResConnectionDenied = "CRDenied"
|
||||
ResOutboundMessage = "outboundMessage"
|
||||
ResConnectionTerminated = "connectionTerminated"
|
||||
ResChannelUpdate = "channelUpdate"
|
||||
ResCommandData = "commandData"
|
||||
ResChannelHistory = "channelHistory"
|
||||
ResErrorOccurred = "errorOccured"
|
||||
ResAdditionalHistory = "additionalHistory"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Con net.Conn
|
||||
UserId string
|
||||
SessionKey string
|
||||
Username string
|
||||
}
|
||||
|
||||
type RawMessage struct {
|
||||
UserId string `json:"userid"`
|
||||
MessageType MessageType `json:"messagetype"`
|
||||
SubType *string `json:"subtype"`
|
||||
Data *string `json:"data"`
|
||||
Metadata string `json:"metadata"`
|
||||
}
|
||||
|
||||
func (m *RawMessage) String() string {
|
||||
return fmt.Sprintf("\n"+
|
||||
" UserId: %v\n"+
|
||||
" Type: %v - %v\n"+
|
||||
" Data: %v\n"+
|
||||
" Metadata: %v", m.UserId, m.MessageType, *m.SubType, *m.Data, m.Metadata)
|
||||
}
|
Loading…
Reference in a new issue