From 2dd8f72714655f03b7a0f870d4e40bdf9c3685e3 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Sat, 29 Jun 2024 09:38:07 +0200 Subject: [PATCH] Add project files --- .gitignore | 3 ++ LICENSE | 2 +- args.go | 30 ++++++++++++ config.example.yml | 8 +++ config.go | 24 +++++++++ go.mod | 17 +++++++ go.sum | 43 ++++++++++++++++ main.go | 15 ++++++ ssh.go | 119 +++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 args.go create mode 100644 config.example.yml create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 ssh.go diff --git a/.gitignore b/.gitignore index 6f6f5e6..64e4d8b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ # Go workspace file go.work go.work.sum + +# build +ass diff --git a/LICENSE b/LICENSE index c06137c..920dc4f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Julian Müller +Copyright (c) 2024 Julian Müller (ChaoticByte) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/args.go b/args.go new file mode 100644 index 0000000..8edf2a8 --- /dev/null +++ b/args.go @@ -0,0 +1,30 @@ +package main + +// Copyright (c) 2024 Julian Müller (ChaoticByte) +// License: MIT + +import ( + "flag" +) + +var configFilepath string +var privateKeyFilepath string +var logFlag bool + +func ParseCommandline() bool { + flag.StringVar(&configFilepath, "config", "", "The path to the config file (required)") + flag.StringVar(&privateKeyFilepath, "pkey", "", "The path to the private key file (required)") + flag.BoolVar(&logFlag, "log", false, "Enable logging of messages") + flag.Parse() + missing := false + if configFilepath == "" { + missing = true + } + if privateKeyFilepath == "" { + missing = true + } + if missing { + flag.Usage() + } + return !missing +} diff --git a/config.example.yml b/config.example.yml new file mode 100644 index 0000000..d0666e6 --- /dev/null +++ b/config.example.yml @@ -0,0 +1,8 @@ +# Example server config file +host: '' +port: 8022 +clients: + # This are just examples. Please generate your own keypair for each client you want to add. + # username: public-key + example1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEg9AzPuPe/zv0HszL+aRdJgZ8DPwVLV59FJoUHe7kWH + example2: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIQH6ESwEHVvlbxZJd7JhTPWXy3yQ5Zaf8qVBxzf6Td/ diff --git a/config.go b/config.go new file mode 100644 index 0000000..8c50776 --- /dev/null +++ b/config.go @@ -0,0 +1,24 @@ +package main + +// Copyright (c) 2024 Julian Müller (ChaoticByte) +// License: MIT + +import ( + "log" + "os" + + "github.com/goccy/go-yaml" +) + +var config struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Clients map[string]string `yaml:"clients"` +} + +func ParseConfig(filepath string) { + data, err := os.ReadFile(filepath) + if err != nil { log.Fatal(err) } + err = yaml.Unmarshal(data, &config) + if err != nil { log.Fatal(err) } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0935297 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/ChaoticByte/ass +go 1.22.4 + +require ( + github.com/gliderlabs/ssh v0.3.7 + github.com/goccy/go-yaml v1.11.3 +) + +require ( + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e17369b --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= +github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b4cf468 --- /dev/null +++ b/main.go @@ -0,0 +1,15 @@ +package main + +// Copyright (c) 2024 Julian Müller (ChaoticByte) +// License: MIT + +import "os" + +func main() { + if ParseCommandline() { + ParseConfig(configFilepath) + RunServer() + } else { + os.Exit(1) + } +} diff --git a/ssh.go b/ssh.go new file mode 100644 index 0000000..9e27614 --- /dev/null +++ b/ssh.go @@ -0,0 +1,119 @@ +package main + +// Copyright (c) 2024 Julian Müller (ChaoticByte) +// License: MIT + +import ( + "bufio" + "fmt" + "log" + "slices" + "sync" + + "github.com/gliderlabs/ssh" +) + +var sessionListMutex = &sync.Mutex{} +var sessionList = []*ssh.Session{} + +func PubkeyAuth(ctx ssh.Context, key ssh.PublicKey) bool { + // verify public key for auth + u := ctx.User() + pubkey := config.Clients[u] + if pubkey == "" { // username not in config.Clients + log.Printf("Authentication failure: Unknown user %s", u) + return false + } + k, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey)) + if err != nil { + log.Printf("Authentication failure: Could not parse public key for %s", u) + } + if ssh.KeysEqual(k, key) { + return true // bassd + } else { + log.Printf("Authentication failure: Invalid public key for user %s", u) + return false + } +} + +func AddToSessionList(s *ssh.Session) int { + // add new session to list + var idx int = -1 + sessionListMutex.Lock() + defer sessionListMutex.Unlock() + lenClientList := len(sessionList) + // try to reuse an existing nil position in the slice + for i := range(lenClientList) { + if sessionList[i] == nil { + // we found one! + sessionList[i] = s + idx = i + break + } + } + if idx == -1 { + // we have to append + sessionList = append(sessionList, s) + idx = lenClientList + } + return idx +} + +func RemoveFromSessionList(idx int) { + sessionListMutex.Lock() + defer sessionListMutex.Unlock() + // we have to set it to nil instead of constructing a new + // slice, so that the other session handlers have still + // the correct index for their own removal + sessionList[idx] = nil // session associated with the pointer reference should be freed automatically +} + +func BroadcastLine(line []byte) { + if logFlag { log.Printf("%s", line) } // output message + sessions := sessionList // create snapshot of client list + for i := range(len(sessions)) { + session := sessions[i] + if session != nil { + (*session).Write(line) + } + } +} + +func HandleSession(session ssh.Session) { + // get username + user := session.Context().User() + msgPrefix := []byte(fmt.Sprintf("%s: ", user)) + // Add client to list + sessionListIdx := AddToSessionList(&session) + defer func () { // cleanup tasks + RemoveFromSessionList(sessionListIdx) + log.Printf("User %s disconnected.\n", user) + BroadcastLine([]byte(fmt.Sprintf("[disconnected] %s\n", user))) + }() + // helo + log.Printf("User %s connected.\n", user) + BroadcastLine([]byte(fmt.Sprintf("[connected] %s\n", user))) + // IO + linebuf := bufio.NewReader(session) + for { + line, err := linebuf.ReadBytes('\n') + if err != nil { + if err.Error() != "EOF" { + log.Println("An error occurred! ", user, " ", err) + } + break // disconnect + } else { + BroadcastLine(slices.Concat(msgPrefix, line)) + } + } +} + +func RunServer() { + log.Printf("Starting SSH server on %s:%v", config.Host, config.Port) + log.Fatal(ssh.ListenAndServe( + fmt.Sprintf("%s:%v", config.Host, config.Port), + HandleSession, + ssh.HostKeyFile(privateKeyFilepath), + ssh.PublicKeyAuth(PubkeyAuth), + ssh.NoPty())) +}