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
24 changes: 20 additions & 4 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,18 @@ func New(options *Options) (*Client, error) {
return nil, errkit.Wrap(err, "failed to decode public key")
}
client.pubKey = pubKey
if serverURL, err := url.Parse(options.SessionInfo.ServerURL); err == nil {
registrationServerURL := options.SessionInfo.ServerURL
if serverURL, err := parseServerURL(options.SessionInfo.ServerURL); err == nil {
client.serverURL = serverURL
registrationServerURL = serverURL.String()
}
// attempts to re-register - server will reject is already existing
registrationRequest, err := encodeRegistrationRequest(options.SessionInfo.PublicKey, options.SessionInfo.SecretKey, options.SessionInfo.CorrelationID)
if err != nil {
return nil, err
}
// silently fails to re-register if the session is still alive
_ = client.performRegistration(options.SessionInfo.ServerURL, registrationRequest)
_ = client.performRegistration(registrationServerURL, registrationRequest)
} else {
payload, err := client.initializeRSAKeys()
if err != nil {
Expand Down Expand Up @@ -327,11 +329,11 @@ func (c *Client) parseServerURLs(serverURL string, payload []byte) error {
}

values := strings.Split(serverURL, ",")
registerFunc := func(idx int, value string) error {
registerFunc := func(_ int, value string) error {
if !stringsutil.HasPrefixAny(value, "http://", "https://") {
value = fmt.Sprintf("https://%s", value)
}
parsed, err := url.Parse(value)
parsed, err := parseServerURL(value)
if err != nil {
return errkit.Wrap(err, "could not parse server URL")
}
Expand Down Expand Up @@ -369,6 +371,20 @@ func (c *Client) parseServerURLs(serverURL string, payload []byte) error {
return nil
}

func parseServerURL(value string) (*url.URL, error) {
parsed, err := url.Parse(value)
if err != nil {
return nil, err
}

for strings.HasSuffix(parsed.EscapedPath(), "/") {
parsed.Path = strings.TrimSuffix(parsed.Path, "/")
parsed.RawPath = strings.TrimSuffix(parsed.RawPath, "/")
}

return parsed, nil
}

// InteractionCallback is a callback function for a reported interaction
type InteractionCallback func(*server.Interaction)

Expand Down
96 changes: 96 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package client

import (
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"

"github.com/projectdiscovery/interactsh/pkg/options"
"github.com/projectdiscovery/interactsh/pkg/server"
"github.com/projectdiscovery/retryablehttp-go"
"github.com/stretchr/testify/require"
)

func TestNewAcceptsServerURLWithOptionalTrailingSlash(t *testing.T) {
var registrationCount atomic.Int64
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.EscapedPath() {
case "//register":
// Some deployments canonicalize this path with a redirect that drops the POST body.
http.Redirect(w, r, "/register", http.StatusMovedPermanently)
case "/register", "/base%2F/register":
request := &server.RegisterRequest{}
if err := json.NewDecoder(r.Body).Decode(request); err != nil {
http.Error(w, fmt.Sprintf(`{"error":"could not decode json body: %s"}`, err), http.StatusBadRequest)
return
}
registrationCount.Add(1)
_, _ = w.Write([]byte(`{"message":"registration successful"}`))
case "/poll":
_, _ = w.Write([]byte(`{"data":[],"extra":[],"aes_key":""}`))
case "/deregister", "/base%2F/deregister":
w.WriteHeader(http.StatusOK)
default:
http.NotFound(w, r)
}
})

testServer := httptest.NewTLSServer(handler)
t.Cleanup(testServer.Close)

httpClient := retryablehttp.NewClient(retryablehttp.DefaultOptionsSpraying)
httpClient.HTTPClient = testServer.Client()

for _, test := range []struct {
name string
suffix string
}{
{name: "without trailing slash"},
{name: "with trailing slash", suffix: "/"},
{name: "with percent-encoded slash", suffix: "/base%2F/"},
} {
t.Run(test.name, func(t *testing.T) {
interactshClient, err := New(&Options{
ServerURL: testServer.URL + test.suffix,
DisableHTTPFallback: true,
HTTPClient: httpClient,
})
require.NoError(t, err)
require.NoError(t, interactshClient.Close())
})
}

t.Run("resumed session with trailing slash", func(t *testing.T) {
originalClient, err := New(&Options{
ServerURL: testServer.URL,
DisableHTTPFallback: true,
HTTPClient: httpClient,
})
require.NoError(t, err)

publicKey, err := encodePublicKey(originalClient.pubKey)
require.NoError(t, err)
sessionInfo := &options.SessionInfo{
ServerURL: testServer.URL + "/",
PrivateKey: string(x509.MarshalPKCS1PrivateKey(originalClient.privKey)),
CorrelationID: originalClient.correlationID,
SecretKey: originalClient.secretKey,
PublicKey: publicKey,
}
require.NoError(t, originalClient.Close())

registrationsBeforeResume := registrationCount.Load()
resumedClient, err := New(&Options{
SessionInfo: sessionInfo,
HTTPClient: httpClient,
})
require.NoError(t, err)
require.Equal(t, registrationsBeforeResume+1, registrationCount.Load())
require.NoError(t, resumedClient.getInteractions(func(*server.Interaction) {}))
require.NoError(t, resumedClient.Close())
})
}
Loading