Restructure project

This commit is contained in:
2021-07-08 13:11:41 -07:00
parent 8099077c50
commit 74c6391dca
12 changed files with 43 additions and 369 deletions

131
internal/config/config.go Normal file
View File

@@ -0,0 +1,131 @@
/*
Copyright © 2021 Arsen Musayelyan
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 config
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pelletier/go-toml"
"github.com/rs/zerolog/log"
)
// Struct for unmarshaling of opensend TOML configs
type Config struct {
Receiver ReceiverConfig
Sender SenderConfig
Targets map[string]Target
}
// Config section for receiver
type ReceiverConfig struct {
DestDir string `toml:"destinationDirectory"`
SkipZeroconf bool
WorkDir string `toml:"workingDirectory"`
}
// Config section for sender
type SenderConfig struct {
WorkDir string `toml:"workingDirectory"`
}
type Target struct {
IP string
}
// Attempt to find config path
func GetConfigPath() string {
// Use ConsoleWriter logger
// Possible config locations
configLocations := []string{"~/.config/opensend.toml", "/etc/opensend.toml"}
// For every possible location
for _, configLocation := range configLocations {
// Expand path (~ -> home dir and os.ExpandEnv())
expandedPath := ExpandPath(configLocation)
// If file does not exist
if _, err := os.Stat(expandedPath); errors.Is(err, os.ErrNotExist) {
// Skip
continue
}
// Return path with existing file
return expandedPath
}
// If all else fails, return empty screen
return ""
}
// Create new config object using values from given path
func NewConfig(path string) *Config {
// Use ConsoleWriter logger
// Create new empty config struct
newConfig := &Config{}
// Set config defaults
newConfig.SetDefaults()
// If path is provided
if path != "" {
// Read file at path
confData, err := ioutil.ReadFile(path)
if err != nil {
log.Fatal().Err(err).Msg("Error reading config")
}
// Unmarshal config data
err = toml.Unmarshal(confData, newConfig)
if err != nil {
log.Fatal().Err(err).Msg("Error unmarshalling toml")
}
}
// Return new config struct
return newConfig
}
// Set config defaults
func (config *Config) SetDefaults() {
// Set destination directory to $HOME/Downloads
config.Receiver.DestDir = ExpandPath("~/Downloads")
// Set receiver working directory to $HOME/.opensend
config.Receiver.WorkDir = ExpandPath("~/.opensend")
// Set do not skip zeroconf
config.Receiver.SkipZeroconf = false
// Set sender working directory to $HOME/.opensend
config.Sender.WorkDir = ExpandPath("~/.opensend")
// Set targets to an empty map[string]map[string]string
config.Targets = map[string]Target{}
}
func ExpandPath(s string) string {
// Use ConsoleWriter logger
// Get user's home directory
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatal().Err(err).Msg("Error getting home directory")
}
// Expand any environment variables in string
expandedString := os.ExpandEnv(s)
// If string starts with ~
if strings.HasPrefix(expandedString, "~") {
// Replace ~ with user's home directory
expandedString = strings.Replace(expandedString, "~", homeDir, 1)
}
// Clean file path
expandedString = filepath.Clean(expandedString)
// Return expanded string
return expandedString
}

193
internal/crypto/file.go Normal file
View File

@@ -0,0 +1,193 @@
/*
Copyright © 2021 Arsen Musayelyan
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 crypto
import (
"bytes"
"crypto/md5"
"crypto/rand"
"encoding/hex"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/klauspost/compress/zstd"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/chacha20poly1305"
)
// Encrypt given file using the shared key
func CompressAndEncryptFile(filePath string, newFilePath string, sharedKey string) {
// Use ConsoleWriter logger
// Read data from file
file, err := os.Open(filePath)
if err != nil {
log.Fatal().Err(err).Msg("Error opening file")
}
// Create buffer for compressed data
compressedBuffer := &bytes.Buffer{}
// Create Zstd encoder
zstdEncoder, err := zstd.NewWriter(compressedBuffer)
if err != nil {
log.Fatal().Err(err).Msg("Error creating Zstd encoder")
}
// Copy file data to Zstd encoder
_, err = io.Copy(zstdEncoder, file)
if err != nil {
log.Fatal().Err(err).Msg("Error reading file")
}
// Close Zstd encoder
zstdEncoder.Close()
// Read compressed data into data variable
data, err := ioutil.ReadAll(compressedBuffer)
if err != nil {
log.Fatal().Err(err).Msg("Error reading compressed buffer")
}
// Create md5 hash of password in order to make it the required size
md5Hash := md5.New()
md5Hash.Write([]byte(sharedKey))
// Encode md5 hash bytes into hexadecimal
hashedKey := hex.EncodeToString(md5Hash.Sum(nil))
// Create new AES c20cipher
c20cipher, err := chacha20poly1305.NewX([]byte(hashedKey))
if err != nil {
log.Fatal().Err(err).Msg("Error creating ChaCha20-Poly1305 cipher")
}
// Make byte slice for nonce
nonce := make([]byte, c20cipher.NonceSize())
// Read random bytes into nonce slice
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
log.Fatal().Err(err).Msg("Error creating nonce")
}
// Encrypt data
ciphertext := c20cipher.Seal(nonce, nonce, data, nil)
// Create new file
newFile, err := os.Create(newFilePath)
if err != nil {
log.Fatal().Err(err).Msg("Error creating file")
}
// Defer file close
defer newFile.Close()
// Write ciphertext to new file
bytesWritten, err := newFile.Write(ciphertext)
if err != nil {
log.Fatal().Err(err).Msg("Error writing to file")
}
// Log bytes written and to which file
log.Info().Str("file", filepath.Base(newFilePath)).Msg("Wrote " + strconv.Itoa(bytesWritten) + " bytes")
}
// Decrypt given file using the shared key
func DecryptAndDecompressFile(filePath string, newFilePath string, sharedKey string) {
// Use ConsoleWriter logger
// Read data from file
data, err := ioutil.ReadFile(filePath)
if err != nil {
log.Fatal().Err(err).Msg("Error reading file")
}
// Create md5 hash of password in order to make it the required size
md5Hash := md5.New()
md5Hash.Write([]byte(sharedKey))
hashedKey := hex.EncodeToString(md5Hash.Sum(nil))
// Create new AES cipher
c20cipher, err := chacha20poly1305.NewX([]byte(hashedKey))
if err != nil {
log.Fatal().Err(err).Msg("Error creating ChaCha20-Poly1305 cipher")
}
// Get standard GCM nonce size
nonceSize := c20cipher.NonceSize()
// Get nonce and ciphertext from data
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
// Decrypt data
plaintext, err := c20cipher.Open(nil, nonce, ciphertext, nil)
if err != nil {
log.Fatal().Err(err).Msg("Error decrypting data")
}
// Create new Zstd decoder
zstdDecoder, err := zstd.NewReader(bytes.NewBuffer(plaintext))
if err != nil {
log.Fatal().Err(err).Msg("Error creating Zstd decoder")
}
// Create new file
newFile, err := os.Create(newFilePath)
if err != nil {
log.Fatal().Err(err).Msg("Error creating file")
}
// Close new file at the end of this function
defer newFile.Close()
// Write decompressed plaintext to new file
bytesWritten, err := io.Copy(newFile, zstdDecoder)
if err != nil {
log.Fatal().Err(err).Msg("Error writing to file")
}
zstdDecoder.Close()
// Log bytes written and to which file
log.Info().Str("file", filepath.Base(newFilePath)).Msg("Wrote " + strconv.Itoa(int(bytesWritten)) + " bytes")
}
// Encrypt files in given directory using shared key
func EncryptFiles(dir string, sharedKey string) {
// Use ConsoleWriter logger
// Walk given directory
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
// If error reading, return err
if err != nil {
return err
}
// If file is not a directory and is not the key
if !info.IsDir() && !strings.Contains(path, "key.aes") {
// Compress and Encrypt the file using shared key, appending .zst.enc
CompressAndEncryptFile(path, path+".zst.enc", sharedKey)
// Remove unencrypted file
err := os.Remove(path)
if err != nil {
return err
}
}
// Return nil if no error occurs
return nil
})
if err != nil {
log.Fatal().Err(err).Msg("Error encrypting files")
}
}
// Decrypt files in given directory using shared key
func DecryptFiles(dir string, sharedKey string) {
// Use ConsoleWriter logger
// Walk given directory
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
// If error reading, return err
if err != nil {
return err
}
// If file is not a directory and is encrypted
if !info.IsDir() && strings.Contains(path, ".enc") {
// Decrypt and decompress the file using the shared key, removing .zst.enc
DecryptAndDecompressFile(path, strings.TrimSuffix(path, ".zst.enc"), sharedKey)
}
// Return nil if no errors occurred
return nil
})
if err != nil {
log.Fatal().Err(err).Msg("Error decrypting files")
}
}

95
internal/crypto/key.go Normal file
View File

@@ -0,0 +1,95 @@
/*
Copyright © 2021 Arsen Musayelyan
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 crypto
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"io/ioutil"
"net/http"
"github.com/rs/zerolog/log"
"go.arsenm.dev/opensend/internal/transfer"
)
// Generate RSA keypair
func GenerateRSAKeypair() (*rsa.PrivateKey, *rsa.PublicKey) {
// Use ConsoleWriter logger
// Generate private/public RSA keypair
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatal().Err(err).Msg("Error generating RSA keypair")
}
// Get public key
publicKey := privateKey.PublicKey
// Return keypair
return privateKey, &publicKey
}
// Get public key from sender
func GetKey(sender *transfer.Sender) []byte {
// Use ConsoleWriter logger
// Send key request to connection
keyReader, code, err := sender.Get("/key")
if err != nil {
log.Fatal().Err(err).Msg("Error sending key request")
}
// If ok code returned
if code == http.StatusOK {
// Read received bytes into key
key, err := ioutil.ReadAll(keyReader)
if err != nil {
log.Fatal().Err(err).Msg("Error reading key")
}
// Return key
return key
// Otherwise
} else {
// Fatally log
if err != nil {
log.Fatal().Msg("Server reported error")
}
}
// Return nil if all else fails
return nil
}
// Encrypt shared key with received public key
func EncryptKey(sharedKey string, recvPubKey *rsa.PublicKey) []byte {
// Use ConsoleWriter logger
// Encrypt shared key using RSA
encryptedSharedKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, recvPubKey, []byte(sharedKey), nil)
if err != nil {
log.Fatal().Err(err).Msg("Error encrypting shared key")
}
// Return encrypted key
return encryptedSharedKey
}
// Decrypt shared key using private RSA key
func DecryptKey(encryptedKey []byte, privateKey *rsa.PrivateKey) string {
// Decrypt shared key using RSA
decryptedKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encryptedKey, nil)
if err != nil {
log.Fatal().Err(err).Msg("Error decrypting shared key")
}
// Get string of decrypted key
sharedKey := string(decryptedKey)
// Return shared key
return sharedKey
}

View File

@@ -0,0 +1,75 @@
/*
Copyright © 2021 Arsen Musayelyan
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 crypto
import (
"crypto/rsa"
"encoding/gob"
"net"
"github.com/rs/zerolog/log"
)
// Exchange keys with sender
func ReceiverKeyExchange(key *rsa.PublicKey) string {
// Use ConsoleWriter logger
// Create TCP listener on port 9797
listener, err := net.Listen("tcp", ":9797")
if err != nil {
log.Fatal().Err(err).Msg("Error starting listener")
}
// Create string for sender address
var senderAddr string
for {
// Accept connection on listener
connection, err := listener.Accept()
if err != nil {
log.Fatal().Err(err).Msg("Error accepting connections")
}
// Get sender address and store it in senderAddr
senderAddr = connection.RemoteAddr().String()
// Create gob encoder with connection as io.Writer
encoder := gob.NewEncoder(connection)
// Encode key into connection
err = encoder.Encode(key)
if err != nil {
log.Fatal().Err(err).Msg("Error encoding key")
}
return senderAddr
}
}
// Exchange keys with receiver
func SenderKeyExchange(receiverIP string) *rsa.PublicKey {
// Use ConsoleWriter logger
// Connect to TCP socket on receiver IP port 9797
connection, err := net.Dial("tcp", receiverIP+":9797")
if err != nil {
log.Fatal().Err(err).Msg("Error connecting to sender")
}
// Create gob decoder
decoder := gob.NewDecoder(connection)
// Instantiate rsa.PublicKey struct
recvPubKey := &rsa.PublicKey{}
// Decode key
err = decoder.Decode(recvPubKey)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding key")
}
// Return received key
return recvPubKey
}

View File

@@ -0,0 +1,40 @@
/*
Copyright © 2021 Arsen Musayelyan
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 logging
import (
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Hook(FatalHook{})
// Fatal hook to run in case of Fatal error
type FatalHook struct {
WorkDir string
}
// Run function on trigger
func (hook FatalHook) Run(_ *zerolog.Event, level zerolog.Level, _ string) {
// If log event is fatal
if level == zerolog.FatalLevel {
// Attempt removal of opensend directory
_ = os.RemoveAll(hook.WorkDir)
}
}

View File

@@ -0,0 +1,204 @@
/*
Copyright © 2021 Arsen Musayelyan
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 serialization
import (
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strconv"
"github.com/mholt/archiver/v3"
"github.com/pkg/browser"
"github.com/rs/zerolog/log"
"github.com/vmihailenco/msgpack/v5"
)
// Create config type to store action type and data
type Parameters struct {
ActionType string
ActionData string
}
// Instantiate and return a new Config struct
func NewParameters(actionType string, actionData string) *Parameters {
return &Parameters{ActionType: actionType, ActionData: actionData}
}
func (parameters *Parameters) Validate() {
if parameters.ActionType == "url" {
// Parse URL in parameters
urlParser, err := url.Parse(parameters.ActionData)
// If there was an error parsing
if err != nil {
// Alert user of invalid url
log.Fatal().Err(err).Msg("Invalid URL")
// If scheme is not detected
} else if urlParser.Scheme == "" {
// Alert user of invalid scheme
log.Fatal().Msg("Invalid URL scheme")
// If host is not detected
} else if urlParser.Host == "" {
// Alert user of invalid host
log.Fatal().Msg("Invalid URL host")
}
}
}
// Create config file
func (parameters *Parameters) CreateFile(dir string) {
// Use ConsoleWriter logger
// Create parameters file at given directory
configFile, err := os.Create(dir + "/parameters.msgpack")
if err != nil {
log.Fatal().Err(err).Msg("Error creating parameters file")
}
// Close parameters file at the end of this function
defer configFile.Close()
// Marshal given Parameters struct into a []byte
MessagePackData, err := msgpack.Marshal(parameters)
if err != nil {
log.Fatal().Err(err).Msg("Error encoding MessagePack")
}
// Write []byte to previously created parameters file
bytesWritten, err := configFile.Write(MessagePackData)
if err != nil {
log.Fatal().Err(err).Msg("Error writing MessagePack to file")
}
// Log bytes written
log.Info().Str("file", "parameters.msgpack").Msg("Wrote " + strconv.Itoa(bytesWritten) + " bytes")
}
// Collect all required files into given directory
func (parameters *Parameters) CollectFiles(dir string) {
// Use ConsoleWriter logger
// If action type is file
if parameters.ActionType == "file" {
// Open file path in parameters.ActionData
src, err := os.Open(parameters.ActionData)
if err != nil {
log.Fatal().Err(err).Msg("Error opening file from parameters")
}
// Close source file at the end of this function
defer src.Close()
// Create new file with the same name at given directory
dst, err := os.Create(dir + "/" + filepath.Base(parameters.ActionData))
if err != nil {
log.Fatal().Err(err).Msg("Error creating file")
}
// Close new file at the end of this function
defer dst.Close()
// Copy data from source file to destination file
_, err = io.Copy(dst, src)
if err != nil {
log.Fatal().Err(err).Msg("Error copying data to file")
}
// Replace file path in parameters.ActionData with file name
parameters.ActionData = filepath.Base(parameters.ActionData)
} else if parameters.ActionType == "dir" {
err := archiver.Archive([]string{parameters.ActionData}, dir+"/"+filepath.Base(parameters.ActionData)+".tar")
if err != nil {
log.Fatal().Err(err).Msg("Error creating tar archive")
}
// Set parameters data to base path for receiver
parameters.ActionData = filepath.Base(parameters.ActionData)
}
}
// Read config file at given file path
func (parameters *Parameters) ReadFile(filePath string) {
// Use ConsoleWriter logger
// Read file at filePath
fileData, err := ioutil.ReadFile(filePath)
if err != nil {
log.Fatal().Err(err).Msg("Error reading parameters file")
}
// Unmarshal data from MessagePack into parameters struct
err = msgpack.Unmarshal(fileData, parameters)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding MessagePack")
}
}
// Execute action specified in config
func (parameters *Parameters) ExecuteAction(srcDir string, destDir string) {
// Use ConsoleWriter logger
// If action is file
switch parameters.ActionType {
case "file":
// Open file from parameters at given directory
src, err := os.Open(srcDir + "/" + parameters.ActionData)
if err != nil {
log.Fatal().Err(err).Msg("Error reading file from parameters")
}
// Close source file at the end of this function
defer src.Close()
// Create file in user's Downloads directory
dst, err := os.Create(filepath.Clean(destDir) + "/" + parameters.ActionData)
if err != nil {
log.Fatal().Err(err).Msg("Error creating file")
}
// Close destination file at the end of this function
defer dst.Close()
// Copy data from source file to destination file
_, err = io.Copy(dst, src)
if err != nil {
log.Fatal().Err(err).Msg("Error copying data to file")
}
// If action is url
case "url":
// Parse received URL
urlParser, err := url.Parse(parameters.ActionData)
// If there was an error parsing
if err != nil {
// Alert user of invalid url
log.Fatal().Err(err).Msg("Invalid URL")
// If scheme is not detected
} else if urlParser.Scheme == "" {
// Alert user of invalid scheme
log.Fatal().Msg("Invalid URL scheme")
// If host is not detected
} else if urlParser.Host == "" {
// Alert user of invalid host
log.Fatal().Msg("Invalid URL host")
}
// Attempt to open URL in browser
err = browser.OpenURL(parameters.ActionData)
if err != nil {
log.Fatal().Err(err).Msg("Error opening browser")
}
// If action is dir
case "dir":
// Set destination directory to ~/Downloads/{dir name}
dstDir := filepath.Dir(filepath.Clean(destDir) + "/" + parameters.ActionData)
fmt.Println(dstDir)
err := archiver.Unarchive(srcDir+"/"+parameters.ActionData+".tar", dstDir)
if err != nil {
log.Fatal().Err(err).Msg("Error extracting tar archive")
}
// Loop to recursively unarchive tar file
// Catchall
default:
// Log unknown action type
log.Fatal().Msg("Unknown action type " + parameters.ActionType)
}
}

View File

@@ -0,0 +1,80 @@
/*
Copyright © 2021 Arsen Musayelyan
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 transfer
import (
"context"
"os"
"time"
"github.com/grandcat/zeroconf"
"github.com/rs/zerolog/log"
)
// Discover opensend receivers on the network
func DiscoverReceivers() ([]string, []string) {
// Use ConsoleWriter logger
// Create zeroconf resolver
resolver, err := zeroconf.NewResolver(nil)
if err != nil {
log.Fatal().Err(err).Msg("Error creating zeroconf resolver")
}
// Create channel for zeroconf entries
entries := make(chan *zeroconf.ServiceEntry)
// Create slice to store hostnames of discovered receivers
var discoveredReceivers []string
// Create slice to store IPs of discovered receivers
var discoveredReceiverIPs []string
// Concurrently run mDNS query
go func(results <-chan *zeroconf.ServiceEntry) {
// For each entry
for entry := range results {
// Append hostname to discoveredReceivers
discoveredReceivers = append(discoveredReceivers, entry.HostName)
// Append IP to discoveredReceiverIPs
discoveredReceiverIPs = append(discoveredReceiverIPs, entry.AddrIPv4[0].String())
}
}(entries)
// Create context with 4 second timeout
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
// Cancel context at the end of this function
defer cancel()
// Browse for mDNS entries
err = resolver.Browse(ctx, "_opensend._tcp", "local.", entries)
if err != nil {
log.Fatal().Err(err).Msg("Error browsing zeroconf services")
}
// Send Done signal to context
<-ctx.Done()
// Return discovered receiver slices
return discoveredReceivers, discoveredReceiverIPs
}
// Register opensend zeroconf service on the network
func RegisterService() func() {
// Get computer hostname
hostname, _ := os.Hostname()
// Register zeroconf service {hostname}._opensend._tcp.local.
server, err := zeroconf.Register(hostname, "_opensend._tcp", "local.", 9797, []string{"txtv=0", "lo=1", "la=2"}, nil)
if err != nil {
log.Fatal().Err(err).Msg("Error registering zeroconf service")
}
// Return server.Shutdown() function to allow for shutdown in main()
return server.Shutdown
}

View File

@@ -0,0 +1,189 @@
/*
Copyright © 2021 Arsen Musayelyan
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 transfer
import (
"io"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/rs/zerolog/log"
)
// Save encrypted key to file
func SaveEncryptedKey(encryptedKey []byte, filePath string) {
// Use ConsoleWriter logger
// Create file at given file path
keyFile, err := os.Create(filePath)
if err != nil {
log.Fatal().Err(err).Msg("Error creating file")
}
// Close file at the end of this function
defer keyFile.Close()
// Write encrypted key to file
bytesWritten, err := keyFile.Write(encryptedKey)
if err != nil {
log.Fatal().Err(err).Msg("Error writing key to file")
}
// Log bytes written
log.Info().Str("file", filepath.Base(filePath)).Msg("Wrote " + strconv.Itoa(bytesWritten) + " bytes")
}
// Create HTTP server to transmit files
func SendFiles(dir string) {
// Use ConsoleWriter logger with normal FatalHook
// Create TCP listener on port 9898
listener, err := net.Listen("tcp", ":9898")
if err != nil {
log.Fatal().Err(err).Msg("Error starting listener")
}
http.HandleFunc("/key", func(res http.ResponseWriter, req *http.Request) {
// Inform user client has requested key
log.Info().Msg("Key requested")
// Read saved key
key, err := ioutil.ReadFile(dir + "/key.aes")
if err != nil {
log.Fatal().Err(err).Msg("Error reading key")
}
// Write saved key to ResponseWriter
_, err = res.Write(key)
if err != nil {
log.Fatal().Err(err).Msg("Error writing response")
}
})
http.HandleFunc("/index", func(res http.ResponseWriter, req *http.Request) {
// Inform user a client has requested the file index
log.Info().Msg("Index requested")
// Get directory listing
dirListing, err := ioutil.ReadDir(dir)
if err != nil {
log.Fatal().Err(err).Msg("Error reading directory")
}
// Create new slice to house filenames for index
var indexSlice []string
// For each file in listing
for _, file := range dirListing {
// If the file is not the key
if !strings.Contains(file.Name(), "key.aes") {
// Append the file path to indexSlice
indexSlice = append(indexSlice, file.Name())
}
}
// Join index slice into string
indexStr := strings.Join(indexSlice, "|")
// Write index to ResponseWriter
_, err = res.Write([]byte(indexStr))
if err != nil {
log.Fatal().Err(err).Msg("Error writing response")
}
})
http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
log.Info().Str("file", filepath.Base(req.URL.Path)).Msg("File requested")
http.FileServer(http.Dir(dir)).ServeHTTP(res, req)
})
http.HandleFunc("/stop", func(res http.ResponseWriter, req *http.Request) {
log.Info().Msg("Stop signal received")
res.WriteHeader(http.StatusOK)
listener.Close()
})
http.Serve(listener, nil)
}
type Sender struct {
RemoteAddr string
}
func (c *Sender) Get(endpoint string) (io.ReadCloser, int, error) {
res, err := http.Get(c.RemoteAddr + endpoint)
if err != nil {
return nil, 0, err
}
return res.Body, res.StatusCode, nil
}
func NewSender(senderAddr string) *Sender {
// Get server address by getting the IP without the port, and appending :9898
host, _, _ := net.SplitHostPort(senderAddr)
serverAddr := "http://" + net.JoinHostPort(host, "9898")
return &Sender{RemoteAddr: serverAddr}
}
// Get files from sender
func RecvFiles(sender *Sender, workDir string) {
// Use ConsoleWriter logger
indexReader, code, err := sender.Get("/index")
if err != nil {
log.Fatal().Err(err).Msg("Error getting index")
}
// If non-ok code returned, fatally log
if code != http.StatusOK {
log.Fatal().Err(err).Msg("Sender reported error")
}
indexBytes, err := ioutil.ReadAll(indexReader)
if err != nil {
log.Fatal().Err(err).Msg("Error reading index from response")
}
// Get index from message
index := strings.Split(strings.TrimSpace(string(indexBytes)), "|")
for _, file := range index {
// Read received message
fileData, code, err := sender.Get("/" + file)
if err != nil {
log.Fatal().Err(err).Msg("Error getting file")
}
// If non-ok code returned
if code != http.StatusOK {
// fatally log
log.Fatal().
Int("status", code).
Str("statusText", http.StatusText(code)).
Err(err).
Msg("Sender reported error")
// Otherwise
} else {
// Create new file at index filepath
newFile, err := os.Create(workDir + "/" + file)
if err != nil {
log.Fatal().Err(err).Msg("Error creating file")
}
// Copy response body to new file
bytesWritten, err := io.Copy(newFile, fileData)
if err != nil {
log.Fatal().Err(err).Msg("Error writing to file")
}
// Log bytes written
log.Info().Str("file", filepath.Base(file)).Msg("Wrote " + strconv.Itoa(int(bytesWritten)) + " bytes")
// Close new file
newFile.Close()
}
}
}
// Send stop signal to sender
func SendSrvStopSignal(sender *Sender) {
_, _, _ = sender.Get("/stop")
}