2312 lines
58 KiB
Go
2312 lines
58 KiB
Go
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"crypto/rand"
|
||
"crypto/rsa"
|
||
"crypto/tls"
|
||
"crypto/x509"
|
||
"crypto/x509/pkix"
|
||
"encoding/json"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"io"
|
||
"math/big"
|
||
"net"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/olekukonko/tablewriter"
|
||
)
|
||
|
||
// ====================================================================
|
||
// CONSTANTS & TYPES
|
||
// ====================================================================
|
||
|
||
const (
|
||
Version = "1.0.0"
|
||
CloudflareBaseURL = "https://api.cloudflare.com/client/v4"
|
||
WorkerScript = `/**
|
||
* FlareTunnel - Cloudflare Worker URL Redirection Script
|
||
*/
|
||
addEventListener('fetch', event => {
|
||
event.respondWith(handleRequest(event.request))
|
||
})
|
||
|
||
async function handleRequest(request) {
|
||
try {
|
||
const url = new URL(request.url)
|
||
const targetUrl = getTargetUrl(url, request.headers)
|
||
|
||
if (!targetUrl) {
|
||
return createErrorResponse('No target URL specified', {
|
||
usage: {
|
||
query_param: '?url=https://example.com',
|
||
header: 'X-Target-URL: https://example.com',
|
||
path: '/https://example.com'
|
||
}
|
||
}, 400)
|
||
}
|
||
|
||
let targetURL
|
||
try {
|
||
targetURL = new URL(targetUrl)
|
||
} catch (e) {
|
||
return createErrorResponse('Invalid target URL', { provided: targetUrl }, 400)
|
||
}
|
||
|
||
// Build target URL with filtered query parameters
|
||
const targetParams = new URLSearchParams()
|
||
for (const [key, value] of url.searchParams) {
|
||
if (!['url', '_cb', '_t'].includes(key)) {
|
||
targetParams.append(key, value)
|
||
}
|
||
}
|
||
if (targetParams.toString()) {
|
||
targetURL.search = targetParams.toString()
|
||
}
|
||
|
||
// Create proxied request
|
||
const proxyRequest = createProxyRequest(request, targetURL)
|
||
const response = await fetch(proxyRequest)
|
||
|
||
// Process and return response
|
||
return createProxyResponse(response, request.method)
|
||
|
||
} catch (error) {
|
||
return createErrorResponse('Proxy request failed', {
|
||
message: error.message,
|
||
timestamp: new Date().toISOString()
|
||
}, 500)
|
||
}
|
||
}
|
||
|
||
function getTargetUrl(url, headers) {
|
||
// Priority: query param > header > path
|
||
let targetUrl = url.searchParams.get('url')
|
||
|
||
if (!targetUrl) {
|
||
targetUrl = headers.get('X-Target-URL')
|
||
}
|
||
|
||
if (!targetUrl && url.pathname !== '/') {
|
||
const pathUrl = url.pathname.slice(1)
|
||
if (pathUrl.startsWith('http')) {
|
||
targetUrl = pathUrl
|
||
}
|
||
}
|
||
|
||
return targetUrl
|
||
}
|
||
|
||
function createProxyRequest(request, targetURL) {
|
||
const proxyHeaders = new Headers()
|
||
const allowedHeaders = [
|
||
'accept', 'accept-language', 'accept-encoding', 'authorization',
|
||
'cache-control', 'content-type', 'origin', 'referer', 'user-agent'
|
||
]
|
||
|
||
// Copy allowed headers
|
||
for (const [key, value] of request.headers) {
|
||
if (allowedHeaders.includes(key.toLowerCase())) {
|
||
proxyHeaders.set(key, value)
|
||
}
|
||
}
|
||
|
||
proxyHeaders.set('Host', targetURL.hostname)
|
||
|
||
// Set X-Forwarded-For header
|
||
const customXForwardedFor = request.headers.get('X-My-X-Forwarded-For')
|
||
if (customXForwardedFor) {
|
||
proxyHeaders.set('X-Forwarded-For', customXForwardedFor)
|
||
} else {
|
||
proxyHeaders.set('X-Forwarded-For', generateRandomIP())
|
||
}
|
||
|
||
return new Request(targetURL.toString(), {
|
||
method: request.method,
|
||
headers: proxyHeaders,
|
||
body: ['GET', 'HEAD'].includes(request.method) ? null : request.body
|
||
})
|
||
}
|
||
|
||
function createProxyResponse(response, requestMethod) {
|
||
const responseHeaders = new Headers()
|
||
|
||
// Copy ALL response headers (let browser handle encoding)
|
||
for (const [key, value] of response.headers) {
|
||
responseHeaders.set(key, value)
|
||
}
|
||
|
||
// Add/Override CORS headers
|
||
responseHeaders.set('Access-Control-Allow-Origin', '*')
|
||
responseHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD')
|
||
responseHeaders.set('Access-Control-Allow-Headers', '*')
|
||
|
||
if (requestMethod === 'OPTIONS') {
|
||
return new Response(null, { status: 204, headers: responseHeaders })
|
||
}
|
||
|
||
return new Response(response.body, {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
headers: responseHeaders
|
||
})
|
||
}
|
||
|
||
function createErrorResponse(error, details, status) {
|
||
return new Response(JSON.stringify({ error, ...details }), {
|
||
status,
|
||
headers: { 'Content-Type': 'application/json' }
|
||
})
|
||
}
|
||
|
||
function generateRandomIP() {
|
||
return [1, 2, 3, 4].map(() => Math.floor(Math.random() * 255) + 1).join('.')
|
||
}`
|
||
)
|
||
|
||
// Account represents a Cloudflare account configuration
|
||
type Account struct {
|
||
Name string `json:"name"`
|
||
APIToken string `json:"api_token"`
|
||
AccountID string `json:"account_id"`
|
||
ZoneID string `json:"zone_id,omitempty"`
|
||
}
|
||
|
||
// Config represents the FlareTunnel configuration
|
||
type Config struct {
|
||
Accounts []Account `json:"accounts"`
|
||
}
|
||
|
||
// Worker represents a Cloudflare Worker deployment
|
||
type Worker struct {
|
||
Name string `json:"name"`
|
||
URL string `json:"url"`
|
||
CreatedAt string `json:"created_at"`
|
||
ID string `json:"id"`
|
||
AccountID string `json:"account_id"`
|
||
ConfigAccountName string `json:"config_account_name,omitempty"`
|
||
}
|
||
|
||
// Analytics represents worker analytics data
|
||
type Analytics struct {
|
||
Success bool `json:"success"`
|
||
TotalRequests int `json:"total_requests"`
|
||
PerWorker map[string]int `json:"per_worker"`
|
||
Limit int `json:"limit"`
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
// ====================================================================
|
||
// CLOUDFLARE API CLIENT
|
||
// ====================================================================
|
||
|
||
type CloudflareClient struct {
|
||
APIToken string
|
||
AccountID string
|
||
BaseURL string
|
||
Headers map[string]string
|
||
subdomain string
|
||
}
|
||
|
||
func NewCloudflareClient(apiToken, accountID string) *CloudflareClient {
|
||
return &CloudflareClient{
|
||
APIToken: apiToken,
|
||
AccountID: accountID,
|
||
BaseURL: CloudflareBaseURL,
|
||
Headers: map[string]string{
|
||
"Authorization": "Bearer " + apiToken,
|
||
"Content-Type": "application/json",
|
||
},
|
||
}
|
||
}
|
||
|
||
func (c *CloudflareClient) GetSubdomain() (string, error) {
|
||
if c.subdomain != "" {
|
||
return c.subdomain, nil
|
||
}
|
||
|
||
url := fmt.Sprintf("%s/accounts/%s/workers/subdomain", c.BaseURL, c.AccountID)
|
||
req, err := http.NewRequest("GET", url, nil)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
for k, v := range c.Headers {
|
||
req.Header.Set(k, v)
|
||
}
|
||
|
||
client := &http.Client{Timeout: 30 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
c.subdomain = strings.ToLower(c.AccountID)
|
||
return c.subdomain, nil
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode == 200 {
|
||
var result struct {
|
||
Result struct {
|
||
Subdomain string `json:"subdomain"`
|
||
} `json:"result"`
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err == nil {
|
||
if result.Result.Subdomain != "" {
|
||
c.subdomain = result.Result.Subdomain
|
||
return c.subdomain, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
c.subdomain = strings.ToLower(c.AccountID)
|
||
return c.subdomain, nil
|
||
}
|
||
|
||
func (c *CloudflareClient) CreateWorker(name string) (*Worker, error) {
|
||
if name == "" {
|
||
name = generateWorkerName()
|
||
}
|
||
|
||
url := fmt.Sprintf("%s/accounts/%s/workers/scripts/%s", c.BaseURL, c.AccountID, name)
|
||
|
||
var b bytes.Buffer
|
||
writer := multipartWriter(&b)
|
||
|
||
metadata := map[string]string{
|
||
"body_part": "script",
|
||
"main_module": "worker.js",
|
||
}
|
||
metadataJSON, _ := json.Marshal(metadata)
|
||
|
||
writer.WriteField("metadata", string(metadataJSON))
|
||
writer.WriteField("script", WorkerScript)
|
||
contentType := writer.FormDataContentType()
|
||
writer.Close()
|
||
|
||
req, err := http.NewRequest("PUT", url, &b)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req.Header.Set("Authorization", "Bearer "+c.APIToken)
|
||
req.Header.Set("Content-Type", contentType)
|
||
|
||
client := &http.Client{Timeout: 60 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return nil, fmt.Errorf("failed to create worker: %s", string(body))
|
||
}
|
||
|
||
// Enable subdomain
|
||
subdomain, _ := c.GetSubdomain()
|
||
subdomainURL := fmt.Sprintf("%s/accounts/%s/workers/scripts/%s/subdomain", c.BaseURL, c.AccountID, name)
|
||
subdomainBody := bytes.NewBufferString(`{"enabled":true}`)
|
||
subdomainReq, _ := http.NewRequest("POST", subdomainURL, subdomainBody)
|
||
for k, v := range c.Headers {
|
||
subdomainReq.Header.Set(k, v)
|
||
}
|
||
client.Do(subdomainReq)
|
||
|
||
workerURL := fmt.Sprintf("https://%s.%s.workers.dev", name, subdomain)
|
||
|
||
return &Worker{
|
||
Name: name,
|
||
URL: workerURL,
|
||
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
|
||
ID: name,
|
||
AccountID: c.AccountID,
|
||
}, nil
|
||
}
|
||
|
||
func (c *CloudflareClient) ListWorkers() ([]*Worker, error) {
|
||
url := fmt.Sprintf("%s/accounts/%s/workers/scripts", c.BaseURL, c.AccountID)
|
||
req, err := http.NewRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for k, v := range c.Headers {
|
||
req.Header.Set(k, v)
|
||
}
|
||
|
||
client := &http.Client{Timeout: 30 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
var result struct {
|
||
Result []struct {
|
||
ID string `json:"id"`
|
||
CreatedOn string `json:"created_on"`
|
||
} `json:"result"`
|
||
}
|
||
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
subdomain, _ := c.GetSubdomain()
|
||
workers := []*Worker{}
|
||
|
||
for _, script := range result.Result {
|
||
if strings.HasPrefix(script.ID, "flaretunnel-") {
|
||
workers = append(workers, &Worker{
|
||
Name: script.ID,
|
||
URL: fmt.Sprintf("https://%s.%s.workers.dev", script.ID, subdomain),
|
||
CreatedAt: script.CreatedOn,
|
||
AccountID: c.AccountID,
|
||
})
|
||
}
|
||
}
|
||
|
||
return workers, nil
|
||
}
|
||
|
||
func (c *CloudflareClient) DeleteWorker(name string) error {
|
||
url := fmt.Sprintf("%s/accounts/%s/workers/scripts/%s", c.BaseURL, c.AccountID, name)
|
||
req, err := http.NewRequest("DELETE", url, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for k, v := range c.Headers {
|
||
req.Header.Set(k, v)
|
||
}
|
||
|
||
client := &http.Client{Timeout: 30 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 && resp.StatusCode != 404 {
|
||
return fmt.Errorf("failed to delete worker: status %d", resp.StatusCode)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (c *CloudflareClient) GetAnalytics() (*Analytics, error) {
|
||
graphqlURL := "https://api.cloudflare.com/client/v4/graphql"
|
||
|
||
now := time.Now()
|
||
dateStart := now.Add(-24 * time.Hour).Format("2006-01-02T15:04:05Z")
|
||
dateEnd := now.Format("2006-01-02T15:04:05Z")
|
||
|
||
query := `
|
||
query WorkersAnalytics($accountTag: string!, $datetimeStart: string!, $datetimeEnd: string!) {
|
||
viewer {
|
||
accounts(filter: {accountTag: $accountTag}) {
|
||
workersInvocationsAdaptive(
|
||
limit: 10000
|
||
filter: {
|
||
datetime_geq: $datetimeStart
|
||
datetime_leq: $datetimeEnd
|
||
}
|
||
orderBy: [sum_requests_DESC]
|
||
) {
|
||
dimensions {
|
||
scriptName
|
||
}
|
||
sum {
|
||
requests
|
||
errors
|
||
subrequests
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
`
|
||
|
||
variables := map[string]interface{}{
|
||
"accountTag": c.AccountID,
|
||
"datetimeStart": dateStart,
|
||
"datetimeEnd": dateEnd,
|
||
}
|
||
|
||
payload := map[string]interface{}{
|
||
"query": query,
|
||
"variables": variables,
|
||
}
|
||
|
||
payloadBytes, _ := json.Marshal(payload)
|
||
req, err := http.NewRequest("POST", graphqlURL, bytes.NewBuffer(payloadBytes))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for k, v := range c.Headers {
|
||
req.Header.Set(k, v)
|
||
}
|
||
|
||
client := &http.Client{Timeout: 30 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return &Analytics{Success: false, Limit: 100000, PerWorker: make(map[string]int)}, nil
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 {
|
||
return &Analytics{Success: false, Limit: 100000, PerWorker: make(map[string]int)}, nil
|
||
}
|
||
|
||
var result struct {
|
||
Data struct {
|
||
Viewer struct {
|
||
Accounts []struct {
|
||
WorkersInvocationsAdaptive []struct {
|
||
Dimensions struct {
|
||
ScriptName string `json:"scriptName"`
|
||
} `json:"dimensions"`
|
||
Sum struct {
|
||
Requests int `json:"requests"`
|
||
} `json:"sum"`
|
||
} `json:"workersInvocationsAdaptive"`
|
||
} `json:"accounts"`
|
||
} `json:"viewer"`
|
||
} `json:"data"`
|
||
}
|
||
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
return &Analytics{Success: false, Limit: 100000, PerWorker: make(map[string]int)}, nil
|
||
}
|
||
|
||
analytics := &Analytics{
|
||
Success: true,
|
||
Limit: 100000,
|
||
PerWorker: make(map[string]int),
|
||
}
|
||
|
||
if len(result.Data.Viewer.Accounts) > 0 {
|
||
for _, inv := range result.Data.Viewer.Accounts[0].WorkersInvocationsAdaptive {
|
||
scriptName := inv.Dimensions.ScriptName
|
||
requests := inv.Sum.Requests
|
||
analytics.PerWorker[scriptName] = requests
|
||
analytics.TotalRequests += requests
|
||
}
|
||
}
|
||
|
||
return analytics, nil
|
||
}
|
||
|
||
// ====================================================================
|
||
// SSL CERTIFICATE GENERATION
|
||
// ====================================================================
|
||
|
||
func generateCACert(certPath, keyPath string) error {
|
||
if _, err := os.Stat(certPath); err == nil {
|
||
if _, err := os.Stat(keyPath); err == nil {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
notBefore := time.Now()
|
||
notAfter := notBefore.Add(3650 * 24 * time.Hour)
|
||
|
||
serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||
|
||
template := x509.Certificate{
|
||
SerialNumber: serialNumber,
|
||
Subject: pkix.Name{
|
||
Country: []string{"US"},
|
||
Province: []string{"CA"},
|
||
Locality: []string{"Local"},
|
||
Organization: []string{"FlareTunnel"},
|
||
CommonName: "FlareTunnel CA",
|
||
},
|
||
NotBefore: notBefore,
|
||
NotAfter: notAfter,
|
||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||
BasicConstraintsValid: true,
|
||
IsCA: true,
|
||
}
|
||
|
||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
certOut, err := os.Create(certPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||
certOut.Close()
|
||
|
||
keyOut, err := os.Create(keyPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||
keyOut.Close()
|
||
|
||
fmt.Printf("✓ Generated CA certificate: %s\n", certPath)
|
||
return nil
|
||
}
|
||
|
||
func generateHostCert(hostname, caCertPath, caKeyPath string) (*tls.Certificate, error) {
|
||
caCertPEM, err := os.ReadFile(caCertPath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
block, _ := pem.Decode(caCertPEM)
|
||
caCert, err := x509.ParseCertificate(block.Bytes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
caKeyPEM, err := os.ReadFile(caKeyPath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
keyBlock, _ := pem.Decode(caKeyPEM)
|
||
caKey, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
notBefore := time.Now()
|
||
notAfter := notBefore.Add(365 * 24 * time.Hour)
|
||
|
||
serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||
|
||
template := x509.Certificate{
|
||
SerialNumber: serialNumber,
|
||
Subject: pkix.Name{
|
||
Country: []string{"US"},
|
||
Province: []string{"CA"},
|
||
Locality: []string{"Local"},
|
||
Organization: []string{"FlareTunnel"},
|
||
CommonName: hostname,
|
||
},
|
||
NotBefore: notBefore,
|
||
NotAfter: notAfter,
|
||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||
BasicConstraintsValid: true,
|
||
DNSNames: []string{hostname, "*." + hostname},
|
||
}
|
||
|
||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, &priv.PublicKey, caKey)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||
|
||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &cert, nil
|
||
}
|
||
|
||
// ====================================================================
|
||
// FLARETUNNEL MANAGER
|
||
// ====================================================================
|
||
|
||
type FlareTunnel struct {
|
||
Config *Config
|
||
Clients map[string]*CloudflareClient
|
||
EndpointsFile string
|
||
ConfigFile string
|
||
workers []*Worker
|
||
workersMutex sync.RWMutex
|
||
}
|
||
|
||
func NewFlareTunnel(configFile string) (*FlareTunnel, error) {
|
||
if configFile == "" {
|
||
configFile = "flaretunnel.json"
|
||
}
|
||
|
||
config, err := loadConfig(configFile)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
ft := &FlareTunnel{
|
||
Config: config,
|
||
Clients: make(map[string]*CloudflareClient),
|
||
EndpointsFile: "flaretunnel_endpoints.json",
|
||
ConfigFile: configFile,
|
||
}
|
||
|
||
for _, account := range config.Accounts {
|
||
ft.Clients[account.Name] = NewCloudflareClient(account.APIToken, account.AccountID)
|
||
}
|
||
|
||
return ft, nil
|
||
}
|
||
|
||
func loadConfig(path string) (*Config, error) {
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Remove BOM if present (UTF-8-sig compatibility)
|
||
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
|
||
data = data[3:]
|
||
}
|
||
|
||
var config Config
|
||
if err := json.Unmarshal(data, &config); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &config, nil
|
||
}
|
||
|
||
func (ft *FlareTunnel) SaveEndpoints(workers []*Worker) error {
|
||
data, err := json.MarshalIndent(workers, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
return os.WriteFile(ft.EndpointsFile, data, 0644)
|
||
}
|
||
|
||
func (ft *FlareTunnel) LoadEndpoints() ([]*Worker, error) {
|
||
data, err := os.ReadFile(ft.EndpointsFile)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Remove BOM if present
|
||
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
|
||
data = data[3:]
|
||
}
|
||
|
||
var workers []*Worker
|
||
if err := json.Unmarshal(data, &workers); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return workers, nil
|
||
}
|
||
|
||
func (ft *FlareTunnel) SyncEndpoints() ([]*Worker, error) {
|
||
allWorkers := []*Worker{}
|
||
|
||
for accountName, client := range ft.Clients {
|
||
workers, err := client.ListWorkers()
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
for _, w := range workers {
|
||
w.ConfigAccountName = accountName
|
||
allWorkers = append(allWorkers, w)
|
||
}
|
||
}
|
||
|
||
if len(allWorkers) > 0 {
|
||
ft.SaveEndpoints(allWorkers)
|
||
}
|
||
|
||
return allWorkers, nil
|
||
}
|
||
|
||
func (ft *FlareTunnel) CreateWorkers(count int, accountName string, distribute bool) error {
|
||
fmt.Printf("\nCreating %d FlareTunnel endpoint(s)...\n", count)
|
||
|
||
created := []*Worker{}
|
||
|
||
if accountName != "" {
|
||
// Single account
|
||
client, ok := ft.Clients[accountName]
|
||
if !ok {
|
||
return fmt.Errorf("account '%s' not found", accountName)
|
||
}
|
||
|
||
fmt.Printf(" Using account: %s\n", accountName)
|
||
|
||
for i := 0; i < count; i++ {
|
||
worker, err := client.CreateWorker("")
|
||
if err != nil {
|
||
fmt.Printf(" [%d/%d] Failed: %v\n", i+1, count, err)
|
||
continue
|
||
}
|
||
worker.ConfigAccountName = accountName
|
||
created = append(created, worker)
|
||
fmt.Printf(" [%d/%d] %s -> %s\n", i+1, count, worker.Name, worker.URL)
|
||
}
|
||
} else if distribute && len(ft.Clients) > 1 {
|
||
// Distribute across accounts
|
||
fmt.Printf(" Distribution mode: Checking quotas across %d account(s)...\n", len(ft.Clients))
|
||
|
||
accountQuotas := make(map[string]int)
|
||
for name, client := range ft.Clients {
|
||
analytics, _ := client.GetAnalytics()
|
||
remaining := 100000 - analytics.TotalRequests
|
||
accountQuotas[name] = remaining
|
||
fmt.Printf(" %s: %d requests remaining\n", name, remaining)
|
||
}
|
||
|
||
totalQuota := 0
|
||
for _, quota := range accountQuotas {
|
||
totalQuota += quota
|
||
}
|
||
|
||
if totalQuota == 0 {
|
||
totalQuota = len(ft.Clients)
|
||
for name := range accountQuotas {
|
||
accountQuotas[name] = 1
|
||
}
|
||
}
|
||
|
||
workersPerAccount := make(map[string]int)
|
||
for name, quota := range accountQuotas {
|
||
proportion := float64(quota) / float64(totalQuota)
|
||
workersPerAccount[name] = max(1, int(float64(count)*proportion))
|
||
}
|
||
|
||
// Adjust to exact count
|
||
for sum := sumMap(workersPerAccount); sum < count; sum = sumMap(workersPerAccount) {
|
||
maxAccount := ""
|
||
maxQuota := 0
|
||
for name, quota := range accountQuotas {
|
||
if quota > maxQuota {
|
||
maxQuota = quota
|
||
maxAccount = name
|
||
}
|
||
}
|
||
workersPerAccount[maxAccount]++
|
||
}
|
||
|
||
fmt.Printf("\n Distribution plan:\n")
|
||
for name, wc := range workersPerAccount {
|
||
fmt.Printf(" %s: %d worker(s)\n", name, wc)
|
||
}
|
||
fmt.Println()
|
||
|
||
createdCount := 0
|
||
for name, wc := range workersPerAccount {
|
||
client := ft.Clients[name]
|
||
for i := 0; i < wc; i++ {
|
||
worker, err := client.CreateWorker("")
|
||
if err != nil {
|
||
fmt.Printf(" [%d/%d] [%s] Failed: %v\n", createdCount+1, count, name, err)
|
||
continue
|
||
}
|
||
worker.ConfigAccountName = name
|
||
created = append(created, worker)
|
||
createdCount++
|
||
fmt.Printf(" [%d/%d] [%s] %s -> %s\n", createdCount, count, name, worker.Name, worker.URL)
|
||
}
|
||
}
|
||
} else {
|
||
// Default to first account
|
||
var firstClient *CloudflareClient
|
||
var firstName string
|
||
for name, client := range ft.Clients {
|
||
firstClient = client
|
||
firstName = name
|
||
break
|
||
}
|
||
|
||
fmt.Printf(" Using account: %s\n", firstName)
|
||
|
||
for i := 0; i < count; i++ {
|
||
worker, err := firstClient.CreateWorker("")
|
||
if err != nil {
|
||
fmt.Printf(" [%d/%d] Failed: %v\n", i+1, count, err)
|
||
continue
|
||
}
|
||
worker.ConfigAccountName = firstName
|
||
created = append(created, worker)
|
||
fmt.Printf(" [%d/%d] %s -> %s\n", i+1, count, worker.Name, worker.URL)
|
||
}
|
||
}
|
||
|
||
ft.SyncEndpoints()
|
||
fmt.Printf("\nCreated: %d, Failed: %d\n", len(created), count-len(created))
|
||
|
||
return nil
|
||
}
|
||
|
||
func (ft *FlareTunnel) ListWorkers(verbose, checkStatus bool) error {
|
||
workers, err := ft.SyncEndpoints()
|
||
if err != nil || len(workers) == 0 {
|
||
fmt.Println("❌ No FlareTunnel endpoints found")
|
||
fmt.Println("💡 Create some with: go run FlareTunnel.go create --count 5")
|
||
return nil
|
||
}
|
||
|
||
// Verbose mode: check status as well
|
||
if verbose {
|
||
checkStatus = true // Force status check in verbose mode!
|
||
}
|
||
|
||
// Get analytics for all accounts
|
||
allAnalytics := make(map[string]*Analytics)
|
||
for accountName, client := range ft.Clients {
|
||
analytics, _ := client.GetAnalytics()
|
||
allAnalytics[accountName] = analytics
|
||
}
|
||
|
||
if checkStatus {
|
||
fmt.Printf("\n🔍 Checking status of %d worker(s)...\n", len(workers))
|
||
fmt.Println("This may take a few seconds...\n")
|
||
}
|
||
|
||
fmt.Println()
|
||
table := tablewriter.NewWriter(os.Stdout)
|
||
|
||
// Build header based on flags
|
||
header := []string{"#", "Account", "Name"}
|
||
if verbose {
|
||
header = append(header, "Created", "Age")
|
||
}
|
||
header = append(header, "URL", "Requests")
|
||
if checkStatus {
|
||
header = append(header, "Status (ms)")
|
||
} else {
|
||
header = append(header, "Status")
|
||
}
|
||
table.SetHeader(header)
|
||
table.SetBorder(true)
|
||
|
||
for idx, worker := range workers {
|
||
row := []string{strconv.Itoa(idx), worker.ConfigAccountName, worker.Name}
|
||
|
||
// Add verbose info (Created, Age)
|
||
if verbose {
|
||
createdStr := "Unknown"
|
||
ageStr := "Unknown"
|
||
|
||
// Extract timestamp from worker name: flaretunnel-1764721536-kmoiok
|
||
re := regexp.MustCompile(`flaretunnel-(\d+)-`)
|
||
if matches := re.FindStringSubmatch(worker.Name); len(matches) > 1 {
|
||
if timestamp, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
|
||
createdTime := time.Unix(timestamp, 0)
|
||
createdStr = createdTime.Format("2006-01-02 15:04:05")
|
||
|
||
// Calculate age
|
||
age := time.Since(createdTime)
|
||
if age.Hours() < 1 {
|
||
ageStr = fmt.Sprintf("%dm ago", int(age.Minutes()))
|
||
} else if age.Hours() < 24 {
|
||
ageStr = fmt.Sprintf("%dh ago", int(age.Hours()))
|
||
} else {
|
||
ageStr = fmt.Sprintf("%dd ago", int(age.Hours()/24))
|
||
}
|
||
}
|
||
}
|
||
|
||
row = append(row, createdStr, ageStr)
|
||
}
|
||
|
||
// Add URL
|
||
row = append(row, worker.URL)
|
||
|
||
// Add requests count
|
||
requests := "0"
|
||
if analytics, ok := allAnalytics[worker.ConfigAccountName]; ok && analytics.Success {
|
||
if req, ok := analytics.PerWorker[worker.Name]; ok {
|
||
requests = strconv.Itoa(req)
|
||
}
|
||
}
|
||
row = append(row, requests)
|
||
|
||
// Add status
|
||
if checkStatus {
|
||
// Test actual worker
|
||
testURL := worker.URL + "?url=" + url.QueryEscape("https://httpbin.org/status/200")
|
||
start := time.Now()
|
||
client := &http.Client{Timeout: 5 * time.Second}
|
||
resp, err := client.Get(testURL)
|
||
elapsed := time.Since(start)
|
||
|
||
if err != nil {
|
||
row = append(row, "❌ Failed")
|
||
} else {
|
||
resp.Body.Close()
|
||
if resp.StatusCode == 200 {
|
||
row = append(row, fmt.Sprintf("✅ %dms", elapsed.Milliseconds()))
|
||
} else {
|
||
row = append(row, fmt.Sprintf("⚠️ %d", resp.StatusCode))
|
||
}
|
||
}
|
||
} else {
|
||
row = append(row, "✅ Active")
|
||
}
|
||
|
||
table.Append(row)
|
||
}
|
||
|
||
table.Render()
|
||
|
||
fmt.Println("\n💡 Use worker index with: go run FlareTunnel.go tunnel --workers 0,1,2 --verbose")
|
||
fmt.Println()
|
||
|
||
return nil
|
||
}
|
||
|
||
func (ft *FlareTunnel) CleanupWorkers(accountName string) error {
|
||
if accountName != "" {
|
||
client, ok := ft.Clients[accountName]
|
||
if !ok {
|
||
return fmt.Errorf("account '%s' not found", accountName)
|
||
}
|
||
|
||
fmt.Printf("\n🗑️ Cleaning up account: %s\n", accountName)
|
||
workers, _ := client.ListWorkers()
|
||
|
||
for _, worker := range workers {
|
||
if err := client.DeleteWorker(worker.Name); err != nil {
|
||
fmt.Printf(" ✗ Failed to delete: %s\n", worker.Name)
|
||
} else {
|
||
fmt.Printf(" ✓ Deleted: %s\n", worker.Name)
|
||
}
|
||
}
|
||
} else {
|
||
fmt.Printf("\n🗑️ Cleaning up ALL accounts (%d total)\n", len(ft.Clients))
|
||
|
||
for name, client := range ft.Clients {
|
||
fmt.Printf(" Account: %s\n", name)
|
||
workers, _ := client.ListWorkers()
|
||
|
||
for _, worker := range workers {
|
||
if err := client.DeleteWorker(worker.Name); err != nil {
|
||
fmt.Printf(" ✗ Failed to delete: %s\n", worker.Name)
|
||
} else {
|
||
fmt.Printf(" ✓ Deleted: %s\n", worker.Name)
|
||
}
|
||
}
|
||
}
|
||
|
||
os.Remove(ft.EndpointsFile)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (ft *FlareTunnel) TestWorkers(targetURL, method string) error {
|
||
workers, err := ft.LoadEndpoints()
|
||
if err != nil || len(workers) == 0 {
|
||
fmt.Println("❌ No workers available")
|
||
return fmt.Errorf("no workers found")
|
||
}
|
||
|
||
fmt.Printf("\nTesting %d FlareTunnel endpoint(s) with %s\n", len(workers), targetURL)
|
||
|
||
successCount := 0
|
||
uniqueIPs := make(map[string]bool)
|
||
|
||
for _, worker := range workers {
|
||
fmt.Printf("\nTesting endpoint: %s\n", worker.Name)
|
||
|
||
testURL := worker.URL + "?url=" + url.QueryEscape(targetURL)
|
||
|
||
client := &http.Client{Timeout: 30 * time.Second}
|
||
req, err := http.NewRequest(method, testURL, nil)
|
||
if err != nil {
|
||
fmt.Printf(" ✗ Request failed: %v\n", err)
|
||
continue
|
||
}
|
||
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
fmt.Printf(" ✗ Request failed: %v\n", err)
|
||
continue
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode == 200 {
|
||
successCount++
|
||
fmt.Printf(" ✓ Request successful! Status: %d\n", resp.StatusCode)
|
||
|
||
body, _ := io.ReadAll(resp.Body)
|
||
bodyStr := strings.TrimSpace(string(body))
|
||
|
||
// Try to extract IP from common formats
|
||
if strings.Contains(targetURL, "ifconfig.me") {
|
||
fmt.Printf(" Origin IP: %s\n", bodyStr)
|
||
uniqueIPs[bodyStr] = true
|
||
} else if strings.Contains(targetURL, "httpbin.org/ip") {
|
||
var data map[string]interface{}
|
||
if json.Unmarshal(body, &data) == nil {
|
||
if origin, ok := data["origin"].(string); ok {
|
||
fmt.Printf(" Origin IP: %s\n", origin)
|
||
uniqueIPs[origin] = true
|
||
}
|
||
}
|
||
} else {
|
||
fmt.Printf(" Response Length: %d bytes\n", len(body))
|
||
}
|
||
} else {
|
||
fmt.Printf(" ✗ Request failed! Status: %d\n", resp.StatusCode)
|
||
}
|
||
}
|
||
|
||
fmt.Printf("\nTest Results:\n")
|
||
fmt.Printf(" Working endpoints: %d/%d\n", successCount, len(workers))
|
||
if len(uniqueIPs) > 0 {
|
||
fmt.Printf(" Unique IP addresses: %d\n", len(uniqueIPs))
|
||
for ip := range uniqueIPs {
|
||
fmt.Printf(" - %s\n", ip)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (ft *FlareTunnel) ExportConfig(outputFile string) error {
|
||
if _, err := os.Stat("flaretunnel.json"); os.IsNotExist(err) {
|
||
fmt.Println("❌ No configuration file found (flaretunnel.json)")
|
||
return err
|
||
}
|
||
|
||
configData, err := os.ReadFile("flaretunnel.json")
|
||
if err != nil {
|
||
fmt.Printf("❌ Failed to read configuration: %v\n", err)
|
||
return err
|
||
}
|
||
|
||
exportData := map[string]interface{}{
|
||
"exported_at": time.Now().Format(time.RFC3339),
|
||
"config": json.RawMessage(configData),
|
||
}
|
||
|
||
data, err := json.MarshalIndent(exportData, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := os.WriteFile(outputFile, data, 0644); err != nil {
|
||
fmt.Printf("❌ Failed to write export file: %v\n", err)
|
||
return err
|
||
}
|
||
|
||
var config Config
|
||
json.Unmarshal(configData, &config)
|
||
|
||
fmt.Printf("✅ Exported configuration to %s\n", outputFile)
|
||
fmt.Println("\n📊 Summary:")
|
||
fmt.Printf(" Accounts: %d\n", len(config.Accounts))
|
||
for _, acc := range config.Accounts {
|
||
fmt.Printf(" • %s: %s...\n", acc.Name, acc.AccountID[:min(8, len(acc.AccountID))])
|
||
}
|
||
fmt.Println("\n⚠️ WARNING: This file contains API tokens! Keep it secure! 🔒")
|
||
|
||
return nil
|
||
}
|
||
|
||
func (ft *FlareTunnel) ImportConfig(inputFile string, merge bool) error {
|
||
if _, err := os.Stat(inputFile); os.IsNotExist(err) {
|
||
fmt.Printf("❌ Import file not found: %s\n", inputFile)
|
||
return err
|
||
}
|
||
|
||
importData, err := os.ReadFile(inputFile)
|
||
if err != nil {
|
||
fmt.Printf("❌ Failed to read import file: %v\n", err)
|
||
return err
|
||
}
|
||
|
||
// Remove BOM if present
|
||
if len(importData) >= 3 && importData[0] == 0xEF && importData[1] == 0xBB && importData[2] == 0xBF {
|
||
importData = importData[3:]
|
||
}
|
||
|
||
var importWrapper map[string]interface{}
|
||
var newConfig Config
|
||
|
||
if err := json.Unmarshal(importData, &importWrapper); err != nil {
|
||
fmt.Printf("❌ Failed to parse import file: %v\n", err)
|
||
return err
|
||
}
|
||
|
||
// Check if it's an export format or direct config
|
||
if configData, ok := importWrapper["config"]; ok {
|
||
configBytes, _ := json.Marshal(configData)
|
||
json.Unmarshal(configBytes, &newConfig)
|
||
} else {
|
||
json.Unmarshal(importData, &newConfig)
|
||
}
|
||
|
||
if len(newConfig.Accounts) == 0 {
|
||
fmt.Println("❌ No accounts found in import file")
|
||
return fmt.Errorf("no accounts in import")
|
||
}
|
||
|
||
fmt.Printf("📥 Import file: %s\n", inputFile)
|
||
fmt.Printf(" Accounts: %d\n\n", len(newConfig.Accounts))
|
||
|
||
for idx, acc := range newConfig.Accounts {
|
||
fmt.Printf(" %d. %s: %s...\n", idx+1, acc.Name, acc.AccountID[:min(8, len(acc.AccountID))])
|
||
}
|
||
|
||
var finalConfig Config
|
||
|
||
if merge && fileExists("flaretunnel.json") {
|
||
existingData, _ := os.ReadFile("flaretunnel.json")
|
||
json.Unmarshal(existingData, &finalConfig)
|
||
|
||
existingNames := make(map[string]bool)
|
||
for _, acc := range finalConfig.Accounts {
|
||
existingNames[acc.Name] = true
|
||
}
|
||
|
||
for _, acc := range newConfig.Accounts {
|
||
if !existingNames[acc.Name] {
|
||
finalConfig.Accounts = append(finalConfig.Accounts, acc)
|
||
fmt.Printf("➕ Added account: %s\n", acc.Name)
|
||
} else {
|
||
fmt.Printf("⚠️ Skipped duplicate: %s\n", acc.Name)
|
||
}
|
||
}
|
||
} else {
|
||
finalConfig = newConfig
|
||
}
|
||
|
||
reader := bufio.NewReader(os.Stdin)
|
||
fmt.Print("\nConfirm import? (y/N): ")
|
||
confirm, _ := reader.ReadString('\n')
|
||
|
||
if strings.ToLower(strings.TrimSpace(confirm)) != "y" {
|
||
fmt.Println("Import cancelled.")
|
||
return nil
|
||
}
|
||
|
||
data, _ := json.MarshalIndent(finalConfig, "", " ")
|
||
if err := os.WriteFile("flaretunnel.json", data, 0644); err != nil {
|
||
fmt.Printf("❌ Failed to save configuration: %v\n", err)
|
||
return err
|
||
}
|
||
|
||
fmt.Println("\n✅ Configuration imported!")
|
||
fmt.Printf("📊 Total accounts: %d\n", len(finalConfig.Accounts))
|
||
|
||
return nil
|
||
}
|
||
|
||
func min(a, b int) int {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|
||
|
||
func fileExists(path string) bool {
|
||
_, err := os.Stat(path)
|
||
return err == nil
|
||
}
|
||
|
||
// ====================================================================
|
||
// PROXY SERVER
|
||
// ====================================================================
|
||
|
||
type ProxyServer struct {
|
||
Host string
|
||
Port int
|
||
Workers []*Worker
|
||
CurrentWorkerIndex int
|
||
RotationMode string
|
||
Verbose bool
|
||
AllowIPAccess bool
|
||
CACertPath string
|
||
CAKeyPath string
|
||
BlacklistPatterns []string
|
||
InlineBlockPatterns []string
|
||
BlacklistStats map[string]int
|
||
UpstreamProxy string
|
||
UpstreamVerifySSL bool
|
||
CacheCerts bool
|
||
NoSSLIntercept bool
|
||
mutex sync.Mutex
|
||
certCache map[string]*tls.Certificate
|
||
certMutex sync.RWMutex
|
||
}
|
||
|
||
func NewProxyServer(host string, port int) *ProxyServer {
|
||
return &ProxyServer{
|
||
Host: host,
|
||
Port: port,
|
||
RotationMode: "round-robin",
|
||
BlacklistStats: make(map[string]int),
|
||
certCache: make(map[string]*tls.Certificate),
|
||
}
|
||
}
|
||
|
||
func (ps *ProxyServer) LoadWorkers(endpointsFile string, workerIndices []int) error {
|
||
data, err := os.ReadFile(endpointsFile)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var allWorkers []*Worker
|
||
if err := json.Unmarshal(data, &allWorkers); err != nil {
|
||
return err
|
||
}
|
||
|
||
if len(workerIndices) > 0 {
|
||
selected := []*Worker{}
|
||
for _, idx := range workerIndices {
|
||
if idx >= 0 && idx < len(allWorkers) {
|
||
selected = append(selected, allWorkers[idx])
|
||
}
|
||
}
|
||
ps.Workers = selected
|
||
} else {
|
||
ps.Workers = allWorkers
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (ps *ProxyServer) LoadBlacklist(blacklistFile string) error {
|
||
patterns := []string{}
|
||
|
||
// Load from file
|
||
if blacklistFile != "" {
|
||
if _, err := os.Stat(blacklistFile); err == nil {
|
||
file, err := os.Open(blacklistFile)
|
||
if err == nil {
|
||
defer file.Close()
|
||
scanner := bufio.NewScanner(file)
|
||
|
||
for scanner.Scan() {
|
||
line := strings.TrimSpace(scanner.Text())
|
||
if line != "" && !strings.HasPrefix(line, "#") {
|
||
patterns = append(patterns, line)
|
||
}
|
||
}
|
||
|
||
if len(patterns) > 0 {
|
||
fmt.Printf("✅ Loaded %d patterns from %s\n", len(patterns), blacklistFile)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Add inline patterns
|
||
if len(ps.InlineBlockPatterns) > 0 {
|
||
patterns = append(patterns, ps.InlineBlockPatterns...)
|
||
fmt.Printf("✅ Added %d inline patterns\n", len(ps.InlineBlockPatterns))
|
||
}
|
||
|
||
ps.BlacklistPatterns = patterns
|
||
|
||
return nil
|
||
}
|
||
|
||
func (ps *ProxyServer) GetWorkerURL() string {
|
||
if len(ps.Workers) == 0 {
|
||
return ""
|
||
}
|
||
|
||
ps.mutex.Lock()
|
||
defer ps.mutex.Unlock()
|
||
|
||
if ps.RotationMode == "random" {
|
||
return ps.Workers[time.Now().UnixNano()%int64(len(ps.Workers))].URL
|
||
} else if ps.RotationMode == "round-robin" {
|
||
worker := ps.Workers[ps.CurrentWorkerIndex]
|
||
ps.CurrentWorkerIndex = (ps.CurrentWorkerIndex + 1) % len(ps.Workers)
|
||
return worker.URL
|
||
}
|
||
|
||
return ps.Workers[0].URL
|
||
}
|
||
|
||
func (ps *ProxyServer) IsBlacklisted(targetURL string) bool {
|
||
if len(ps.BlacklistPatterns) == 0 {
|
||
return false
|
||
}
|
||
|
||
urlLower := strings.ToLower(targetURL)
|
||
parsedURL, _ := url.Parse(targetURL)
|
||
hostname := ""
|
||
path := ""
|
||
|
||
if parsedURL != nil {
|
||
hostname = strings.ToLower(parsedURL.Hostname())
|
||
path = strings.ToLower(parsedURL.Path)
|
||
}
|
||
|
||
for _, pattern := range ps.BlacklistPatterns {
|
||
patternLower := strings.ToLower(pattern)
|
||
if strings.Contains(hostname, patternLower) ||
|
||
strings.Contains(path, patternLower) ||
|
||
strings.Contains(urlLower, patternLower) ||
|
||
strings.HasSuffix(urlLower, patternLower) {
|
||
|
||
ps.mutex.Lock()
|
||
ps.BlacklistStats[pattern]++
|
||
ps.mutex.Unlock()
|
||
|
||
return true
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
func (ps *ProxyServer) HandleHTTP(w http.ResponseWriter, r *http.Request) {
|
||
targetURL := r.URL.String()
|
||
if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") {
|
||
targetURL = "http://" + r.Host + r.URL.String()
|
||
}
|
||
|
||
if ps.IsBlacklisted(targetURL) {
|
||
if ps.Verbose {
|
||
fmt.Printf("🚫 BLOCKED (blacklist): %s\n", targetURL)
|
||
}
|
||
http.Error(w, "Blacklisted", http.StatusForbidden)
|
||
return
|
||
}
|
||
|
||
parsedURL, _ := url.Parse(targetURL)
|
||
if !ps.AllowIPAccess && isIPAddress(parsedURL.Hostname()) {
|
||
if ps.Verbose {
|
||
fmt.Printf("🛡️ BLOCKED (IP): %s\n", parsedURL.Hostname())
|
||
}
|
||
http.Error(w, "Direct IP access blocked", http.StatusForbidden)
|
||
return
|
||
}
|
||
|
||
workerURL := ps.GetWorkerURL()
|
||
if workerURL == "" {
|
||
http.Error(w, "No workers available", http.StatusServiceUnavailable)
|
||
return
|
||
}
|
||
|
||
proxyURL := workerURL + "?url=" + url.QueryEscape(targetURL)
|
||
|
||
if ps.Verbose {
|
||
fmt.Printf("\n📤 [%s] %s\n", r.Method, targetURL)
|
||
fmt.Printf(" ↓ via Worker: %s\n", workerURL)
|
||
}
|
||
|
||
// Create proxy request
|
||
proxyReq, err := http.NewRequest(r.Method, proxyURL, r.Body)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// Copy headers
|
||
for k, v := range r.Header {
|
||
if !strings.EqualFold(k, "Host") && !strings.EqualFold(k, "Connection") {
|
||
proxyReq.Header[k] = v
|
||
}
|
||
}
|
||
|
||
// Send request with optional upstream proxy
|
||
transport := &http.Transport{
|
||
TLSClientConfig: &tls.Config{InsecureSkipVerify: !ps.UpstreamVerifySSL},
|
||
}
|
||
|
||
if ps.UpstreamProxy != "" {
|
||
proxyURL, err := url.Parse(ps.UpstreamProxy)
|
||
if err == nil {
|
||
transport.Proxy = http.ProxyURL(proxyURL)
|
||
}
|
||
}
|
||
|
||
client := &http.Client{
|
||
Timeout: 30 * time.Second,
|
||
Transport: transport,
|
||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||
return http.ErrUseLastResponse
|
||
},
|
||
}
|
||
|
||
resp, err := client.Do(proxyReq)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// Copy response headers
|
||
for k, v := range resp.Header {
|
||
w.Header()[k] = v
|
||
}
|
||
|
||
w.WriteHeader(resp.StatusCode)
|
||
io.Copy(w, resp.Body)
|
||
|
||
if ps.Verbose {
|
||
status := "✅"
|
||
if resp.StatusCode >= 400 {
|
||
status = "⚠️"
|
||
}
|
||
fmt.Printf(" ↑ %s %d\n", status, resp.StatusCode)
|
||
}
|
||
}
|
||
|
||
func (ps *ProxyServer) HandleCONNECT(w http.ResponseWriter, r *http.Request) {
|
||
if ps.CACertPath == "" {
|
||
http.Error(w, "HTTPS not supported", http.StatusNotImplemented)
|
||
return
|
||
}
|
||
|
||
host := r.Host
|
||
hostname := strings.Split(host, ":")[0]
|
||
|
||
if ps.Verbose {
|
||
fmt.Printf("\n🔒 [CONNECT] %s\n", host)
|
||
}
|
||
|
||
// Get or generate certificate
|
||
ps.certMutex.RLock()
|
||
cert, exists := ps.certCache[hostname]
|
||
ps.certMutex.RUnlock()
|
||
|
||
if !exists {
|
||
newCert, err := generateHostCert(hostname, ps.CACertPath, ps.CAKeyPath)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
cert = newCert
|
||
|
||
ps.certMutex.Lock()
|
||
ps.certCache[hostname] = cert
|
||
ps.certMutex.Unlock()
|
||
}
|
||
|
||
// Send 200 Connection Established
|
||
w.WriteHeader(http.StatusOK)
|
||
|
||
// Hijack connection
|
||
hijacker, ok := w.(http.Hijacker)
|
||
if !ok {
|
||
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
clientConn, _, err := hijacker.Hijack()
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||
return
|
||
}
|
||
defer clientConn.Close()
|
||
|
||
// Wrap with TLS
|
||
tlsConfig := &tls.Config{
|
||
Certificates: []tls.Certificate{*cert},
|
||
}
|
||
|
||
tlsConn := tls.Server(clientConn, tlsConfig)
|
||
defer tlsConn.Close()
|
||
|
||
if err := tlsConn.Handshake(); err != nil {
|
||
if ps.Verbose {
|
||
fmt.Printf("✗ SSL handshake failed: %v\n", err)
|
||
}
|
||
return
|
||
}
|
||
|
||
// Read HTTPS request
|
||
reader := bufio.NewReader(tlsConn)
|
||
req, err := http.ReadRequest(reader)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
targetURL := "https://" + hostname + req.URL.String()
|
||
|
||
if ps.IsBlacklisted(targetURL) {
|
||
if ps.Verbose {
|
||
fmt.Printf(" 🚫 BLOCKED (blacklist): %s\n", targetURL)
|
||
}
|
||
return
|
||
}
|
||
|
||
workerURL := ps.GetWorkerURL()
|
||
if workerURL == "" {
|
||
return
|
||
}
|
||
|
||
proxyURL := workerURL + "?url=" + url.QueryEscape(targetURL)
|
||
|
||
if ps.Verbose {
|
||
fmt.Printf(" 📤 [%s] %s\n", req.Method, targetURL)
|
||
fmt.Printf(" ↓ via Worker: %s\n", workerURL)
|
||
}
|
||
|
||
// Create proxy request
|
||
proxyReq, err := http.NewRequest(req.Method, proxyURL, req.Body)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
for k, v := range req.Header {
|
||
if !strings.EqualFold(k, "Host") && !strings.EqualFold(k, "Connection") {
|
||
proxyReq.Header[k] = v
|
||
}
|
||
}
|
||
|
||
// Setup transport with optional upstream proxy
|
||
transport := &http.Transport{
|
||
TLSClientConfig: &tls.Config{InsecureSkipVerify: !ps.UpstreamVerifySSL},
|
||
}
|
||
|
||
if ps.UpstreamProxy != "" {
|
||
upstreamURL, err := url.Parse(ps.UpstreamProxy)
|
||
if err == nil {
|
||
transport.Proxy = http.ProxyURL(upstreamURL)
|
||
}
|
||
}
|
||
|
||
client := &http.Client{
|
||
Timeout: 30 * time.Second,
|
||
Transport: transport,
|
||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||
return http.ErrUseLastResponse
|
||
},
|
||
}
|
||
|
||
resp, err := client.Do(proxyReq)
|
||
if err != nil {
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// Write response
|
||
tlsConn.Write([]byte(fmt.Sprintf("HTTP/1.1 %d %s\r\n", resp.StatusCode, resp.Status)))
|
||
|
||
for k, v := range resp.Header {
|
||
for _, vv := range v {
|
||
tlsConn.Write([]byte(fmt.Sprintf("%s: %s\r\n", k, vv)))
|
||
}
|
||
}
|
||
|
||
tlsConn.Write([]byte("\r\n"))
|
||
io.Copy(tlsConn, resp.Body)
|
||
|
||
if ps.Verbose {
|
||
status := "✅"
|
||
if resp.StatusCode >= 400 {
|
||
status = "⚠️"
|
||
}
|
||
fmt.Printf(" ↑ %s %d\n", status, resp.StatusCode)
|
||
}
|
||
}
|
||
|
||
func (ps *ProxyServer) Start(blacklistFile string) error {
|
||
// Setup SSL
|
||
ps.CACertPath = "flaretunnel_ca.crt"
|
||
ps.CAKeyPath = "flaretunnel_ca.key"
|
||
|
||
if !ps.NoSSLIntercept {
|
||
if err := generateCACert(ps.CACertPath, ps.CAKeyPath); err != nil {
|
||
fmt.Printf("⚠️ SSL setup failed: %v\n", err)
|
||
ps.CACertPath = ""
|
||
}
|
||
} else {
|
||
ps.CACertPath = ""
|
||
}
|
||
|
||
// Load blacklist
|
||
ps.LoadBlacklist(blacklistFile)
|
||
|
||
fmt.Println("\n" + strings.Repeat("=", 80))
|
||
fmt.Println("🚀 FlareTunnel Tunnel Server Started")
|
||
fmt.Println(strings.Repeat("=", 80))
|
||
fmt.Printf("📡 Listening: %s:%d\n", ps.Host, ps.Port)
|
||
fmt.Printf("⚙️ Workers: %d\n", len(ps.Workers))
|
||
fmt.Printf("🔄 Rotation: %s\n", ps.RotationMode)
|
||
|
||
if ps.NoSSLIntercept {
|
||
fmt.Printf("🔒 SSL/HTTPS: ✗ Disabled by --no-ssl-intercept\n")
|
||
} else if ps.CACertPath != "" {
|
||
fmt.Printf("🔒 SSL/HTTPS: ✓ Enabled (HTTPS CONNECT supported)\n")
|
||
} else {
|
||
fmt.Printf("🔒 SSL/HTTPS: ✗ Disabled\n")
|
||
}
|
||
|
||
if ps.AllowIPAccess {
|
||
fmt.Printf("🛡️ IP Blocking: ❌ Disabled (--unsafe)\n")
|
||
} else {
|
||
fmt.Printf("🛡️ IP Blocking: ✅ Enabled (saves Worker requests)\n")
|
||
}
|
||
|
||
blacklistMsg := fmt.Sprintf("🚫 Blacklist: %d pattern(s)", len(ps.BlacklistPatterns))
|
||
if blacklistFile == "blacklist-minimal.txt" && len(ps.BlacklistPatterns) > 0 {
|
||
blacklistMsg += " ✅ (default: blacklist-minimal.txt)"
|
||
}
|
||
fmt.Println(blacklistMsg)
|
||
|
||
if ps.UpstreamProxy != "" {
|
||
fmt.Printf("🔗 Upstream: %s\n", ps.UpstreamProxy)
|
||
} else {
|
||
fmt.Printf("🔗 Upstream: ❌ None (direct to Workers)\n")
|
||
}
|
||
|
||
if ps.Verbose {
|
||
fmt.Printf("📝 Verbose: ✅ Enabled\n")
|
||
} else {
|
||
fmt.Printf("📝 Verbose: ❌ Disabled\n")
|
||
}
|
||
|
||
fmt.Println("\n📋 Selected Workers:")
|
||
for idx, worker := range ps.Workers {
|
||
workerName := worker.Name
|
||
if len(workerName) > 50 {
|
||
workerName = workerName[:47] + "..."
|
||
}
|
||
fmt.Printf(" [%d] %s\n", idx, workerName)
|
||
}
|
||
|
||
if ps.UpstreamProxy != "" {
|
||
fmt.Println("\n🔀 Request Flow:")
|
||
fmt.Printf(" Client → FlareTunnel:%d → %s → Cloudflare Workers → Target\n", ps.Port, ps.UpstreamProxy)
|
||
}
|
||
|
||
fmt.Println("\n⚙️ Proxy Configuration:")
|
||
fmt.Printf(" HTTP Proxy: %s:%d\n", ps.Host, ps.Port)
|
||
fmt.Printf(" HTTPS Proxy: %s:%d\n", ps.Host, ps.Port)
|
||
|
||
if ps.CACertPath != "" {
|
||
fmt.Println("\n🔐 For HTTPS without warnings, install CA certificate:")
|
||
fmt.Printf(" 📄 File: %s\n", ps.CACertPath)
|
||
fmt.Printf(" 💡 Or use verify=False in code\n")
|
||
}
|
||
|
||
fmt.Println("\n💡 Tips:")
|
||
if len(ps.BlacklistPatterns) > 0 {
|
||
fmt.Println(" • Blacklist active - saving Worker requests! 💰")
|
||
} else {
|
||
fmt.Println(" • No blacklist - use --blacklist blacklist-minimal.txt to save requests")
|
||
}
|
||
if !ps.AllowIPAccess {
|
||
fmt.Println(" • IP blocking active - Cloudflare doesn't support IPs anyway")
|
||
}
|
||
fmt.Println(" • Press Ctrl+C to stop")
|
||
fmt.Println(strings.Repeat("=", 80))
|
||
fmt.Println()
|
||
|
||
// Start server
|
||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method == http.MethodConnect {
|
||
ps.HandleCONNECT(w, r)
|
||
} else {
|
||
ps.HandleHTTP(w, r)
|
||
}
|
||
})
|
||
|
||
server := &http.Server{
|
||
Addr: fmt.Sprintf("%s:%d", ps.Host, ps.Port),
|
||
Handler: handler,
|
||
}
|
||
|
||
return server.ListenAndServe()
|
||
}
|
||
|
||
// ====================================================================
|
||
// UTILITY FUNCTIONS
|
||
// ====================================================================
|
||
|
||
func generateWorkerName() string {
|
||
timestamp := time.Now().Unix()
|
||
suffix := randomString(6)
|
||
return fmt.Sprintf("flaretunnel-%d-%s", timestamp, suffix)
|
||
}
|
||
|
||
func randomString(n int) string {
|
||
const letters = "abcdefghijklmnopqrstuvwxyz"
|
||
b := make([]byte, n)
|
||
for i := range b {
|
||
b[i] = letters[time.Now().UnixNano()%int64(len(letters))]
|
||
time.Sleep(1 * time.Nanosecond)
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
func isIPAddress(hostname string) bool {
|
||
host := strings.Split(hostname, ":")[0]
|
||
ip := net.ParseIP(host)
|
||
return ip != nil
|
||
}
|
||
|
||
func testWorker(workerURL string) bool {
|
||
testURL := workerURL + "?url=" + url.QueryEscape("https://httpbin.org/status/200")
|
||
client := &http.Client{Timeout: 5 * time.Second}
|
||
resp, err := client.Get(testURL)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
defer resp.Body.Close()
|
||
return resp.StatusCode == 200
|
||
}
|
||
|
||
func max(a, b int) int {
|
||
if a > b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|
||
|
||
func sumMap(m map[string]int) int {
|
||
sum := 0
|
||
for _, v := range m {
|
||
sum += v
|
||
}
|
||
return sum
|
||
}
|
||
|
||
// Simple multipart writer
|
||
type simpleMultipartWriter struct {
|
||
buf *bytes.Buffer
|
||
boundary string
|
||
}
|
||
|
||
func multipartWriter(buf *bytes.Buffer) *simpleMultipartWriter {
|
||
return &simpleMultipartWriter{
|
||
buf: buf,
|
||
boundary: "----FlareTunnelBoundary" + randomString(16),
|
||
}
|
||
}
|
||
|
||
func (w *simpleMultipartWriter) WriteField(field, value string) {
|
||
w.buf.WriteString("--" + w.boundary + "\r\n")
|
||
w.buf.WriteString(fmt.Sprintf("Content-Disposition: form-data; name=\"%s\"\r\n\r\n", field))
|
||
w.buf.WriteString(value + "\r\n")
|
||
}
|
||
|
||
func (w *simpleMultipartWriter) Close() {
|
||
w.buf.WriteString("--" + w.boundary + "--\r\n")
|
||
}
|
||
|
||
func (w *simpleMultipartWriter) FormDataContentType() string {
|
||
return "multipart/form-data; boundary=" + w.boundary
|
||
}
|
||
|
||
// ====================================================================
|
||
// CLI COMMANDS
|
||
// ====================================================================
|
||
|
||
func setupConfig() error {
|
||
fmt.Println(strings.Repeat("=", 70))
|
||
fmt.Println("🔧 FlareTunnel Multi-Account Configuration")
|
||
fmt.Println(strings.Repeat("=", 70))
|
||
fmt.Println()
|
||
fmt.Println("Getting Cloudflare Credentials:")
|
||
fmt.Println("1. Sign up at https://cloudflare.com")
|
||
fmt.Println("2. Go to https://dash.cloudflare.com/profile/api-tokens")
|
||
fmt.Println("3. Click Create Token - use 'Edit Cloudflare Workers' template")
|
||
fmt.Println("4. Set account and zone resources to all")
|
||
fmt.Println("5. Copy the token and Account ID")
|
||
fmt.Println()
|
||
fmt.Println("💡 Tip: You can add multiple accounts for more quota!")
|
||
fmt.Println(" Each account = 100,000 requests/day")
|
||
fmt.Println()
|
||
|
||
accounts := []Account{}
|
||
reader := bufio.NewReader(os.Stdin)
|
||
|
||
for accountNum := 1; ; accountNum++ {
|
||
fmt.Printf("\n📋 Account #%d\n", accountNum)
|
||
fmt.Println(strings.Repeat("-", 40))
|
||
|
||
fmt.Printf("Account name (e.g., 'main', 'backup') [account-%d]: ", accountNum)
|
||
accountName, _ := reader.ReadString('\n')
|
||
accountName = strings.TrimSpace(accountName)
|
||
if accountName == "" {
|
||
accountName = fmt.Sprintf("account-%d", accountNum)
|
||
}
|
||
|
||
fmt.Print("API token: ")
|
||
apiToken, _ := reader.ReadString('\n')
|
||
apiToken = strings.TrimSpace(apiToken)
|
||
if apiToken == "" {
|
||
if accountNum == 1 {
|
||
fmt.Println("❌ API token is required")
|
||
return fmt.Errorf("no API token provided")
|
||
}
|
||
break
|
||
}
|
||
|
||
fmt.Print("Account ID: ")
|
||
accountID, _ := reader.ReadString('\n')
|
||
accountID = strings.TrimSpace(accountID)
|
||
if accountID == "" {
|
||
if accountNum == 1 {
|
||
fmt.Println("❌ Account ID is required")
|
||
return fmt.Errorf("no account ID provided")
|
||
}
|
||
break
|
||
}
|
||
|
||
accounts = append(accounts, Account{
|
||
Name: accountName,
|
||
APIToken: apiToken,
|
||
AccountID: accountID,
|
||
})
|
||
|
||
fmt.Printf("✅ Account '%s' added!\n", accountName)
|
||
|
||
if accountNum >= 1 {
|
||
fmt.Print("\nAdd another account? (y/N): ")
|
||
addMore, _ := reader.ReadString('\n')
|
||
if strings.ToLower(strings.TrimSpace(addMore)) != "y" {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(accounts) == 0 {
|
||
fmt.Println("❌ No accounts configured")
|
||
return fmt.Errorf("no accounts configured")
|
||
}
|
||
|
||
config := Config{Accounts: accounts}
|
||
data, _ := json.MarshalIndent(config, "", " ")
|
||
|
||
if err := os.WriteFile("flaretunnel.json", data, 0644); err != nil {
|
||
fmt.Printf("❌ Error saving configuration: %v\n", err)
|
||
return err
|
||
}
|
||
|
||
fmt.Println()
|
||
fmt.Println(strings.Repeat("=", 70))
|
||
fmt.Println("✅ Configuration saved!")
|
||
fmt.Println(strings.Repeat("=", 70))
|
||
fmt.Println("📄 Config file: flaretunnel.json")
|
||
fmt.Printf("📊 Accounts configured: %d\n", len(accounts))
|
||
fmt.Printf("🚀 Total daily quota: %d requests\n", len(accounts)*100000)
|
||
fmt.Println()
|
||
|
||
for _, acc := range accounts {
|
||
fmt.Printf(" • %s: %s...\n", acc.Name, acc.AccountID[:8])
|
||
}
|
||
|
||
fmt.Println()
|
||
fmt.Println("🎉 FlareTunnel is now configured!")
|
||
|
||
return nil
|
||
}
|
||
|
||
func parseWorkerIndices(indicesStr string) []int {
|
||
indices := []int{}
|
||
parts := strings.Split(indicesStr, ",")
|
||
|
||
for _, part := range parts {
|
||
part = strings.TrimSpace(part)
|
||
if strings.Contains(part, "-") {
|
||
rangeParts := strings.Split(part, "-")
|
||
if len(rangeParts) == 2 {
|
||
start, err1 := strconv.Atoi(strings.TrimSpace(rangeParts[0]))
|
||
end, err2 := strconv.Atoi(strings.TrimSpace(rangeParts[1]))
|
||
if err1 == nil && err2 == nil {
|
||
for i := start; i <= end; i++ {
|
||
indices = append(indices, i)
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
if idx, err := strconv.Atoi(part); err == nil {
|
||
indices = append(indices, idx)
|
||
}
|
||
}
|
||
}
|
||
|
||
return indices
|
||
}
|
||
|
||
// ====================================================================
|
||
// MAIN
|
||
// ====================================================================
|
||
|
||
func main() {
|
||
if len(os.Args) < 2 {
|
||
printHelp()
|
||
return
|
||
}
|
||
|
||
command := os.Args[1]
|
||
|
||
switch command {
|
||
case "config":
|
||
setupConfig()
|
||
|
||
case "create":
|
||
count := 1
|
||
accountName := ""
|
||
distribute := false
|
||
|
||
for i := 2; i < len(os.Args); i++ {
|
||
switch os.Args[i] {
|
||
case "--count":
|
||
if i+1 < len(os.Args) {
|
||
count, _ = strconv.Atoi(os.Args[i+1])
|
||
i++
|
||
}
|
||
case "--account":
|
||
if i+1 < len(os.Args) {
|
||
accountName = os.Args[i+1]
|
||
i++
|
||
}
|
||
case "--distribute":
|
||
distribute = true
|
||
}
|
||
}
|
||
|
||
ft, err := NewFlareTunnel("flaretunnel.json")
|
||
if err != nil {
|
||
fmt.Printf("❌ Configuration error: %v\n", err)
|
||
fmt.Println("Run: go run FlareTunnel.go config")
|
||
return
|
||
}
|
||
|
||
ft.CreateWorkers(count, accountName, distribute)
|
||
|
||
case "list":
|
||
verbose := false
|
||
checkStatus := false
|
||
|
||
for i := 2; i < len(os.Args); i++ {
|
||
switch os.Args[i] {
|
||
case "--verbose", "-v":
|
||
verbose = true
|
||
case "--status":
|
||
checkStatus = true
|
||
}
|
||
}
|
||
|
||
// If both flags provided, verbose already includes status check
|
||
if verbose && checkStatus {
|
||
fmt.Println("⚠️ Note: --verbose already includes live status check, --status flag is redundant\n")
|
||
// checkStatus is already covered by verbose, no need to set it
|
||
}
|
||
|
||
ft, err := NewFlareTunnel("flaretunnel.json")
|
||
if err != nil {
|
||
fmt.Printf("❌ Configuration error: %v\n", err)
|
||
return
|
||
}
|
||
|
||
ft.ListWorkers(verbose, checkStatus)
|
||
|
||
case "test":
|
||
targetURL := "https://ifconfig.me/ip"
|
||
method := "GET"
|
||
|
||
for i := 2; i < len(os.Args); i++ {
|
||
switch os.Args[i] {
|
||
case "--url":
|
||
if i+1 < len(os.Args) {
|
||
targetURL = os.Args[i+1]
|
||
i++
|
||
}
|
||
case "--method":
|
||
if i+1 < len(os.Args) {
|
||
method = os.Args[i+1]
|
||
i++
|
||
}
|
||
}
|
||
}
|
||
|
||
ft, err := NewFlareTunnel("flaretunnel.json")
|
||
if err != nil {
|
||
fmt.Printf("❌ Configuration error: %v\n", err)
|
||
return
|
||
}
|
||
|
||
ft.TestWorkers(targetURL, method)
|
||
|
||
case "export":
|
||
outputFile := "flaretunnel_config_backup.json"
|
||
|
||
for i := 2; i < len(os.Args); i++ {
|
||
switch os.Args[i] {
|
||
case "--output":
|
||
if i+1 < len(os.Args) {
|
||
outputFile = os.Args[i+1]
|
||
i++
|
||
}
|
||
}
|
||
}
|
||
|
||
ft, _ := NewFlareTunnel("flaretunnel.json")
|
||
ft.ExportConfig(outputFile)
|
||
|
||
case "import":
|
||
inputFile := ""
|
||
merge := false
|
||
|
||
for i := 2; i < len(os.Args); i++ {
|
||
switch os.Args[i] {
|
||
case "--input":
|
||
if i+1 < len(os.Args) {
|
||
inputFile = os.Args[i+1]
|
||
i++
|
||
}
|
||
case "--merge":
|
||
merge = true
|
||
}
|
||
}
|
||
|
||
if inputFile == "" {
|
||
fmt.Println("❌ Error: --input required for import command")
|
||
fmt.Println("Usage: go run FlareTunnel.go import --input config_backup.json [--merge]")
|
||
return
|
||
}
|
||
|
||
ft := &FlareTunnel{}
|
||
ft.ImportConfig(inputFile, merge)
|
||
|
||
case "cleanup":
|
||
accountName := ""
|
||
|
||
for i := 2; i < len(os.Args); i++ {
|
||
switch os.Args[i] {
|
||
case "--account":
|
||
if i+1 < len(os.Args) {
|
||
accountName = os.Args[i+1]
|
||
i++
|
||
}
|
||
}
|
||
}
|
||
|
||
ft, err := NewFlareTunnel("flaretunnel.json")
|
||
if err != nil {
|
||
fmt.Printf("❌ Configuration error: %v\n", err)
|
||
return
|
||
}
|
||
|
||
reader := bufio.NewReader(os.Stdin)
|
||
if accountName != "" {
|
||
fmt.Printf("Delete ALL workers from account '%s'? (y/N): ", accountName)
|
||
} else {
|
||
fmt.Print("Delete ALL FlareTunnel endpoints from ALL accounts? (y/N): ")
|
||
}
|
||
|
||
confirm, _ := reader.ReadString('\n')
|
||
if strings.ToLower(strings.TrimSpace(confirm)) == "y" {
|
||
ft.CleanupWorkers(accountName)
|
||
} else {
|
||
fmt.Println("Cleanup cancelled.")
|
||
}
|
||
|
||
case "tunnel":
|
||
host := "127.0.0.1"
|
||
port := 8080
|
||
workersStr := ""
|
||
mode := "round-robin"
|
||
verbose := false
|
||
unsafe := false
|
||
blacklist := "blacklist-minimal.txt"
|
||
upstreamProxy := ""
|
||
upstreamVerifySSL := false
|
||
cacheCerts := false
|
||
noSSLIntercept := false
|
||
blockPatterns := []string{}
|
||
|
||
for i := 2; i < len(os.Args); i++ {
|
||
switch os.Args[i] {
|
||
case "--host":
|
||
if i+1 < len(os.Args) {
|
||
host = os.Args[i+1]
|
||
i++
|
||
}
|
||
case "--port":
|
||
if i+1 < len(os.Args) {
|
||
port, _ = strconv.Atoi(os.Args[i+1])
|
||
i++
|
||
}
|
||
case "--workers":
|
||
if i+1 < len(os.Args) {
|
||
workersStr = os.Args[i+1]
|
||
i++
|
||
}
|
||
case "--mode":
|
||
if i+1 < len(os.Args) {
|
||
mode = os.Args[i+1]
|
||
i++
|
||
}
|
||
case "--verbose", "-v":
|
||
verbose = true
|
||
case "--unsafe":
|
||
unsafe = true
|
||
case "--blacklist":
|
||
if i+1 < len(os.Args) {
|
||
blacklist = os.Args[i+1]
|
||
i++
|
||
}
|
||
case "--upstream-proxy":
|
||
if i+1 < len(os.Args) {
|
||
upstreamProxy = os.Args[i+1]
|
||
i++
|
||
}
|
||
case "--upstream-verify-ssl":
|
||
upstreamVerifySSL = true
|
||
case "--cache-certs":
|
||
cacheCerts = true
|
||
case "--no-ssl-intercept":
|
||
noSSLIntercept = true
|
||
case "--block":
|
||
if i+1 < len(os.Args) {
|
||
blockPatterns = append(blockPatterns, os.Args[i+1])
|
||
i++
|
||
}
|
||
}
|
||
}
|
||
|
||
ps := NewProxyServer(host, port)
|
||
ps.RotationMode = mode
|
||
ps.Verbose = verbose
|
||
ps.AllowIPAccess = unsafe
|
||
ps.UpstreamProxy = upstreamProxy
|
||
ps.UpstreamVerifySSL = upstreamVerifySSL
|
||
ps.CacheCerts = cacheCerts
|
||
ps.NoSSLIntercept = noSSLIntercept
|
||
|
||
// Add inline block patterns
|
||
if len(blockPatterns) > 0 {
|
||
ps.InlineBlockPatterns = blockPatterns
|
||
}
|
||
|
||
var workerIndices []int
|
||
if workersStr != "" {
|
||
workerIndices = parseWorkerIndices(workersStr)
|
||
}
|
||
|
||
if err := ps.LoadWorkers("flaretunnel_endpoints.json", workerIndices); err != nil {
|
||
fmt.Printf("❌ Failed to load workers: %v\n", err)
|
||
fmt.Println("Run: go run FlareTunnel.go create --count 5")
|
||
return
|
||
}
|
||
|
||
if len(ps.Workers) == 0 {
|
||
fmt.Println("❌ No workers available")
|
||
return
|
||
}
|
||
|
||
ps.Start(blacklist)
|
||
|
||
default:
|
||
printHelp()
|
||
}
|
||
}
|
||
|
||
func printHelp() {
|
||
help := `
|
||
FlareTunnel - Cloudflare Workers Proxy System (Go Version)
|
||
|
||
Usage: go run FlareTunnel.go <command> [options]
|
||
|
||
Commands:
|
||
config Configure Cloudflare API credentials (interactive)
|
||
create Create new Cloudflare Workers
|
||
list List all Workers with analytics
|
||
test Test Workers with a target URL
|
||
export Export configuration to backup file
|
||
import Import configuration from backup file
|
||
cleanup Delete Workers (prompts for confirmation)
|
||
tunnel Start local proxy server
|
||
|
||
Examples:
|
||
# Configuration
|
||
go run FlareTunnel.go config
|
||
|
||
# Create Workers
|
||
go run FlareTunnel.go create --count 5
|
||
go run FlareTunnel.go create --count 10 --distribute
|
||
go run FlareTunnel.go create --count 3 --account main
|
||
|
||
# List Workers
|
||
go run FlareTunnel.go list # Basic list
|
||
go run FlareTunnel.go list --verbose # Detailed + live status check
|
||
go run FlareTunnel.go list --status # Only live response times
|
||
|
||
# Test Workers
|
||
go run FlareTunnel.go test
|
||
go run FlareTunnel.go test --url https://httpbin.org/ip
|
||
go run FlareTunnel.go test --url https://example.com --method POST
|
||
|
||
# Export/Import Config
|
||
go run FlareTunnel.go export --output my_backup.json
|
||
go run FlareTunnel.go import --input my_backup.json
|
||
go run FlareTunnel.go import --input config.json --merge
|
||
|
||
# Start Tunnel
|
||
go run FlareTunnel.go tunnel --verbose
|
||
go run FlareTunnel.go tunnel --workers 0,1,2 --mode random
|
||
go run FlareTunnel.go tunnel --port 9090 --blacklist blacklist.txt
|
||
go run FlareTunnel.go tunnel --upstream-proxy http://127.0.0.1:8888
|
||
|
||
# Cleanup
|
||
go run FlareTunnel.go cleanup
|
||
go run FlareTunnel.go cleanup --account main
|
||
|
||
Options:
|
||
Create:
|
||
--count N Number of workers to create (default: 1)
|
||
--account NAME Create on specific account
|
||
--distribute Auto-distribute across accounts based on quota
|
||
|
||
List:
|
||
--verbose, -v Show detailed info + live status check (created, age, response times)
|
||
--status Check only live worker status (response times, no verbose info)
|
||
|
||
Test:
|
||
--url URL Target URL to test (default: https://ifconfig.me/ip)
|
||
--method METHOD HTTP method (default: GET)
|
||
|
||
Export:
|
||
--output FILE Output file (default: flaretunnel_config_backup.json)
|
||
|
||
Import:
|
||
--input FILE Input config backup file (REQUIRED)
|
||
--merge Merge with existing config (skip duplicates)
|
||
|
||
Cleanup:
|
||
--account NAME Delete from specific account only
|
||
|
||
Tunnel:
|
||
--host HOST Bind host (default: 127.0.0.1)
|
||
--port PORT Bind port (default: 8080)
|
||
--workers INDICES Worker indices (e.g., '0,1,2' or '0-2')
|
||
--mode MODE Rotation mode: random, round-robin (default: round-robin)
|
||
--verbose, -v Enable verbose logging
|
||
--unsafe Allow IP access (not recommended)
|
||
--upstream-proxy URL Upstream proxy (e.g., http://127.0.0.1:8080)
|
||
--upstream-verify-ssl Verify SSL for upstream proxy
|
||
--cache-certs Cache SSL certs to disk
|
||
--no-ssl-intercept Disable SSL interception
|
||
--blacklist FILE Blacklist file (default: blacklist-minimal.txt)
|
||
--block PATTERN Block pattern (can be used multiple times)
|
||
`
|
||
fmt.Println(help)
|
||
}
|
||
|