mirror of
https://github.com/arnaucube/bc.git
synced 2026-02-07 11:06:40 +01:00
first commit
This commit is contained in:
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# bc
|
||||||
|
|
||||||
|
Own p2p network and own blockchain libraries written in Go, to develop own decentralized apps.
|
||||||
4
memory.md
Normal file
4
memory.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
- p2plib/messages.go / MessageHandler
|
||||||
|
put in a way that the messages handlers can be created from the main code (not from the package)
|
||||||
|
|
||||||
|
- subsitute the REST functions by the messages in the MessageHandler
|
||||||
1
peer/.gitignore
vendored
Normal file
1
peer/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.data
|
||||||
73
peer/README.md
Normal file
73
peer/README.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Peer
|
||||||
|
|
||||||
|
To run as a normal peer:
|
||||||
|
```
|
||||||
|
./peer
|
||||||
|
```
|
||||||
|
|
||||||
|
To run as a p2p server:
|
||||||
|
```
|
||||||
|
./peer server
|
||||||
|
```
|
||||||
|
|
||||||
|
Needs the config.json file:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"port": "3001",
|
||||||
|
"serverip": "127.0.0.1",
|
||||||
|
"serverport": "3000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Peer REST API
|
||||||
|
- GET /
|
||||||
|
- Returns the peer.ID (where peer.ID = hash(peer.IP + ":" + peer.Port))
|
||||||
|
|
||||||
|
|
||||||
|
- GET /peers
|
||||||
|
- Returns the peer outcomingPeersList (the peers which the peer have connection)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"PeerID": "VOnL-15rFsUiCnRoyGFksKvWKcwNBRz5iarRem0Ilvo=",
|
||||||
|
"peerslist": [
|
||||||
|
{
|
||||||
|
"id": "VOnL-15rFsUiCnRoyGFksKvWKcwNBRz5iarRem0Ilvo=",
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"port": "3000",
|
||||||
|
"role": "server",
|
||||||
|
"conn": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Lk9jEP1YcOAzl51yY61GdWADNe35_g5Teh12JeguHhA=",
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"port": "3003",
|
||||||
|
"role": "client",
|
||||||
|
"conn": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xj78wuyN2_thFBsXOUXnwij4L8vualxQ9GnVRK6RS4c=",
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"port": "3005",
|
||||||
|
"role": "client",
|
||||||
|
"conn": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"date": "0001-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST /register
|
||||||
|
- Adds the address (pubK of the user) to the blockchain
|
||||||
|
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
- When a peer connects to the network, sends his last Block, and receives the new Blocks from this last Block --> DONE with REST petitions, maybe is better with tcp conn
|
||||||
|
- Delete the peer from the peers list when the connection is closed --> DONE
|
||||||
|
- REST:
|
||||||
|
- endpoint to get if the address is in the blockchain (to verify users)
|
||||||
|
- parameters Date or LastUpdate on the structs needs to be updated values
|
||||||
|
- implement rsa encryption between peers
|
||||||
|
- store blockchain in a .data file
|
||||||
20
peer/blockchainlib/block.go
Normal file
20
peer/blockchainlib/block.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package blockchainlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HashBlock(b Block) string {
|
||||||
|
blockJson, err := json.Marshal(b)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
blockString := string(blockJson)
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(blockString))
|
||||||
|
return base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
155
peer/blockchainlib/blockchain.go
Normal file
155
peer/blockchainlib/blockchain.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package blockchainlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Block struct {
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Height int64 `json:"height"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
PreviousHash string `json:"previoushash"`
|
||||||
|
NextHash string `json:"nexthash"`
|
||||||
|
Data []string `json:"data"`
|
||||||
|
Emitter string `json:"emitter"` //the ID of the peer that has emmited the block
|
||||||
|
}
|
||||||
|
|
||||||
|
type Blockchain struct {
|
||||||
|
GenesisBlock string `json:"genesisblock"`
|
||||||
|
LastUpdate time.Time `json:"lastupdate"`
|
||||||
|
Blocks []Block `json:"blocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//var blockchain Blockchain
|
||||||
|
|
||||||
|
func (bc *Blockchain) GetBlockByHash(hash string) (Block, error) {
|
||||||
|
for _, block := range bc.Blocks {
|
||||||
|
if block.Hash == hash {
|
||||||
|
return block, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var b Block
|
||||||
|
return b, errors.New("Block Hash not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Blockchain) CreateBlock(data string) Block {
|
||||||
|
var b Block
|
||||||
|
b.Height = int64(len(bc.Blocks))
|
||||||
|
if len(bc.Blocks) == 0 {
|
||||||
|
b.Height = 0
|
||||||
|
} else {
|
||||||
|
b.PreviousHash = bc.Blocks[len(bc.Blocks)-1].Hash
|
||||||
|
}
|
||||||
|
b.Date = time.Now()
|
||||||
|
b.Data = append(b.Data, data)
|
||||||
|
//b.Emitter = runningPeer.ID
|
||||||
|
|
||||||
|
b.Hash = HashBlock(b)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Blockchain) BlockExists(block Block) bool {
|
||||||
|
for _, b := range bc.Blocks {
|
||||||
|
if b.Hash == block.Hash {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Blockchain) AddBlock(block Block) error {
|
||||||
|
if bc.BlockExists(block) {
|
||||||
|
return errors.New("[Error adding Block]: Block already exists in the Blockchain")
|
||||||
|
}
|
||||||
|
if len(bc.Blocks) > 0 {
|
||||||
|
bc.Blocks[len(bc.Blocks)-1].NextHash = block.Hash
|
||||||
|
} else {
|
||||||
|
bc.GenesisBlock = block.Hash
|
||||||
|
}
|
||||||
|
bc.Blocks = append(bc.Blocks, block)
|
||||||
|
|
||||||
|
bc.SaveToDisk()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Blockchain) ReconstructBlockchainFromBlock(urlAPI string, h string) {
|
||||||
|
color.Yellow("reconstructing the blockchain from last block in memory")
|
||||||
|
var block Block
|
||||||
|
var err error
|
||||||
|
|
||||||
|
block, err = bc.GetBlockByHash(h)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
if h == "" {
|
||||||
|
//no genesis block yet
|
||||||
|
color.Green(urlAPI + "/blocks/genesis")
|
||||||
|
res, err := http.Get(urlAPI + "/blocks/genesis")
|
||||||
|
check(err)
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
check(err)
|
||||||
|
err = json.Unmarshal(body, &block)
|
||||||
|
check(err)
|
||||||
|
color.Yellow("[New Block]: " + block.Hash)
|
||||||
|
err = bc.AddBlock(block)
|
||||||
|
check(err)
|
||||||
|
} else {
|
||||||
|
block.NextHash = h
|
||||||
|
}
|
||||||
|
|
||||||
|
for block.NextHash != "" && block.Hash != "" {
|
||||||
|
res, err := http.Get(urlAPI + "/blocks/next/" + block.Hash)
|
||||||
|
check(err)
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
check(err)
|
||||||
|
err = json.Unmarshal(body, &block)
|
||||||
|
check(err)
|
||||||
|
if block.Hash != "" {
|
||||||
|
color.Yellow("[New Block]: " + block.Hash)
|
||||||
|
err = bc.AddBlock(block)
|
||||||
|
check(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bc.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Blockchain) Print() {
|
||||||
|
color.Green("Printing Blockchain stored in memory")
|
||||||
|
color.Green("Genesis Block: " + bc.GenesisBlock)
|
||||||
|
for _, b := range bc.Blocks {
|
||||||
|
color.Green("Block height:")
|
||||||
|
fmt.Println(b.Height)
|
||||||
|
color.Green("Hash: " + b.Hash)
|
||||||
|
color.Green("Date: " + b.Date.String())
|
||||||
|
color.Green("---")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Blockchain) ReadFromDisk() error {
|
||||||
|
file, err := ioutil.ReadFile("blockchain.data")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content := string(file)
|
||||||
|
json.Unmarshal([]byte(content), &bc)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Blockchain) SaveToDisk() error {
|
||||||
|
bytesBlockchain, err := json.Marshal(bc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile("blockchain.data", bytesBlockchain, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
15
peer/blockchainlib/errors.go
Normal file
15
peer/blockchainlib/errors.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package blockchainlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func check(err error) {
|
||||||
|
if err != nil {
|
||||||
|
_, fn, line, _ := runtime.Caller(1)
|
||||||
|
log.Println(line)
|
||||||
|
log.Println(fn)
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
peer/config.json
Executable file
6
peer/config.json
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"serverip": "127.0.0.1",
|
||||||
|
"serverport": "3000",
|
||||||
|
"serverrestport": "3002"
|
||||||
|
}
|
||||||
15
peer/errors.go
Executable file
15
peer/errors.go
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func check(err error) {
|
||||||
|
if err != nil {
|
||||||
|
_, fn, line, _ := runtime.Caller(1)
|
||||||
|
log.Println(line)
|
||||||
|
log.Println(fn)
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
24
peer/log.go
Executable file
24
peer/log.go
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func savelog() {
|
||||||
|
timeS := time.Now().String()
|
||||||
|
_ = os.Mkdir("logs", os.ModePerm)
|
||||||
|
//next 3 lines are to avoid windows filesystem errors
|
||||||
|
timeS = strings.Replace(timeS, " ", "_", -1)
|
||||||
|
timeS = strings.Replace(timeS, ".", "-", -1)
|
||||||
|
timeS = strings.Replace(timeS, ":", "-", -1)
|
||||||
|
logFile, err := os.OpenFile("logs/log-"+timeS+".log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
mw := io.MultiWriter(os.Stdout, logFile)
|
||||||
|
log.SetOutput(mw)
|
||||||
|
}
|
||||||
48
peer/main.go
Normal file
48
peer/main.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
blockchainlib "./blockchainlib"
|
||||||
|
p2plib "./p2plib"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tp p2plib.ThisPeer
|
||||||
|
var blockchain blockchainlib.Blockchain
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
color.Red("need to call:")
|
||||||
|
color.Red("./peer client 3001 3002")
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
color.Blue("Starting Peer")
|
||||||
|
//read configuration file
|
||||||
|
readConfig("config.json")
|
||||||
|
|
||||||
|
//read the stored blockchain
|
||||||
|
err := blockchain.ReadFromDisk()
|
||||||
|
check(err)
|
||||||
|
blockchain.Print()
|
||||||
|
|
||||||
|
tp = p2plib.InitializePeer(os.Args[1], "127.0.0.1",
|
||||||
|
os.Args[2], os.Args[3], config.ServerIP, config.ServerPort)
|
||||||
|
|
||||||
|
if tp.RunningPeer.Role == "client" {
|
||||||
|
color.Red("http://" + config.IP + ":" + config.ServerRESTPort)
|
||||||
|
fmt.Println(blockchain.GenesisBlock)
|
||||||
|
blockchain.ReconstructBlockchainFromBlock("http://"+config.IP+":"+config.ServerRESTPort, blockchain.GenesisBlock)
|
||||||
|
}
|
||||||
|
color.Blue("initialized")
|
||||||
|
go runRestServer()
|
||||||
|
|
||||||
|
fmt.Println(tp.Running)
|
||||||
|
for tp.Running {
|
||||||
|
time.Sleep(1000 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
81
peer/p2plib/connections.go
Normal file
81
peer/p2plib/connections.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package p2plib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (tp *ThisPeer) AcceptPeers(peer Peer) {
|
||||||
|
fmt.Println("accepting peers at: " + peer.Port)
|
||||||
|
l, err := net.Listen("tcp", peer.IP+":"+peer.Port)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error accepting peers. Listening port: " + peer.Port)
|
||||||
|
tp.Running = false
|
||||||
|
}
|
||||||
|
for tp.Running {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error accepting peers. Error accepting connection")
|
||||||
|
tp.Running = false
|
||||||
|
}
|
||||||
|
var newPeer Peer
|
||||||
|
newPeer.IP = GetIPFromConn(conn)
|
||||||
|
newPeer.Port = GetPortFromConn(conn)
|
||||||
|
newPeer.Conn = conn
|
||||||
|
globalTP.PeersConnections.Incoming = AppendPeerIfNoExist(globalTP.PeersConnections.Incoming, newPeer)
|
||||||
|
go HandleConn(conn, newPeer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func ConnectToPeer(peer Peer) {
|
||||||
|
color.Green("connecting to new peer")
|
||||||
|
log.Println("Connecting to new peer: " + peer.IP + ":" + peer.Port)
|
||||||
|
conn, err := net.Dial("tcp", peer.IP+":"+peer.Port)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error connecting to: " + peer.IP + ":" + peer.Port)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peer.Conn = conn
|
||||||
|
globalTP.PeersConnections.Outcoming = AppendPeerIfNoExist(globalTP.PeersConnections.Outcoming, peer)
|
||||||
|
go HandleConn(conn, peer)
|
||||||
|
}
|
||||||
|
func HandleConn(conn net.Conn, connPeer Peer) {
|
||||||
|
connRunning := true
|
||||||
|
log.Println("handling conn: " + conn.RemoteAddr().String())
|
||||||
|
//reply to the conn, send the peerList
|
||||||
|
var msg Msg
|
||||||
|
msg.Construct("PeersList", "here my outcomingPeersList")
|
||||||
|
msg.PeersList = globalTP.PeersConnections.Outcoming
|
||||||
|
msgB := msg.ToBytes()
|
||||||
|
_, err := conn.Write(msgB)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for connRunning {
|
||||||
|
/*
|
||||||
|
buffer := make([]byte, 1024)
|
||||||
|
bytesRead, err := conn.Read(buffer)
|
||||||
|
*/
|
||||||
|
newmsg, err := bufio.NewReader(conn).ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
connRunning = false
|
||||||
|
} else {
|
||||||
|
var msg Msg
|
||||||
|
//msg = msg.createFromBytes([]byte(string(buffer[0:bytesRead])))
|
||||||
|
msg = msg.CreateFromBytes([]byte(newmsg))
|
||||||
|
MessageHandler(connPeer, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//TODO add that if the peer closed is the p2p server, show a warning message at the peer
|
||||||
|
log.Println("Peer: " + conn.RemoteAddr().String() + " connection closed")
|
||||||
|
conn.Close()
|
||||||
|
//TODO delete the peer from the outcomingPeersList --> DONE
|
||||||
|
DeletePeerFromPeersList(connPeer, &globalTP.PeersConnections.Outcoming)
|
||||||
|
/*color.Yellow("peer deleted, current peerList:")
|
||||||
|
PrintPeersList()*/
|
||||||
|
}
|
||||||
15
peer/p2plib/errors.go
Normal file
15
peer/p2plib/errors.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package p2plib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func check(err error) {
|
||||||
|
if err != nil {
|
||||||
|
_, fn, line, _ := runtime.Caller(1)
|
||||||
|
log.Println(line)
|
||||||
|
log.Println(fn)
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
peer/p2plib/init.go
Normal file
43
peer/p2plib/init.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package p2plib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitializePeer(role, ip, port, restport, serverip, serverport string) ThisPeer {
|
||||||
|
//initialize some vars
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
|
||||||
|
var tp ThisPeer
|
||||||
|
tp.Running = true
|
||||||
|
tp.RunningPeer.Role = role
|
||||||
|
tp.RunningPeer.Port = port
|
||||||
|
tp.RunningPeer.RESTPort = restport
|
||||||
|
tp.RunningPeer.ID = HashPeer(tp.RunningPeer)
|
||||||
|
|
||||||
|
tp.ID = tp.RunningPeer.ID
|
||||||
|
globalTP.PeersConnections.Outcoming.PeerID = tp.RunningPeer.ID
|
||||||
|
fmt.Println(tp.RunningPeer)
|
||||||
|
//outcomingPeersList.Peers = append(outcomingPeersList.Peers, peer.RunningPeer)
|
||||||
|
globalTP.PeersConnections.Outcoming = AppendPeerIfNoExist(globalTP.PeersConnections.Outcoming, tp.RunningPeer)
|
||||||
|
fmt.Println(globalTP.PeersConnections.Outcoming)
|
||||||
|
|
||||||
|
if tp.RunningPeer.Role == "server" {
|
||||||
|
go tp.AcceptPeers(tp.RunningPeer)
|
||||||
|
}
|
||||||
|
if tp.RunningPeer.Role == "client" {
|
||||||
|
var serverPeer Peer
|
||||||
|
serverPeer.IP = serverip
|
||||||
|
serverPeer.Port = serverport
|
||||||
|
serverPeer.Role = "server"
|
||||||
|
serverPeer.ID = HashPeer(serverPeer)
|
||||||
|
go tp.AcceptPeers(tp.RunningPeer)
|
||||||
|
ConnectToPeer(serverPeer)
|
||||||
|
}
|
||||||
|
globalTP = tp
|
||||||
|
|
||||||
|
return tp
|
||||||
|
|
||||||
|
}
|
||||||
78
peer/p2plib/messages.go
Normal file
78
peer/p2plib/messages.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package p2plib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Msg struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
PeersList PeersList `json:"peerslist"`
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MessageHandler(peer Peer, msg Msg) {
|
||||||
|
|
||||||
|
log.Println("[New msg]")
|
||||||
|
log.Println(msg)
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
case "Hi":
|
||||||
|
color.Yellow(msg.Type)
|
||||||
|
color.Green(msg.Content)
|
||||||
|
break
|
||||||
|
case "PeersList":
|
||||||
|
color.Blue("newPeerslist")
|
||||||
|
fmt.Println(msg.PeersList)
|
||||||
|
color.Red("PeersList")
|
||||||
|
|
||||||
|
UpdateNetworkPeersList(peer.Conn, msg.PeersList)
|
||||||
|
PropagatePeersList(peer)
|
||||||
|
PrintPeersList()
|
||||||
|
break
|
||||||
|
case "PeersList_Response":
|
||||||
|
//for the moment is not beeing used
|
||||||
|
color.Blue("newPeerslist, from PeersList_Response")
|
||||||
|
fmt.Println(msg.PeersList)
|
||||||
|
color.Red("PeersList_Response")
|
||||||
|
|
||||||
|
UpdateNetworkPeersList(peer.Conn, msg.PeersList)
|
||||||
|
PropagatePeersList(peer)
|
||||||
|
PrintPeersList()
|
||||||
|
break
|
||||||
|
case "Block":
|
||||||
|
/*//TODO check if the block is signed by an autorized emitter
|
||||||
|
if !blockchain.blockExists(msg.Block) {
|
||||||
|
blockchain.addBlock(msg.Block)
|
||||||
|
propagateBlock(msg.Block)
|
||||||
|
}*/
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
log.Println("Msg.Type not supported")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func (msg *Msg) Construct(msgtype string, msgcontent string) {
|
||||||
|
msg.Type = msgtype
|
||||||
|
msg.Content = msgcontent
|
||||||
|
msg.Date = time.Now()
|
||||||
|
}
|
||||||
|
func (msg Msg) ToBytes() []byte {
|
||||||
|
msgS, err := json.Marshal(msg)
|
||||||
|
check(err)
|
||||||
|
l := string(msgS) + "\n"
|
||||||
|
r := []byte(l)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
func (msg Msg) CreateFromBytes(bytes []byte) Msg {
|
||||||
|
err := json.Unmarshal(bytes, &msg)
|
||||||
|
check(err)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
164
peer/p2plib/peers.go
Normal file
164
peer/p2plib/peers.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package p2plib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Peer struct {
|
||||||
|
ID string `json:"id"` //in the future, this will be the peer hash
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
RESTPort string `json:"restport"`
|
||||||
|
Role string `json:"role"` //client or server
|
||||||
|
Conn net.Conn `json:"conn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeersList struct {
|
||||||
|
PeerID string
|
||||||
|
Peers []Peer `json:"peerslist"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeersConnections struct {
|
||||||
|
Incoming PeersList
|
||||||
|
Outcoming PeersList
|
||||||
|
Network PeersList //the peers that have been received in the lists from other peers
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThisPeer struct {
|
||||||
|
Running bool
|
||||||
|
ID string
|
||||||
|
RunningPeer Peer
|
||||||
|
PeersConnections PeersConnections
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalTP ThisPeer
|
||||||
|
|
||||||
|
func PeerIsInPeersList(p Peer, pl []Peer) int {
|
||||||
|
r := -1
|
||||||
|
for i, peer := range pl {
|
||||||
|
if peer.IP+":"+peer.Port == p.IP+":"+p.Port {
|
||||||
|
r = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeletePeerFromPeersList(p Peer, pl *PeersList) {
|
||||||
|
i := PeerIsInPeersList(p, pl.Peers)
|
||||||
|
if i != -1 {
|
||||||
|
//delete peer from pl.Peers
|
||||||
|
pl.Peers = append(pl.Peers[:i], pl.Peers[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func AppendPeerIfNoExist(pl PeersList, p Peer) PeersList {
|
||||||
|
i := PeerIsInPeersList(p, pl.Peers)
|
||||||
|
if i == -1 {
|
||||||
|
pl.Peers = append(pl.Peers, p)
|
||||||
|
}
|
||||||
|
return pl
|
||||||
|
}
|
||||||
|
func UpdateNetworkPeersList(conn net.Conn, newPeersList PeersList) {
|
||||||
|
for _, peer := range newPeersList.Peers {
|
||||||
|
if GetIPPortFromConn(conn) == peer.IP+":"+peer.Port {
|
||||||
|
peer.ID = newPeersList.PeerID
|
||||||
|
color.Yellow(peer.ID)
|
||||||
|
}
|
||||||
|
i := PeerIsInPeersList(peer, globalTP.PeersConnections.Network.Peers)
|
||||||
|
if i == -1 {
|
||||||
|
globalTP.PeersConnections.Network.Peers = append(globalTP.PeersConnections.Network.Peers, peer)
|
||||||
|
} else {
|
||||||
|
fmt.Println(globalTP.PeersConnections.Network.Peers[i])
|
||||||
|
globalTP.PeersConnections.Network.Peers[i].ID = peer.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func SearchPeerAndUpdate(p Peer) {
|
||||||
|
for _, peer := range globalTP.PeersConnections.Outcoming.Peers {
|
||||||
|
color.Red(p.IP + ":" + p.Port)
|
||||||
|
color.Yellow(peer.IP + ":" + peer.Port)
|
||||||
|
if p.IP+":"+p.Port == peer.IP+":"+peer.Port {
|
||||||
|
peer.ID = p.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//send the outcomingPeersList to all the peers except the peer p that has send the outcomingPeersList
|
||||||
|
func PropagatePeersList(p Peer) {
|
||||||
|
for _, peer := range globalTP.PeersConnections.Network.Peers {
|
||||||
|
if peer.Conn != nil {
|
||||||
|
if peer.ID != p.ID && p.ID != "" {
|
||||||
|
color.Yellow(peer.ID + " - " + p.ID)
|
||||||
|
var msg Msg
|
||||||
|
msg.Construct("PeersList", "here my outcomingPeersList")
|
||||||
|
msg.PeersList = globalTP.PeersConnections.Outcoming
|
||||||
|
msgB := msg.ToBytes()
|
||||||
|
_, err := peer.Conn.Write(msgB)
|
||||||
|
check(err)
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
for the moment, this is not being called, due that in the IncomingPeersList,
|
||||||
|
there is no peer.ID, so in the comparation wih the peer that has send the
|
||||||
|
peersList, is comparing ID with "", so nevere enters this 'else' section
|
||||||
|
|
||||||
|
maybe it's not needed. TODO check if it's needed the PeerList_Response
|
||||||
|
For the moment is working without it
|
||||||
|
*/
|
||||||
|
//to the peer that has sent the peerList, we send our PeersList
|
||||||
|
|
||||||
|
var msg Msg
|
||||||
|
msg.Construct("PeersList_Response", "here my outcomingPeersList")
|
||||||
|
msg.PeersList = globalTP.PeersConnections.Outcoming
|
||||||
|
msgB := msg.ToBytes()
|
||||||
|
_, err := peer.Conn.Write(msgB)
|
||||||
|
check(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//connect to peer
|
||||||
|
if peer.ID != p.ID && peer.ID != globalTP.RunningPeer.ID {
|
||||||
|
if PeerIsInPeersList(peer, globalTP.PeersConnections.Outcoming.Peers) == -1 {
|
||||||
|
color.Red("no connection, connecting to peer: " + peer.Port)
|
||||||
|
ConnectToPeer(peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func PrintPeersList() {
|
||||||
|
fmt.Println("")
|
||||||
|
color.Blue("runningPeer.ID: " + globalTP.RunningPeer.ID)
|
||||||
|
color.Green("OUTCOMING PEERSLIST:")
|
||||||
|
for _, peer := range globalTP.PeersConnections.Outcoming.Peers {
|
||||||
|
fmt.Println(peer)
|
||||||
|
}
|
||||||
|
color.Green("INCOMING PEERSLIST:")
|
||||||
|
for _, peer := range globalTP.PeersConnections.Incoming.Peers {
|
||||||
|
fmt.Println(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
color.Green("NETWORK PEERSLIST:")
|
||||||
|
for _, peer := range globalTP.PeersConnections.Network.Peers {
|
||||||
|
fmt.Println(peer)
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
}
|
||||||
|
|
||||||
|
//send the block to all the peers of the outcomingPeersList
|
||||||
|
/*func PropagateBlock(b Block) {
|
||||||
|
//prepare the msg to send to all connected peers
|
||||||
|
var msg Msg
|
||||||
|
msg.construct("Block", "new block")
|
||||||
|
msg.Block = b
|
||||||
|
msgB := msg.toBytes()
|
||||||
|
for _, peer := range outcomingPeersList.Peers {
|
||||||
|
if peer.Conn != nil {
|
||||||
|
_, err := peer.Conn.Write(msgB)
|
||||||
|
check(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
38
peer/p2plib/utils.go
Normal file
38
peer/p2plib/utils.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package p2plib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetIPPortFromConn(conn net.Conn) string {
|
||||||
|
ip := GetIPFromConn(conn)
|
||||||
|
port := GetPortFromConn(conn)
|
||||||
|
return ip + ":" + port
|
||||||
|
}
|
||||||
|
func GetIPFromConn(conn net.Conn) string {
|
||||||
|
s := conn.RemoteAddr().String()
|
||||||
|
s = strings.Split(s, ":")[0]
|
||||||
|
s = strings.Trim(s, ":")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
func GetPortFromConn(conn net.Conn) string {
|
||||||
|
s := conn.RemoteAddr().String()
|
||||||
|
s = strings.Split(s, ":")[1]
|
||||||
|
s = strings.Trim(s, ":")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
func RandInt(min int, max int) int {
|
||||||
|
r := rand.Intn(max-min) + min
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
func HashPeer(p Peer) string {
|
||||||
|
peerString := p.IP + ":" + p.Port
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(peerString))
|
||||||
|
return base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
26
peer/readConfig.go
Executable file
26
peer/readConfig.go
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Config reads the config
|
||||||
|
type Config struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
RestIP string `json:"restip"`
|
||||||
|
RESTPort string `json:"restport"`
|
||||||
|
ServerIP string `json:"serverip"`
|
||||||
|
ServerPort string `json:"serverport"`
|
||||||
|
ServerRESTPort string `json:"serverrestport"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
func readConfig(path string) {
|
||||||
|
file, err := ioutil.ReadFile(path)
|
||||||
|
check(err)
|
||||||
|
content := string(file)
|
||||||
|
json.Unmarshal([]byte(content), &config)
|
||||||
|
}
|
||||||
47
peer/restConfig.go
Executable file
47
peer/restConfig.go
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Route struct {
|
||||||
|
Name string
|
||||||
|
Method string
|
||||||
|
Pattern string
|
||||||
|
HandlerFunc http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func Logger(inner http.Handler, name string) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
inner.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"%s\t%s\t%s\t%s",
|
||||||
|
r.Method,
|
||||||
|
r.RequestURI,
|
||||||
|
name,
|
||||||
|
time.Since(start),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func NewRouter() *mux.Router {
|
||||||
|
router := mux.NewRouter().StrictSlash(true)
|
||||||
|
for _, route := range routes {
|
||||||
|
var handler http.Handler
|
||||||
|
handler = route.HandlerFunc
|
||||||
|
handler = Logger(handler, route.Name)
|
||||||
|
|
||||||
|
router.
|
||||||
|
Methods(route.Method).
|
||||||
|
Path(route.Pattern).
|
||||||
|
Name(route.Name).
|
||||||
|
Handler(handler)
|
||||||
|
}
|
||||||
|
return router
|
||||||
|
}
|
||||||
141
peer/restRoutes.go
Executable file
141
peer/restRoutes.go
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
blockchainlib "./blockchainlib"
|
||||||
|
p2plib "./p2plib"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Routes []Route
|
||||||
|
|
||||||
|
var routes = Routes{
|
||||||
|
Route{
|
||||||
|
"Index",
|
||||||
|
"GET",
|
||||||
|
"/",
|
||||||
|
Index,
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
"GetPeers",
|
||||||
|
"GET",
|
||||||
|
"/peers",
|
||||||
|
GetPeers,
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
"PostUser",
|
||||||
|
"POST",
|
||||||
|
"/register",
|
||||||
|
PostUser,
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
"GenesisBlock",
|
||||||
|
"GET",
|
||||||
|
"/blocks/genesis",
|
||||||
|
GenesisBlock,
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
"NextBlock",
|
||||||
|
"GET",
|
||||||
|
"/blocks/next/{blockhash}",
|
||||||
|
NextBlock,
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
"LastBlock",
|
||||||
|
"GET",
|
||||||
|
"/blocks/last",
|
||||||
|
LastBlock,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Address struct {
|
||||||
|
Address string `json:"address"` //the pubK of the user, to perform logins
|
||||||
|
}
|
||||||
|
|
||||||
|
func Index(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintln(w, tp.ID)
|
||||||
|
}
|
||||||
|
func GetPeers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
jResp, err := json.Marshal(tp.PeersConnections.Outcoming)
|
||||||
|
check(err)
|
||||||
|
fmt.Fprintln(w, string(jResp))
|
||||||
|
}
|
||||||
|
func PostUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
var address string
|
||||||
|
err := decoder.Decode(&address)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
fmt.Println(address)
|
||||||
|
color.Blue(address)
|
||||||
|
|
||||||
|
//TODO add the verification of the address, to decide if it's accepted to create a new Block
|
||||||
|
block := blockchain.CreateBlock(address)
|
||||||
|
blockchain.AddBlock(block)
|
||||||
|
|
||||||
|
go PropagateBlock(block)
|
||||||
|
|
||||||
|
jResp, err := json.Marshal(blockchain)
|
||||||
|
check(err)
|
||||||
|
fmt.Fprintln(w, string(jResp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func PropagateBlock(b blockchainlib.Block) {
|
||||||
|
//prepare the msg to send to all connected peers
|
||||||
|
var msg p2plib.Msg
|
||||||
|
msg.Construct("Block", "new block")
|
||||||
|
bJson, err := json.Marshal(b)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
msg.Data = []byte(bJson)
|
||||||
|
msgB := msg.ToBytes()
|
||||||
|
for _, peer := range tp.PeersConnections.Outcoming.Peers {
|
||||||
|
if peer.Conn != nil {
|
||||||
|
_, err := peer.Conn.Write(msgB)
|
||||||
|
check(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenesisBlock(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var genesis blockchainlib.Block
|
||||||
|
if len(blockchain.Blocks) >= 0 {
|
||||||
|
genesis = blockchain.Blocks[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
jResp, err := json.Marshal(genesis)
|
||||||
|
check(err)
|
||||||
|
fmt.Fprintln(w, string(jResp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NextBlock(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
blockhash := vars["blockhash"]
|
||||||
|
|
||||||
|
currBlock, err := blockchain.GetBlockByHash(blockhash)
|
||||||
|
check(err)
|
||||||
|
nextBlock, err := blockchain.GetBlockByHash(currBlock.NextHash)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
jResp, err := json.Marshal(nextBlock)
|
||||||
|
check(err)
|
||||||
|
fmt.Fprintln(w, string(jResp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func LastBlock(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var genesis blockchainlib.Block
|
||||||
|
if len(blockchain.Blocks) > 0 {
|
||||||
|
genesis = blockchain.Blocks[len(blockchain.Blocks)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
jResp, err := json.Marshal(genesis)
|
||||||
|
check(err)
|
||||||
|
fmt.Fprintln(w, string(jResp))
|
||||||
|
}
|
||||||
20
peer/restServer.go
Normal file
20
peer/restServer.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runRestServer() {
|
||||||
|
//run API
|
||||||
|
log.Println("server running")
|
||||||
|
log.Print("port: ")
|
||||||
|
log.Println(config.RESTPort)
|
||||||
|
router := NewRouter()
|
||||||
|
headersOk := handlers.AllowedHeaders([]string{"X-Requested-With", "Access-Control-Allow-Origin"})
|
||||||
|
originsOk := handlers.AllowedOrigins([]string{"*"})
|
||||||
|
methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS"})
|
||||||
|
log.Fatal(http.ListenAndServe(":"+config.RESTPort, handlers.CORS(originsOk, headersOk, methodsOk)(router)))
|
||||||
|
}
|
||||||
1
peer/tests.sh
Normal file
1
peer/tests.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
curl -X POST http://127.0.0.1:3002/register -d '{\"address\": \"sampleaddress\"}'
|
||||||
16
runTmuxTestPeers.sh
Normal file
16
runTmuxTestPeers.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
SESSION='peersTest'
|
||||||
|
|
||||||
|
tmux new-session -d -s $SESSION
|
||||||
|
tmux split-window -d -t 0 -v
|
||||||
|
tmux split-window -d -t 1 -h
|
||||||
|
tmux split-window -d -t 0 -h
|
||||||
|
|
||||||
|
tmux send-keys -t 0 'cd peer && go run *.go server 3001 3002' enter
|
||||||
|
sleep 2
|
||||||
|
tmux send-keys -t 1 "curl -X POST http://127.0.0.1:3002/register -d '{\"address\": \"firstaddress\"}'" enter
|
||||||
|
sleep 1
|
||||||
|
tmux send-keys -t 1 'cd peer && go run *.go client 3003 3004' enter
|
||||||
|
tmux send-keys -t 2 'cd peer && go run *.go client 3005 3006' enter
|
||||||
|
tmux send-keys -t 3 'cd peer && go run *.go client 3007 3008' enter
|
||||||
|
|
||||||
|
tmux attach
|
||||||
Reference in New Issue
Block a user