Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/proxies/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func (p ProxyCmd) Check(ctx context.Context, in ProxyCheckInput) error {
}
rows = append(rows, []string{"Name", name})
rows = append(rows, []string{"Type", string(proxy.Type)})
rows = append(rows, []string{"Bypass Hosts", formatBypassHosts(proxy.BypassHosts)})

// Display protocol (default to https if not set)
protocol := string(proxy.Protocol)
Expand Down
36 changes: 36 additions & 0 deletions cmd/proxies/check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package proxies

import (
"context"
"testing"

"github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/option"
"github.com/stretchr/testify/assert"
)

func TestProxyCheck_ShowsBypassHosts(t *testing.T) {
buf := captureOutput(t)

fake := &FakeProxyService{
CheckFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) {
return &kernel.ProxyCheckResponse{
ID: id,
Name: "Proxy 1",
Type: kernel.ProxyCheckResponseTypeDatacenter,
BypassHosts: []string{"localhost", "internal.service.local"},
Status: kernel.ProxyCheckResponseStatusAvailable,
}, nil
},
}

p := ProxyCmd{proxies: fake}
err := p.Check(context.Background(), ProxyCheckInput{ID: "proxy-1"})

assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Bypass Hosts")
assert.Contains(t, output, "localhost")
assert.Contains(t, output, "internal.service.local")
assert.Contains(t, output, "Proxy health check passed")
}
49 changes: 34 additions & 15 deletions cmd/proxies/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package proxies
import (
"context"
"fmt"
"strings"

"github.com/kernel/cli/pkg/table"
"github.com/kernel/cli/pkg/util"
Expand Down Expand Up @@ -40,6 +41,9 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
if in.Name != "" {
params.Name = kernel.Opt(in.Name)
}
if len(in.BypassHosts) > 0 {
params.BypassHosts = normalizeBypassHosts(in.BypassHosts)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normalization may send unintended empty array to API

Low Severity

When in.BypassHosts contains only whitespace or empty strings (e.g., --bypass-host " "), the guard len(in.BypassHosts) > 0 passes, but normalizeBypassHosts filters everything out and returns an empty []string{}. This empty (non-nil) slice gets assigned to params.BypassHosts, which serializes as "bypass_hosts":[] in JSON rather than omitting the field entirely. The API may treat an explicit empty array differently from an absent field.

Fix in Cursor Fix in Web


// Build config based on type
switch proxyType {
Expand Down Expand Up @@ -189,6 +193,7 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
}
rows = append(rows, []string{"Name", name})
rows = append(rows, []string{"Type", string(proxy.Type)})
rows = append(rows, []string{"Bypass Hosts", formatBypassHosts(proxy.BypassHosts)})

// Display protocol (default to https if not set)
protocol := string(proxy.Protocol)
Expand Down Expand Up @@ -219,26 +224,40 @@ func runProxiesCreate(cmd *cobra.Command, args []string) error {
port, _ := cmd.Flags().GetInt("port")
username, _ := cmd.Flags().GetString("username")
password, _ := cmd.Flags().GetString("password")
bypassHosts, _ := cmd.Flags().GetStringSlice("bypass-host")

output, _ := cmd.Flags().GetString("output")

svc := client.Proxies
p := ProxyCmd{proxies: &svc}
return p.Create(cmd.Context(), ProxyCreateInput{
Name: name,
Type: proxyType,
Protocol: protocol,
Country: country,
City: city,
State: state,
Zip: zip,
ASN: asn,
OS: os,
Carrier: carrier,
Host: host,
Port: port,
Username: username,
Password: password,
Output: output,
Name: name,
Type: proxyType,
Protocol: protocol,
BypassHosts: bypassHosts,
Country: country,
City: city,
State: state,
Zip: zip,
ASN: asn,
OS: os,
Carrier: carrier,
Host: host,
Port: port,
Username: username,
Password: password,
Output: output,
})
}

func normalizeBypassHosts(hosts []string) []string {
normalized := make([]string, 0, len(hosts))
for _, host := range hosts {
trimmed := strings.TrimSpace(host)
if trimmed != "" {
normalized = append(normalized, trimmed)
}
}

return normalized
}
39 changes: 33 additions & 6 deletions cmd/proxies/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,28 @@ func TestProxyCreate_Datacenter_Success(t *testing.T) {
// Verify the request
assert.Equal(t, kernel.ProxyNewParamsTypeDatacenter, body.Type)
assert.Equal(t, "My DC Proxy", body.Name.Value)
assert.Equal(t, []string{"localhost", "internal.service.local"}, body.BypassHosts)

// Check config
dcConfig := body.Config.OfProxyNewsConfigDatacenterProxyConfig
assert.NotNil(t, dcConfig)
assert.Equal(t, "US", dcConfig.Country.Value)

return &kernel.ProxyNewResponse{
ID: "dc-new",
Name: "My DC Proxy",
Type: kernel.ProxyNewResponseTypeDatacenter,
ID: "dc-new",
Name: "My DC Proxy",
Type: kernel.ProxyNewResponseTypeDatacenter,
BypassHosts: []string{"localhost", "internal.service.local"},
}, nil
},
}

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "My DC Proxy",
Type: "datacenter",
Country: "US",
Name: "My DC Proxy",
Type: "datacenter",
Country: "US",
BypassHosts: []string{"localhost", "internal.service.local"},
})

assert.NoError(t, err)
Expand All @@ -46,6 +49,9 @@ func TestProxyCreate_Datacenter_Success(t *testing.T) {
assert.Contains(t, output, "Successfully created proxy")
assert.Contains(t, output, "dc-new")
assert.Contains(t, output, "My DC Proxy")
assert.Contains(t, output, "Bypass Hosts")
assert.Contains(t, output, "localhost")
assert.Contains(t, output, "internal.service.local")
}

func TestProxyCreate_Datacenter_WithoutCountry(t *testing.T) {
Expand Down Expand Up @@ -306,6 +312,27 @@ func TestProxyCreate_Protocol_Invalid(t *testing.T) {
assert.Contains(t, err.Error(), "invalid protocol: ftp")
}

func TestProxyCreate_BypassHosts_Normalized(t *testing.T) {
fake := &FakeProxyService{
NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) {
assert.Equal(t, []string{"localhost", "internal.service.local"}, body.BypassHosts)
return &kernel.ProxyNewResponse{
ID: "test-proxy",
Type: kernel.ProxyNewResponseTypeDatacenter,
}, nil
},
}

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Type: "datacenter",
Country: "US",
BypassHosts: []string{" localhost ", "", "internal.service.local"},
})

assert.NoError(t, err)
}

func TestProxyCreate_APIError(t *testing.T) {
_ = captureOutput(t)

Expand Down
11 changes: 11 additions & 0 deletions cmd/proxies/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package proxies

import "strings"

func formatBypassHosts(hosts []string) string {
if len(hosts) == 0 {
return "-"
}

return strings.Join(hosts, ", ")
}
1 change: 1 addition & 0 deletions cmd/proxies/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func (p ProxyCmd) Get(ctx context.Context, in ProxyGetInput) error {
}
rows = append(rows, []string{"Name", name})
rows = append(rows, []string{"Type", string(item.Type)})
rows = append(rows, []string{"Bypass Hosts", formatBypassHosts(item.BypassHosts)})

// Display protocol (default to https if not set)
protocol := string(item.Protocol)
Expand Down
10 changes: 7 additions & 3 deletions cmd/proxies/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ func TestProxyGet_Datacenter(t *testing.T) {
fake := &FakeProxyService{
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) {
return &kernel.ProxyGetResponse{
ID: "dc-1",
Name: "US Datacenter",
Type: kernel.ProxyGetResponseTypeDatacenter,
ID: "dc-1",
Name: "US Datacenter",
Type: kernel.ProxyGetResponseTypeDatacenter,
BypassHosts: []string{"localhost", "internal.service.local"},
Config: kernel.ProxyGetResponseConfigUnion{
Country: "US",
},
Expand All @@ -40,6 +41,9 @@ func TestProxyGet_Datacenter(t *testing.T) {
assert.Contains(t, output, "US Datacenter")
assert.Contains(t, output, "Type")
assert.Contains(t, output, "datacenter")
assert.Contains(t, output, "Bypass Hosts")
assert.Contains(t, output, "localhost")
assert.Contains(t, output, "internal.service.local")
assert.Contains(t, output, "Country")
assert.Contains(t, output, "US")
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/proxies/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (p ProxyCmd) List(ctx context.Context, in ProxyListInput) error {

// Prepare table data
tableData := pterm.TableData{
{"ID", "Name", "Type", "Protocol", "Config", "Status", "Last Checked"},
{"ID", "Name", "Type", "Protocol", "Bypass Hosts", "Config", "Status", "Last Checked"},
}

for _, proxy := range *items {
Expand Down Expand Up @@ -77,6 +77,7 @@ func (p ProxyCmd) List(ctx context.Context, in ProxyListInput) error {
name,
string(proxy.Type),
protocol,
formatBypassHosts(proxy.BypassHosts),
configStr,
status,
lastChecked,
Expand Down
8 changes: 5 additions & 3 deletions cmd/proxies/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ func TestProxyList_WithProxies(t *testing.T) {
createResidentialProxy("res-1", "SF Residential", "US", "sanfrancisco", "CA"),
createCustomProxy("custom-1", "My Proxy", "proxy.example.com", 8080),
{
ID: "mobile-1",
Name: "Mobile Proxy",
Type: kernel.ProxyListResponseTypeMobile,
ID: "mobile-1",
Name: "Mobile Proxy",
Type: kernel.ProxyListResponseTypeMobile,
BypassHosts: []string{"abc"},
Config: kernel.ProxyListResponseConfigUnion{
Country: "US",
Carrier: "verizon",
Expand Down Expand Up @@ -85,6 +86,7 @@ func TestProxyList_WithProxies(t *testing.T) {

assert.Contains(t, output, "mobile-1")
assert.Contains(t, output, "mobile")
assert.Contains(t, output, "abc")

assert.Contains(t, output, "isp-1")
assert.Contains(t, output, "-") // Empty name shows as "-"
Expand Down
10 changes: 7 additions & 3 deletions cmd/proxies/proxies.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ Proxy types (from best to worst for bot detection):

Examples:
# Create a datacenter proxy
kernel beta proxies create --type datacenter --country US --name "US Datacenter"
kernel proxies create --type datacenter --country US --name "US Datacenter"

# Create a custom proxy
kernel beta proxies create --type custom --host proxy.example.com --port 8080 --username myuser --password mypass
kernel proxies create --type custom --host proxy.example.com --port 8080 --username myuser --password mypass

# Create a residential proxy with location
kernel beta proxies create --type residential --country US --city sanfrancisco --state CA`,
kernel proxies create --type residential --country US --city sanfrancisco --state CA

# Create a proxy with bypass hosts
kernel proxies create --type datacenter --country US --bypass-host localhost,internal.service.local`,
RunE: runProxiesCreate,
}

Expand Down Expand Up @@ -105,6 +108,7 @@ func init() {
proxiesCreateCmd.Flags().Int("port", 0, "Proxy port")
proxiesCreateCmd.Flags().String("username", "", "Username for proxy authentication")
proxiesCreateCmd.Flags().String("password", "", "Password for proxy authentication")
proxiesCreateCmd.Flags().StringSlice("bypass-host", nil, "Hostname(s) to bypass proxy and connect directly (repeat or comma-separated)")

// Delete flags
proxiesDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
Expand Down
2 changes: 2 additions & 0 deletions cmd/proxies/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type ProxyCreateInput struct {
Name string
Type string
Protocol string
// Hostnames that should bypass the parent proxy and connect directly.
BypassHosts []string
// Datacenter/ISP config
Country string
// Residential/Mobile config
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/joho/godotenv v1.5.1
github.com/kernel/kernel-go-sdk v0.37.0
github.com/kernel/kernel-go-sdk v0.39.0
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/pterm/pterm v0.12.80
github.com/samber/lo v1.51.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kernel/kernel-go-sdk v0.37.0 h1:90/AJUSSY0P09S2qO9GLP3xPr0qS8z0Fb7frDbVnJGQ=
github.com/kernel/kernel-go-sdk v0.37.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/kernel/kernel-go-sdk v0.39.0 h1:P7R7dG1b3T9MQ420mhESWwbp+KgRTbQCTD5dVBQnkm4=
github.com/kernel/kernel-go-sdk v0.39.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
Expand Down