mirror of
https://github.com/anotherhadi/iknowyou.git
synced 2026-04-12 00:47:26 +02:00
add proxy settings
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -9,9 +9,14 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ProxyEntry struct {
|
||||
URL string `yaml:"url" json:"url"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Tools map[string]yaml.Node `yaml:"tools" json:"tools"`
|
||||
Profiles map[string]Profile `yaml:"profiles" json:"profiles"`
|
||||
Proxies []ProxyEntry `yaml:"proxies,omitempty" json:"proxies,omitempty"`
|
||||
Tools map[string]yaml.Node `yaml:"tools" json:"tools"`
|
||||
Profiles map[string]Profile `yaml:"profiles" json:"profiles"`
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
@@ -40,11 +41,18 @@ func (h *ConfigHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
toolConfigs[toolName] = m
|
||||
}
|
||||
}
|
||||
proxies := cfg.Proxies
|
||||
if proxies == nil {
|
||||
proxies = []config.ProxyEntry{}
|
||||
}
|
||||
_, pcErr := exec.LookPath("proxychains4")
|
||||
respond.JSON(w, http.StatusOK, map[string]any{
|
||||
"tools": toolConfigs,
|
||||
"profiles": cfg.Profiles,
|
||||
"readonly": h.demo || config.IsReadonly(h.configPath),
|
||||
"demo": h.demo,
|
||||
"tools": toolConfigs,
|
||||
"profiles": cfg.Profiles,
|
||||
"proxies": proxies,
|
||||
"proxychains_available": pcErr == nil,
|
||||
"readonly": h.demo || config.IsReadonly(h.configPath),
|
||||
"demo": h.demo,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -512,6 +520,39 @@ func (h *ConfigHandler) DeleteProfileToolConfig(w http.ResponseWriter, r *http.R
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// PUT /api/config/proxies
|
||||
func (h *ConfigHandler) UpdateProxies(w http.ResponseWriter, r *http.Request) {
|
||||
if h.demo {
|
||||
respond.Error(w, http.StatusForbidden, "demo mode: modifications are disabled")
|
||||
return
|
||||
}
|
||||
if config.IsReadonly(h.configPath) {
|
||||
respond.Error(w, http.StatusForbidden, "config is read-only")
|
||||
return
|
||||
}
|
||||
|
||||
var proxies []config.ProxyEntry
|
||||
if err := json.NewDecoder(r.Body).Decode(&proxies); err != nil {
|
||||
respond.Error(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
cfg, err := config.Load(h.configPath)
|
||||
if err != nil {
|
||||
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
cfg.Proxies = proxies
|
||||
if err := config.Save(h.configPath, cfg); err != nil {
|
||||
respond.Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
respond.JSON(w, http.StatusOK, proxies)
|
||||
}
|
||||
|
||||
func validateProfileName(name string) error {
|
||||
for _, c := range name {
|
||||
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') {
|
||||
|
||||
@@ -55,6 +55,7 @@ func NewRouter(
|
||||
|
||||
r.Route("/config", func(r chi.Router) {
|
||||
r.Get("/", configHandler.Get)
|
||||
r.Put("/proxies", configHandler.UpdateProxies)
|
||||
|
||||
r.Route("/tools", func(r chi.Router) {
|
||||
r.Patch("/{toolName}", configHandler.UpdateToolConfig)
|
||||
|
||||
154
back/internal/proxy/proxy.go
Normal file
154
back/internal/proxy/proxy.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/anotherhadi/iknowyou/config"
|
||||
)
|
||||
|
||||
type httpClientKey struct{}
|
||||
type proxychainsConfKey struct{}
|
||||
|
||||
// WithClient injects a proxy-aware HTTP client into the context.
|
||||
func WithClient(ctx context.Context, client *http.Client) context.Context {
|
||||
return context.WithValue(ctx, httpClientKey{}, client)
|
||||
}
|
||||
|
||||
// ClientFromContext returns the proxy-aware HTTP client stored in ctx,
|
||||
// or http.DefaultClient if none was set.
|
||||
func ClientFromContext(ctx context.Context) *http.Client {
|
||||
if client, ok := ctx.Value(httpClientKey{}).(*http.Client); ok && client != nil {
|
||||
return client
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
// WithProxychainsConf injects a proxychains config file path into the context.
|
||||
func WithProxychainsConf(ctx context.Context, confPath string) context.Context {
|
||||
return context.WithValue(ctx, proxychainsConfKey{}, confPath)
|
||||
}
|
||||
|
||||
// ProxychainsConfFromContext returns the proxychains config file path stored in
|
||||
// ctx, or an empty string if none was set.
|
||||
func ProxychainsConfFromContext(ctx context.Context) string {
|
||||
if path, ok := ctx.Value(proxychainsConfKey{}).(string); ok {
|
||||
return path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// fallbackTransport is an http.RoundTripper that tries proxies in random order
|
||||
// and falls back to the next one on network error.
|
||||
type fallbackTransport struct {
|
||||
transports []*http.Transport
|
||||
}
|
||||
|
||||
func (t *fallbackTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
order := rand.Perm(len(t.transports))
|
||||
var lastErr error
|
||||
for _, i := range order {
|
||||
resp, err := t.transports[i].RoundTrip(req)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// NewClient builds an *http.Client that routes requests through the given
|
||||
// proxies, trying them in random order and falling back on network error.
|
||||
// Returns nil if proxies is empty.
|
||||
func NewClient(proxies []config.ProxyEntry) (*http.Client, error) {
|
||||
if len(proxies) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
transports := make([]*http.Transport, 0, len(proxies))
|
||||
for _, p := range proxies {
|
||||
u, err := url.Parse(p.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proxy: invalid URL %q: %w", p.URL, err)
|
||||
}
|
||||
transports = append(transports, &http.Transport{
|
||||
Proxy: http.ProxyURL(u),
|
||||
})
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: &fallbackTransport{transports: transports},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WriteProxychainsConf generates a temporary proxychains4 config file from the
|
||||
// given proxy list and returns its path along with a cleanup function.
|
||||
// Returns ("", nil, nil) if proxies is empty.
|
||||
func WriteProxychainsConf(proxies []config.ProxyEntry) (string, func(), error) {
|
||||
if len(proxies) == 0 {
|
||||
return "", func() {}, nil
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("dynamic_chain\nproxy_dns\n\n[ProxyList]\n")
|
||||
|
||||
for _, p := range proxies {
|
||||
u, err := url.Parse(p.URL)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("proxy: invalid URL %q: %w", p.URL, err)
|
||||
}
|
||||
|
||||
scheme := u.Scheme
|
||||
// proxychains only knows socks4, socks5, http
|
||||
if scheme != "socks4" && scheme != "socks5" && scheme != "http" {
|
||||
scheme = "socks5"
|
||||
}
|
||||
|
||||
host := u.Hostname()
|
||||
port := u.Port()
|
||||
if port == "" {
|
||||
port = defaultPort(scheme)
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("%s %s %s", scheme, host, port)
|
||||
if u.User != nil {
|
||||
user := u.User.Username()
|
||||
pass, _ := u.User.Password()
|
||||
if user != "" {
|
||||
line += " " + user
|
||||
if pass != "" {
|
||||
line += " " + pass
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp("", "iky-proxychains-*.conf")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("proxy: create temp conf: %w", err)
|
||||
}
|
||||
if _, err := f.WriteString(sb.String()); err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(f.Name())
|
||||
return "", nil, fmt.Errorf("proxy: write conf: %w", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
path := f.Name()
|
||||
cleanup := func() { _ = os.Remove(path) }
|
||||
return path, cleanup, nil
|
||||
}
|
||||
|
||||
func defaultPort(scheme string) string {
|
||||
switch scheme {
|
||||
case "socks4", "socks5":
|
||||
return "1080"
|
||||
case "http":
|
||||
return "8080"
|
||||
}
|
||||
return "1080"
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/anotherhadi/iknowyou/config"
|
||||
"github.com/anotherhadi/iknowyou/internal/proxy"
|
||||
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||
)
|
||||
|
||||
@@ -66,6 +67,24 @@ func (m *Manager) Start(
|
||||
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
|
||||
// Inject proxy-aware HTTP client into context.
|
||||
if httpClient, err := proxy.NewClient(cfg.Proxies); err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("manager: building proxy client: %w", err)
|
||||
} else if httpClient != nil {
|
||||
ctx = proxy.WithClient(ctx, httpClient)
|
||||
}
|
||||
|
||||
// Generate proxychains config for external binary tools.
|
||||
var proxychainsCleanup func()
|
||||
if confPath, cleanup, err := proxy.WriteProxychainsConf(cfg.Proxies); err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("manager: writing proxychains config: %w", err)
|
||||
} else if confPath != "" {
|
||||
ctx = proxy.WithProxychainsConf(ctx, confPath)
|
||||
proxychainsCleanup = cleanup
|
||||
}
|
||||
|
||||
s := &Search{
|
||||
ID: uuid.NewString(),
|
||||
Target: target,
|
||||
@@ -81,7 +100,7 @@ func (m *Manager) Start(
|
||||
m.searches[s.ID] = s
|
||||
m.mu.Unlock()
|
||||
|
||||
go m.runAll(ctx, s, activeTools)
|
||||
go m.runAll(ctx, s, activeTools, proxychainsCleanup)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -208,7 +227,10 @@ func (m *Manager) instantiate(cfg *config.Config, inputType tools.InputType, pro
|
||||
return runners, statuses, nil
|
||||
}
|
||||
|
||||
func (m *Manager) runAll(ctx context.Context, s *Search, runners []tools.ToolRunner) {
|
||||
func (m *Manager) runAll(ctx context.Context, s *Search, runners []tools.ToolRunner, cleanup func()) {
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for _, tool := range runners {
|
||||
wg.Add(1)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/anotherhadi/iknowyou/internal/proxy"
|
||||
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||
)
|
||||
|
||||
@@ -80,7 +81,7 @@ func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out
|
||||
req.Header.Set("X-RapidAPI-Host", "breachdirectory.p.rapidapi.com")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := proxy.ClientFromContext(ctx).Do(req)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anotherhadi/iknowyou/internal/proxy"
|
||||
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||
)
|
||||
|
||||
@@ -61,7 +62,7 @@ func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; crtsh-scanner/1.0)")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := proxy.ClientFromContext(ctx).Do(req)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||
|
||||
@@ -94,7 +94,7 @@ func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out
|
||||
parsed[i] = ansiRe.ReplaceAllString(l, "")
|
||||
}
|
||||
|
||||
start := 0
|
||||
start := -1
|
||||
for i, l := range parsed {
|
||||
if strings.Contains(l, "[+] Authenticated !") {
|
||||
start = i + 1
|
||||
@@ -102,6 +102,14 @@ func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out
|
||||
}
|
||||
}
|
||||
|
||||
if start == -1 {
|
||||
// Banner printed but auth line never appeared — bad/expired credentials.
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "GHunt authentication failed — credentials may be missing or expired (run 'ghunt login' and update your creds in Settings)"}
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||
return nil
|
||||
}
|
||||
|
||||
end := len(lines)
|
||||
for i := start; i < len(parsed); i++ {
|
||||
if strings.Contains(parsed[i], "Traceback (most recent call last)") {
|
||||
@@ -117,6 +125,8 @@ func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out
|
||||
} else if output != "" {
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: output}
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 1}
|
||||
} else {
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0}
|
||||
}
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeDone}
|
||||
return nil
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/anotherhadi/iknowyou/internal/proxy"
|
||||
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||
)
|
||||
|
||||
@@ -76,7 +77,7 @@ func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := proxy.ClientFromContext(ctx).Do(req)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/anotherhadi/iknowyou/internal/proxy"
|
||||
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||
)
|
||||
|
||||
@@ -90,7 +91,7 @@ func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputTy
|
||||
req.Header.Set("X-API-Key", r.cfg.APIKey)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := proxy.ClientFromContext(ctx).Do(req)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
|
||||
"github.com/anotherhadi/iknowyou/internal/proxy"
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
@@ -14,7 +15,14 @@ var oscRe = regexp.MustCompile(`\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)`)
|
||||
|
||||
// RunWithPTY runs cmd under a pseudo-terminal (preserving ANSI colours) and
|
||||
// returns the full output once the process exits.
|
||||
// If a proxychains config path is stored in ctx, the command is transparently
|
||||
// wrapped with proxychains4.
|
||||
func RunWithPTY(ctx context.Context, cmd *exec.Cmd) (string, error) {
|
||||
if confPath := proxy.ProxychainsConfFromContext(ctx); confPath != "" {
|
||||
args := append([]string{"-q", "-f", confPath, cmd.Path}, cmd.Args[1:]...)
|
||||
cmd = exec.CommandContext(ctx, "proxychains4", args...)
|
||||
}
|
||||
|
||||
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 50, Cols: 220})
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
wappalyzergo "github.com/projectdiscovery/wappalyzergo"
|
||||
|
||||
"github.com/anotherhadi/iknowyou/internal/proxy"
|
||||
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||
)
|
||||
|
||||
@@ -55,7 +56,7 @@ func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)")
|
||||
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
resp, err = proxy.ClientFromContext(ctx).Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anotherhadi/iknowyou/internal/proxy"
|
||||
"github.com/anotherhadi/iknowyou/internal/tools"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
@@ -101,9 +102,9 @@ func prettyResult(r gjson.Result, depth int) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func doRequest(ctx context.Context, req *http.Request) ([]byte, *http.Response, error) {
|
||||
func doRequest(ctx context.Context, client *http.Client, req *http.Request) ([]byte, *http.Response, error) {
|
||||
for {
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -161,7 +162,7 @@ func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputTy
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
body, resp, err := doRequest(ctx, req)
|
||||
body, resp, err := doRequest(ctx, proxy.ClientFromContext(ctx), req)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"}
|
||||
|
||||
Reference in New Issue
Block a user