mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 00:33:50 +00:00
77e8b63470
Added package cgroup to orchestrate cgroups. Only support for cgroup2 was added to utilize because cgroup2 cgroups have unique IDs that can be used correlated with BPF events. Added bpf package that contains three BPF programs: execsnoop, opensnoop, and tcpconnect. The bpf package starts and stops these programs as well correlating their output with Teleport sessions and emitting them to the audit log. Added support for Teleport to re-exec itself before launching a shell. This allows Teleport to start a child process, capture it's PID, place the PID in a cgroup, and then continue to process. Once the process is continued it can be tracked by it's cgroup ID. Reduced the total number of connections to a host so Teleport does not quickly exhaust all file descriptors. Exhausting all file descriptors happens very quickly when disk events are emitted to the audit log which are emitted at a very high rate. Added tarballs for exec sessions. Updated session.start and session.end events with additional metadata. Updated the format of session tarballs to include enhanced events. Added file configuration for enhanced session recording. Added code to startup enhanced session recording and pass package to SSH nodes.
1822 lines
52 KiB
Go
1822 lines
52 KiB
Go
/*
|
|
Copyright 2015 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/flate"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/base32"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"os/user"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/net/websocket"
|
|
"golang.org/x/text/encoding/unicode"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/lib/auth"
|
|
"github.com/gravitational/teleport/lib/auth/mocku2f"
|
|
"github.com/gravitational/teleport/lib/backend"
|
|
"github.com/gravitational/teleport/lib/bpf"
|
|
"github.com/gravitational/teleport/lib/client"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/events"
|
|
"github.com/gravitational/teleport/lib/fixtures"
|
|
"github.com/gravitational/teleport/lib/httplib"
|
|
"github.com/gravitational/teleport/lib/httplib/csrf"
|
|
"github.com/gravitational/teleport/lib/pam"
|
|
"github.com/gravitational/teleport/lib/reversetunnel"
|
|
"github.com/gravitational/teleport/lib/secret"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/session"
|
|
"github.com/gravitational/teleport/lib/srv"
|
|
"github.com/gravitational/teleport/lib/srv/regular"
|
|
"github.com/gravitational/teleport/lib/sshutils"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
"github.com/gravitational/teleport/lib/web/ui"
|
|
|
|
"github.com/gravitational/roundtrip"
|
|
"github.com/gravitational/trace"
|
|
|
|
"github.com/beevik/etree"
|
|
"github.com/gokyle/hotp"
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/jonboulle/clockwork"
|
|
lemma_secret "github.com/mailgun/lemma/secret"
|
|
"github.com/pborman/uuid"
|
|
"github.com/pquerna/otp/totp"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/tstranex/u2f"
|
|
. "gopkg.in/check.v1"
|
|
kyaml "k8s.io/apimachinery/pkg/util/yaml"
|
|
)
|
|
|
|
const hostID = "00000000-0000-0000-0000-000000000000"
|
|
|
|
func TestWeb(t *testing.T) {
|
|
TestingT(t)
|
|
}
|
|
|
|
type WebSuite struct {
|
|
node *regular.Server
|
|
proxy *regular.Server
|
|
srvID string
|
|
|
|
user string
|
|
webServer *httptest.Server
|
|
freePorts []string
|
|
|
|
mockU2F *mocku2f.Key
|
|
server *auth.TestTLSServer
|
|
proxyClient *auth.Client
|
|
clock clockwork.Clock
|
|
}
|
|
|
|
var _ = Suite(&WebSuite{
|
|
clock: clockwork.NewFakeClock(),
|
|
})
|
|
|
|
// TestMain will re-execute Teleport to run a command if "exec" is passed to
|
|
// it as an argument. Otherwise it will run tests as normal.
|
|
func TestMain(m *testing.M) {
|
|
// If the test is re-executing itself, execute the command that comes over
|
|
// the pipe.
|
|
if len(os.Args) == 2 && os.Args[1] == teleport.ExecSubCommand {
|
|
srv.RunCommand()
|
|
return
|
|
}
|
|
|
|
// Otherwise run tests as normal.
|
|
code := m.Run()
|
|
os.Exit(code)
|
|
}
|
|
|
|
func (s *WebSuite) SetUpSuite(c *C) {
|
|
var err error
|
|
os.Unsetenv(teleport.DebugEnvVar)
|
|
utils.InitLoggerForTests(testing.Verbose())
|
|
|
|
// configure tests to use static assets from web/dist:
|
|
debugAssetsPath = "../../web/dist"
|
|
os.Setenv(teleport.DebugEnvVar, "true")
|
|
|
|
//sessionStreamPollPeriod = time.Millisecond
|
|
s.mockU2F, err = mocku2f.Create()
|
|
c.Assert(err, IsNil)
|
|
c.Assert(s.mockU2F, NotNil)
|
|
}
|
|
|
|
func (s *WebSuite) TearDownSuite(c *C) {
|
|
os.Unsetenv(teleport.DebugEnvVar)
|
|
}
|
|
|
|
func (s *WebSuite) SetUpTest(c *C) {
|
|
u, err := user.Current()
|
|
c.Assert(err, IsNil)
|
|
s.user = u.Username
|
|
|
|
s.freePorts, err = utils.GetFreeTCPPorts(6)
|
|
c.Assert(err, IsNil)
|
|
|
|
authServer, err := auth.NewTestAuthServer(auth.TestAuthServerConfig{
|
|
ClusterName: "localhost",
|
|
Dir: c.MkDir(),
|
|
Clock: clockwork.NewFakeClockAt(time.Date(2017, 05, 10, 18, 53, 0, 0, time.UTC)),
|
|
})
|
|
c.Assert(err, IsNil)
|
|
s.server, err = authServer.NewTestTLSServer()
|
|
c.Assert(err, IsNil)
|
|
|
|
// start node
|
|
nodePort := s.freePorts[len(s.freePorts)-1]
|
|
s.freePorts = s.freePorts[:len(s.freePorts)-1]
|
|
|
|
certs, err := s.server.Auth().GenerateServerKeys(auth.GenerateServerKeysRequest{
|
|
HostID: hostID,
|
|
NodeName: s.server.ClusterName(),
|
|
Roles: teleport.Roles{teleport.RoleNode},
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
signer, err := sshutils.NewSigner(certs.Key, certs.Cert)
|
|
c.Assert(err, IsNil)
|
|
|
|
nodeClient, err := s.server.NewClient(auth.TestBuiltin(teleport.RoleNode))
|
|
c.Assert(err, IsNil)
|
|
|
|
// create SSH service:
|
|
nodeDataDir := c.MkDir()
|
|
node, err := regular.New(
|
|
utils.NetAddr{AddrNetwork: "tcp", Addr: fmt.Sprintf("127.0.0.1:%v", nodePort)},
|
|
s.server.ClusterName(),
|
|
[]ssh.Signer{signer},
|
|
nodeClient,
|
|
nodeDataDir,
|
|
"",
|
|
utils.NetAddr{},
|
|
regular.SetNamespace(defaults.Namespace),
|
|
regular.SetShell("/bin/sh"),
|
|
regular.SetSessionServer(nodeClient),
|
|
regular.SetAuditLog(nodeClient),
|
|
regular.SetPAMConfig(&pam.Config{Enabled: false}),
|
|
regular.SetBPF(&bpf.NOP{}),
|
|
)
|
|
c.Assert(err, IsNil)
|
|
s.node = node
|
|
s.srvID = node.ID()
|
|
c.Assert(s.node.Start(), IsNil)
|
|
|
|
c.Assert(auth.CreateUploaderDir(nodeDataDir), IsNil)
|
|
|
|
// create reverse tunnel service:
|
|
s.proxyClient, err = s.server.NewClient(auth.TestBuiltin(teleport.RoleProxy))
|
|
c.Assert(err, IsNil)
|
|
|
|
revTunListener, err := net.Listen("tcp", fmt.Sprintf("%v:0", s.server.ClusterName()))
|
|
c.Assert(err, IsNil)
|
|
|
|
revTunServer, err := reversetunnel.NewServer(reversetunnel.Config{
|
|
ID: node.ID(),
|
|
Listener: revTunListener,
|
|
ClientTLS: s.proxyClient.TLSConfig(),
|
|
ClusterName: s.server.ClusterName(),
|
|
HostSigners: []ssh.Signer{signer},
|
|
LocalAuthClient: s.proxyClient,
|
|
LocalAccessPoint: s.proxyClient,
|
|
NewCachingAccessPoint: auth.NoCache,
|
|
DirectClusters: []reversetunnel.DirectCluster{{Name: s.server.ClusterName(), Client: s.proxyClient}},
|
|
DataDir: c.MkDir(),
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
// proxy server:
|
|
proxyPort := s.freePorts[len(s.freePorts)-1]
|
|
s.freePorts = s.freePorts[:len(s.freePorts)-1]
|
|
proxyAddr := utils.NetAddr{
|
|
AddrNetwork: "tcp", Addr: fmt.Sprintf("127.0.0.1:%v", proxyPort),
|
|
}
|
|
s.proxy, err = regular.New(proxyAddr,
|
|
s.server.ClusterName(),
|
|
[]ssh.Signer{signer},
|
|
s.proxyClient,
|
|
c.MkDir(),
|
|
"",
|
|
utils.NetAddr{},
|
|
regular.SetProxyMode(revTunServer),
|
|
regular.SetSessionServer(s.proxyClient),
|
|
regular.SetAuditLog(s.proxyClient),
|
|
regular.SetNamespace(defaults.Namespace),
|
|
regular.SetBPF(&bpf.NOP{}),
|
|
)
|
|
c.Assert(err, IsNil)
|
|
|
|
handler, err := NewHandler(Config{
|
|
Proxy: revTunServer,
|
|
AuthServers: utils.FromAddr(s.server.Addr()),
|
|
DomainName: s.server.ClusterName(),
|
|
ProxyClient: s.proxyClient,
|
|
CipherSuites: utils.DefaultCipherSuites(),
|
|
}, SetSessionStreamPollPeriod(200*time.Millisecond))
|
|
c.Assert(err, IsNil)
|
|
|
|
s.webServer = httptest.NewUnstartedServer(handler)
|
|
s.webServer.StartTLS()
|
|
err = s.proxy.Start()
|
|
c.Assert(err, IsNil)
|
|
|
|
addr, _ := utils.ParseAddr(s.webServer.Listener.Addr().String())
|
|
handler.handler.cfg.ProxyWebAddr = *addr
|
|
handler.handler.cfg.ProxySSHAddr = proxyAddr
|
|
}
|
|
|
|
func (s *WebSuite) TearDownTest(c *C) {
|
|
c.Assert(s.node.Close(), IsNil)
|
|
c.Assert(s.server.Close(), IsNil)
|
|
s.webServer.Close()
|
|
s.proxy.Close()
|
|
}
|
|
|
|
type authPack struct {
|
|
otpSecret string
|
|
user string
|
|
login string
|
|
otp *hotp.HOTP
|
|
session *CreateSessionResponse
|
|
clt *client.WebClient
|
|
cookies []*http.Cookie
|
|
}
|
|
|
|
func (s *WebSuite) authPackFromResponse(c *C, re *roundtrip.Response) *authPack {
|
|
var sess *createSessionResponseRaw
|
|
c.Assert(json.Unmarshal(re.Bytes(), &sess), IsNil)
|
|
|
|
jar, err := cookiejar.New(nil)
|
|
c.Assert(err, IsNil)
|
|
|
|
clt := s.client(roundtrip.BearerAuth(sess.Token), roundtrip.CookieJar(jar))
|
|
jar.SetCookies(s.url(), re.Cookies())
|
|
|
|
session, err := sess.response()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if session.ExpiresIn < 0 {
|
|
c.Errorf("expected expiry time to be in the future but got %v", session.ExpiresIn)
|
|
}
|
|
return &authPack{
|
|
session: session,
|
|
clt: clt,
|
|
cookies: re.Cookies(),
|
|
}
|
|
}
|
|
|
|
// authPack returns new authenticated package consisting of created valid
|
|
// user, otp token, created web session and authenticated client.
|
|
func (s *WebSuite) authPack(c *C, user string) *authPack {
|
|
login := s.user
|
|
pass := "abc123"
|
|
rawSecret := "def456"
|
|
otpSecret := base32.StdEncoding.EncodeToString([]byte(rawSecret))
|
|
|
|
ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{
|
|
Type: teleport.Local,
|
|
SecondFactor: teleport.OTP,
|
|
})
|
|
c.Assert(err, IsNil)
|
|
err = s.server.Auth().SetAuthPreference(ap)
|
|
c.Assert(err, IsNil)
|
|
|
|
s.createUser(c, user, login, pass, otpSecret)
|
|
|
|
// create a valid otp token
|
|
validToken, err := totp.GenerateCode(otpSecret, time.Now())
|
|
c.Assert(err, IsNil)
|
|
|
|
clt := s.client()
|
|
req := createSessionReq{
|
|
User: user,
|
|
Pass: pass,
|
|
SecondFactorToken: validToken,
|
|
}
|
|
|
|
csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
|
|
re, err := s.login(clt, csrfToken, csrfToken, req)
|
|
c.Assert(err, IsNil)
|
|
|
|
var rawSess *createSessionResponseRaw
|
|
c.Assert(json.Unmarshal(re.Bytes(), &rawSess), IsNil)
|
|
|
|
sess, err := rawSess.response()
|
|
c.Assert(err, IsNil)
|
|
|
|
jar, err := cookiejar.New(nil)
|
|
c.Assert(err, IsNil)
|
|
|
|
clt = s.client(roundtrip.BearerAuth(sess.Token), roundtrip.CookieJar(jar))
|
|
jar.SetCookies(s.url(), re.Cookies())
|
|
|
|
return &authPack{
|
|
otpSecret: otpSecret,
|
|
user: user,
|
|
login: login,
|
|
session: sess,
|
|
clt: clt,
|
|
cookies: re.Cookies(),
|
|
}
|
|
}
|
|
|
|
func (s *WebSuite) createUser(c *C, user string, login string, pass string, otpSecret string) {
|
|
teleUser, err := services.NewUser(user)
|
|
c.Assert(err, IsNil)
|
|
role := services.RoleForUser(teleUser)
|
|
role.SetLogins(services.Allow, []string{login})
|
|
options := role.GetOptions()
|
|
options.ForwardAgent = services.NewBool(true)
|
|
role.SetOptions(options)
|
|
err = s.server.Auth().UpsertRole(role)
|
|
c.Assert(err, IsNil)
|
|
teleUser.AddRole(role.GetName())
|
|
|
|
err = s.server.Auth().UpsertUser(teleUser)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = s.server.Auth().UpsertPassword(user, []byte(pass))
|
|
c.Assert(err, IsNil)
|
|
|
|
err = s.server.Auth().UpsertTOTP(user, otpSecret)
|
|
c.Assert(err, IsNil)
|
|
}
|
|
|
|
func (s *WebSuite) TestNewUser(c *C) {
|
|
token, err := s.server.Auth().CreateSignupToken(services.UserV1{Name: "bob", AllowedLogins: []string{s.user}}, 0)
|
|
c.Assert(err, IsNil)
|
|
|
|
// Save the original signup token, after GET /v2/webapi/users/invites/<token>
|
|
// this should change.
|
|
ost, err := s.server.Auth().GetSignupToken(token)
|
|
c.Assert(err, IsNil)
|
|
|
|
tokens, err := s.server.Auth().GetTokens()
|
|
c.Assert(err, IsNil)
|
|
c.Assert(len(tokens), Equals, 1)
|
|
c.Assert(tokens[0].GetName(), Equals, token)
|
|
|
|
clt := s.client()
|
|
re, err := clt.Get(context.Background(), clt.Endpoint("webapi", "users", "invites", token), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
var out *renderUserInviteResponse
|
|
c.Assert(json.Unmarshal(re.Bytes(), &out), IsNil)
|
|
c.Assert(out.User, Equals, "bob")
|
|
c.Assert(out.InviteToken, Equals, token)
|
|
|
|
st, err := s.server.Auth().GetSignupToken(token)
|
|
c.Assert(err, IsNil)
|
|
|
|
// Make sure that the signup token changed after rending the endpoint
|
|
// GET /v2/webapi/users/invites/<token> above.
|
|
c.Assert(st, Not(Equals), ost)
|
|
|
|
validToken, err := totp.GenerateCode(st.OTPKey, time.Now())
|
|
c.Assert(err, IsNil)
|
|
|
|
tempPass := "abc123"
|
|
|
|
re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "users"), createNewUserReq{
|
|
InviteToken: token,
|
|
Pass: tempPass,
|
|
SecondFactorToken: validToken,
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
var rawSess *createSessionResponseRaw
|
|
c.Assert(json.Unmarshal(re.Bytes(), &rawSess), IsNil)
|
|
cookies := re.Cookies()
|
|
c.Assert(len(cookies), Equals, 1)
|
|
|
|
// now make sure we are logged in by calling authenticated method
|
|
// we need to supply both session cookie and bearer token for
|
|
// request to succeed
|
|
jar, err := cookiejar.New(nil)
|
|
c.Assert(err, IsNil)
|
|
|
|
clt = s.client(roundtrip.BearerAuth(rawSess.Token), roundtrip.CookieJar(jar))
|
|
jar.SetCookies(s.url(), re.Cookies())
|
|
|
|
re, err = clt.Get(context.Background(), clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
var clusters *ui.AvailableClusters
|
|
c.Assert(json.Unmarshal(re.Bytes(), &clusters), IsNil)
|
|
|
|
// in absence of session cookie or bearer auth the same request fill fail
|
|
|
|
// no session cookie:
|
|
clt = s.client(roundtrip.BearerAuth(rawSess.Token))
|
|
re, err = clt.Get(context.Background(), clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, NotNil)
|
|
c.Assert(trace.IsAccessDenied(err), Equals, true)
|
|
|
|
// no bearer token:
|
|
clt = s.client(roundtrip.CookieJar(jar))
|
|
re, err = clt.Get(context.Background(), clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, NotNil)
|
|
c.Assert(trace.IsAccessDenied(err), Equals, true)
|
|
}
|
|
|
|
func (s *WebSuite) TestSAMLSuccess(c *C) {
|
|
input := fixtures.SAMLOktaConnectorV2
|
|
|
|
decoder := kyaml.NewYAMLOrJSONDecoder(strings.NewReader(input), defaults.LookaheadBufSize)
|
|
var raw services.UnknownResource
|
|
err := decoder.Decode(&raw)
|
|
c.Assert(err, IsNil)
|
|
|
|
connector, err := services.GetSAMLConnectorMarshaler().UnmarshalSAMLConnector(raw.Raw)
|
|
c.Assert(err, IsNil)
|
|
err = connector.CheckAndSetDefaults()
|
|
|
|
role, err := services.NewRole(connector.GetAttributesToRoles()[0].Roles[0], services.RoleSpecV3{
|
|
Options: services.RoleOptions{
|
|
MaxSessionTTL: services.NewDuration(defaults.MaxCertDuration),
|
|
},
|
|
Allow: services.RoleConditions{
|
|
NodeLabels: services.Labels{services.Wildcard: []string{services.Wildcard}},
|
|
Namespaces: []string{defaults.Namespace},
|
|
Rules: []services.Rule{
|
|
services.NewRule(services.Wildcard, services.RW()),
|
|
},
|
|
},
|
|
})
|
|
c.Assert(err, IsNil)
|
|
role.SetLogins(services.Allow, []string{s.user})
|
|
err = s.server.Auth().UpsertRole(role)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = s.server.Auth().CreateSAMLConnector(connector)
|
|
c.Assert(err, IsNil)
|
|
s.server.AuthServer.AuthServer.SetClock(clockwork.NewFakeClockAt(time.Date(2017, 05, 10, 18, 53, 0, 0, time.UTC)))
|
|
clt := s.clientNoRedirects()
|
|
|
|
csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
|
|
|
|
baseURL, err := url.Parse(clt.Endpoint("webapi", "saml", "sso") + `?redirect_url=http://localhost/after;connector_id=` + connector.GetName())
|
|
c.Assert(err, IsNil)
|
|
req, err := http.NewRequest("GET", baseURL.String(), nil)
|
|
addCSRFCookieToReq(req, csrfToken)
|
|
re, err := clt.Client.RoundTrip(func() (*http.Response, error) {
|
|
return clt.Client.HTTPClient().Do(req)
|
|
})
|
|
|
|
// we got a redirect
|
|
locationURL := re.Headers().Get("Location")
|
|
u, err := url.Parse(locationURL)
|
|
c.Assert(err, IsNil)
|
|
c.Assert(u.Scheme+"://"+u.Host+u.Path, Equals, fixtures.SAMLOktaSSO)
|
|
data, err := base64.StdEncoding.DecodeString(u.Query().Get("SAMLRequest"))
|
|
c.Assert(err, IsNil)
|
|
buf, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(data)))
|
|
c.Assert(err, IsNil)
|
|
doc := etree.NewDocument()
|
|
err = doc.ReadFromBytes(buf)
|
|
c.Assert(err, IsNil)
|
|
id := doc.Root().SelectAttr("ID")
|
|
c.Assert(id, NotNil)
|
|
|
|
authRequest, err := s.server.Auth().GetSAMLAuthRequest(id.Value)
|
|
c.Assert(err, IsNil)
|
|
|
|
// now swap the request id to the hardcoded one in fixtures
|
|
authRequest.ID = fixtures.SAMLOktaAuthRequestID
|
|
authRequest.CSRFToken = csrfToken
|
|
s.server.Auth().Identity.CreateSAMLAuthRequest(*authRequest, backend.Forever)
|
|
|
|
// now respond with pre-recorded request to the POST url
|
|
in := &bytes.Buffer{}
|
|
fw, err := flate.NewWriter(in, flate.DefaultCompression)
|
|
c.Assert(err, IsNil)
|
|
|
|
_, err = fw.Write([]byte(fixtures.SAMLOktaAuthnResponseXML))
|
|
c.Assert(err, IsNil)
|
|
err = fw.Close()
|
|
c.Assert(err, IsNil)
|
|
encodedResponse := base64.StdEncoding.EncodeToString(in.Bytes())
|
|
c.Assert(encodedResponse, NotNil)
|
|
|
|
// now send the response to the server to exchange it for auth session
|
|
form := url.Values{}
|
|
form.Add("SAMLResponse", encodedResponse)
|
|
req, err = http.NewRequest("POST", clt.Endpoint("webapi", "saml", "acs"), strings.NewReader(form.Encode()))
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
addCSRFCookieToReq(req, csrfToken)
|
|
c.Assert(err, IsNil)
|
|
authRe, err := clt.Client.RoundTrip(func() (*http.Response, error) {
|
|
return clt.Client.HTTPClient().Do(req)
|
|
})
|
|
|
|
c.Assert(err, IsNil)
|
|
comment := Commentf("Response: %v", string(authRe.Bytes()))
|
|
c.Assert(authRe.Code(), Equals, http.StatusFound, comment)
|
|
// we have got valid session
|
|
c.Assert(authRe.Headers().Get("Set-Cookie"), Not(Equals), "")
|
|
// we are being redirected to orignal URL
|
|
c.Assert(authRe.Headers().Get("Location"), Equals, "/after")
|
|
}
|
|
|
|
func (s *WebSuite) TestWebSessionsCRUD(c *C) {
|
|
pack := s.authPack(c, "foo")
|
|
|
|
// make sure we can use client to make authenticated requests
|
|
re, err := pack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
var clusters *ui.AvailableClusters
|
|
c.Assert(json.Unmarshal(re.Bytes(), &clusters), IsNil)
|
|
|
|
// now delete session
|
|
_, err = pack.clt.Delete(
|
|
context.Background(),
|
|
pack.clt.Endpoint("webapi", "sessions"))
|
|
c.Assert(err, IsNil)
|
|
|
|
// subsequent requests trying to use this session will fail
|
|
re, err = pack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, NotNil)
|
|
c.Assert(trace.IsAccessDenied(err), Equals, true)
|
|
}
|
|
|
|
func (s *WebSuite) TestNamespace(c *C) {
|
|
pack := s.authPack(c, "foo")
|
|
|
|
_, err := pack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites", s.server.ClusterName(), "namespaces", "..%252fevents%3f", "nodes"), url.Values{})
|
|
c.Assert(err, NotNil)
|
|
|
|
_, err = pack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites", s.server.ClusterName(), "namespaces", "default", "nodes"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
}
|
|
|
|
func (s *WebSuite) TestCSRF(c *C) {
|
|
type input struct {
|
|
reqToken string
|
|
cookieToken string
|
|
}
|
|
|
|
// create a valid user
|
|
user := "csrfuser"
|
|
pass := "abc123"
|
|
otpSecret := base32.StdEncoding.EncodeToString([]byte("def456"))
|
|
s.createUser(c, user, user, pass, otpSecret)
|
|
|
|
// create a valid login form request
|
|
validToken, err := totp.GenerateCode(otpSecret, time.Now())
|
|
c.Assert(err, IsNil)
|
|
loginForm := createSessionReq{
|
|
User: user,
|
|
Pass: pass,
|
|
SecondFactorToken: validToken,
|
|
}
|
|
|
|
encodedToken1 := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
|
|
encodedToken2 := "bf355921bbf3ef3672a03e410d4194077dfa5fe863c652521763b3e7f81e7b11"
|
|
invalid := []input{
|
|
{reqToken: encodedToken2, cookieToken: encodedToken1},
|
|
{reqToken: "", cookieToken: encodedToken1},
|
|
{reqToken: "", cookieToken: ""},
|
|
{reqToken: encodedToken1, cookieToken: ""},
|
|
}
|
|
|
|
clt := s.client()
|
|
|
|
// valid
|
|
_, err = s.login(clt, encodedToken1, encodedToken1, loginForm)
|
|
c.Assert(err, IsNil)
|
|
|
|
// invalid
|
|
for i := range invalid {
|
|
_, err := s.login(clt, invalid[i].cookieToken, invalid[i].reqToken, loginForm)
|
|
c.Assert(err, NotNil)
|
|
c.Assert(trace.IsAccessDenied(err), Equals, true)
|
|
}
|
|
}
|
|
|
|
func (s *WebSuite) TestPasswordChange(c *C) {
|
|
pack := s.authPack(c, "foo")
|
|
fakeClock := clockwork.NewFakeClock()
|
|
s.server.AuthServer.AuthServer.SetClock(fakeClock)
|
|
|
|
validToken, err := totp.GenerateCode(pack.otpSecret, fakeClock.Now())
|
|
c.Assert(err, IsNil)
|
|
|
|
req := changePasswordReq{
|
|
OldPassword: []byte("abc123"),
|
|
NewPassword: []byte("abc1234"),
|
|
SecondFactorToken: validToken,
|
|
}
|
|
|
|
_, err = pack.clt.PutJSON(context.Background(), pack.clt.Endpoint("webapi", "users", "password"), req)
|
|
c.Assert(err, IsNil)
|
|
}
|
|
|
|
func (s *WebSuite) TestWebSessionsRenew(c *C) {
|
|
pack := s.authPack(c, "foo")
|
|
|
|
// make sure we can use client to make authenticated requests
|
|
// before we issue this request, we will recover session id and bearer token
|
|
//
|
|
prevSessionCookie := *pack.cookies[0]
|
|
prevBearerToken := pack.session.Token
|
|
re, err := pack.clt.PostJSON(context.Background(), pack.clt.Endpoint("webapi", "sessions", "renew"), nil)
|
|
c.Assert(err, IsNil)
|
|
|
|
newPack := s.authPackFromResponse(c, re)
|
|
|
|
// new session is functioning
|
|
re, err = newPack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
// old session is stil valid too (until it expires)
|
|
jar, err := cookiejar.New(nil)
|
|
c.Assert(err, IsNil)
|
|
oldClt := s.client(roundtrip.BearerAuth(prevBearerToken), roundtrip.CookieJar(jar))
|
|
jar.SetCookies(s.url(), []*http.Cookie{&prevSessionCookie})
|
|
re, err = oldClt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
// now delete session
|
|
_, err = newPack.clt.Delete(
|
|
context.Background(),
|
|
pack.clt.Endpoint("webapi", "sessions"))
|
|
c.Assert(err, IsNil)
|
|
|
|
// subsequent requests trying to use this session will fail
|
|
re, err = newPack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, NotNil)
|
|
c.Assert(trace.IsAccessDenied(err), Equals, true)
|
|
}
|
|
|
|
func (s *WebSuite) TestWebSessionsBadInput(c *C) {
|
|
user := "bob"
|
|
pass := "abc123"
|
|
rawSecret := "def456"
|
|
otpSecret := base32.StdEncoding.EncodeToString([]byte(rawSecret))
|
|
|
|
err := s.server.Auth().UpsertPassword(user, []byte(pass))
|
|
c.Assert(err, IsNil)
|
|
|
|
err = s.server.Auth().UpsertTOTP(user, otpSecret)
|
|
c.Assert(err, IsNil)
|
|
|
|
// create valid token
|
|
validToken, err := totp.GenerateCode(otpSecret, time.Now())
|
|
c.Assert(err, IsNil)
|
|
|
|
clt := s.client()
|
|
|
|
reqs := []createSessionReq{
|
|
// empty request
|
|
{},
|
|
// missing user
|
|
{
|
|
Pass: pass,
|
|
SecondFactorToken: validToken,
|
|
},
|
|
// missing pass
|
|
{
|
|
User: user,
|
|
SecondFactorToken: validToken,
|
|
},
|
|
// bad pass
|
|
{
|
|
User: user,
|
|
Pass: "bla bla",
|
|
SecondFactorToken: validToken,
|
|
},
|
|
// bad hotp token
|
|
{
|
|
User: user,
|
|
Pass: pass,
|
|
SecondFactorToken: "bad token",
|
|
},
|
|
// missing hotp token
|
|
{
|
|
User: user,
|
|
Pass: pass,
|
|
},
|
|
}
|
|
for i, req := range reqs {
|
|
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "sessions"), req)
|
|
c.Assert(err, NotNil, Commentf("tc %v", i))
|
|
c.Assert(trace.IsAccessDenied(err), Equals, true, Commentf("tc %v %T is not access denied", i, err))
|
|
}
|
|
}
|
|
|
|
type getSiteNodeResponse struct {
|
|
Items []ui.Server `json:"items"`
|
|
}
|
|
|
|
func (s *WebSuite) TestGetSiteNodes(c *C) {
|
|
pack := s.authPack(c, "foo")
|
|
|
|
// get site nodes
|
|
re, err := pack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites", s.server.ClusterName(), "nodes"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
nodes := getSiteNodeResponse{}
|
|
c.Assert(json.Unmarshal(re.Bytes(), &nodes), IsNil)
|
|
c.Assert(len(nodes.Items), Equals, 1)
|
|
|
|
// get site nodes using shortcut
|
|
re, err = pack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites", currentSiteShortcut, "nodes"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
nodes2 := getSiteNodeResponse{}
|
|
c.Assert(json.Unmarshal(re.Bytes(), &nodes2), IsNil)
|
|
c.Assert(len(nodes.Items), Equals, 1)
|
|
c.Assert(nodes2, DeepEquals, nodes)
|
|
}
|
|
|
|
func (s *WebSuite) TestSiteNodeConnectInvalidSessionID(c *C) {
|
|
_, err := s.makeTerminal(s.authPack(c, "foo"), session.ID("/../../../foo"))
|
|
c.Assert(err, NotNil)
|
|
}
|
|
|
|
func (s *WebSuite) TestResolveServerHostPort(c *C) {
|
|
sampleNode := services.ServerV2{}
|
|
sampleNode.SetName("eca53e45-86a9-11e7-a893-0242ac0a0101")
|
|
sampleNode.Spec.Hostname = "nodehostname"
|
|
|
|
// valid cases
|
|
validCases := []struct {
|
|
server string
|
|
nodes []services.Server
|
|
expectedHost string
|
|
expectedPort int
|
|
}{
|
|
{
|
|
server: "localhost",
|
|
expectedHost: "localhost",
|
|
expectedPort: 0,
|
|
},
|
|
{
|
|
server: "localhost:8080",
|
|
expectedHost: "localhost",
|
|
expectedPort: 8080,
|
|
},
|
|
{
|
|
server: "eca53e45-86a9-11e7-a893-0242ac0a0101",
|
|
nodes: []services.Server{&sampleNode},
|
|
expectedHost: "nodehostname",
|
|
expectedPort: 0,
|
|
},
|
|
}
|
|
|
|
// invalid cases
|
|
invalidCases := []struct {
|
|
server string
|
|
expectedErr string
|
|
}{
|
|
{
|
|
server: ":22",
|
|
expectedErr: "empty hostname",
|
|
},
|
|
{
|
|
server: ":",
|
|
expectedErr: "empty hostname",
|
|
},
|
|
{
|
|
server: "",
|
|
expectedErr: "empty server name",
|
|
},
|
|
{
|
|
server: "host:",
|
|
expectedErr: "invalid port",
|
|
},
|
|
{
|
|
server: "host:port",
|
|
expectedErr: "invalid port",
|
|
},
|
|
}
|
|
|
|
for _, testCase := range validCases {
|
|
host, port, err := resolveServerHostPort(testCase.server, testCase.nodes)
|
|
c.Assert(err, IsNil, Commentf(testCase.server))
|
|
c.Assert(host, Equals, testCase.expectedHost)
|
|
c.Assert(port, Equals, testCase.expectedPort)
|
|
}
|
|
|
|
for _, testCase := range invalidCases {
|
|
_, _, err := resolveServerHostPort(testCase.server, nil)
|
|
c.Assert(err, NotNil, Commentf(testCase.expectedErr))
|
|
c.Assert(err, ErrorMatches, ".*"+testCase.expectedErr+".*")
|
|
}
|
|
|
|
}
|
|
|
|
func (s *WebSuite) TestNewTerminalHandler(c *C) {
|
|
validNode := services.ServerV2{}
|
|
validNode.SetName("eca53e45-86a9-11e7-a893-0242ac0a0101")
|
|
validNode.Spec.Hostname = "nodehostname"
|
|
|
|
validServer := "localhost"
|
|
validLogin := "root"
|
|
validSID := session.ID("eca53e45-86a9-11e7-a893-0242ac0a0101")
|
|
validParams := session.TerminalParams{
|
|
H: 1,
|
|
W: 1,
|
|
}
|
|
|
|
makeProvider := func(server services.ServerV2) AuthProvider {
|
|
return authProviderMock{
|
|
server: server,
|
|
}
|
|
}
|
|
|
|
// valid cases
|
|
validCases := []struct {
|
|
req TerminalRequest
|
|
authProvider AuthProvider
|
|
expectedHost string
|
|
expectedPort int
|
|
}{
|
|
{
|
|
req: TerminalRequest{
|
|
Login: validLogin,
|
|
Server: validServer,
|
|
SessionID: validSID,
|
|
Term: validParams,
|
|
},
|
|
authProvider: makeProvider(validNode),
|
|
expectedHost: validServer,
|
|
expectedPort: 0,
|
|
},
|
|
{
|
|
req: TerminalRequest{
|
|
Login: validLogin,
|
|
Server: "eca53e45-86a9-11e7-a893-0242ac0a0101",
|
|
SessionID: validSID,
|
|
Term: validParams,
|
|
},
|
|
authProvider: makeProvider(validNode),
|
|
expectedHost: "nodehostname",
|
|
expectedPort: 0,
|
|
},
|
|
}
|
|
|
|
// invalid cases
|
|
invalidCases := []struct {
|
|
req TerminalRequest
|
|
authProvider AuthProvider
|
|
expectedErr string
|
|
}{
|
|
{
|
|
expectedErr: "invalid session",
|
|
authProvider: makeProvider(validNode),
|
|
req: TerminalRequest{
|
|
SessionID: "",
|
|
Login: validLogin,
|
|
Server: validServer,
|
|
Term: validParams,
|
|
},
|
|
},
|
|
{
|
|
expectedErr: "bad term dimensions",
|
|
authProvider: makeProvider(validNode),
|
|
req: TerminalRequest{
|
|
SessionID: validSID,
|
|
Login: validLogin,
|
|
Server: validServer,
|
|
Term: session.TerminalParams{
|
|
H: -1,
|
|
W: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
expectedErr: "invalid server name",
|
|
authProvider: makeProvider(validNode),
|
|
req: TerminalRequest{
|
|
Server: "localhost:port",
|
|
SessionID: validSID,
|
|
Login: validLogin,
|
|
Term: validParams,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range validCases {
|
|
term, err := NewTerminal(testCase.req, testCase.authProvider, nil)
|
|
c.Assert(err, IsNil)
|
|
c.Assert(term.params, DeepEquals, testCase.req)
|
|
c.Assert(term.hostName, Equals, testCase.expectedHost)
|
|
c.Assert(term.hostPort, Equals, testCase.expectedPort)
|
|
}
|
|
|
|
for _, testCase := range invalidCases {
|
|
_, err := NewTerminal(testCase.req, testCase.authProvider, nil)
|
|
c.Assert(err, ErrorMatches, ".*"+testCase.expectedErr+".*")
|
|
}
|
|
}
|
|
|
|
func (s *WebSuite) TestResizeTerminal(c *C) {
|
|
sid := session.NewID()
|
|
|
|
// Create a new user "foo", open a terminal to a new session, and wait for
|
|
// it to be ready.
|
|
pack1 := s.authPack(c, "foo")
|
|
ws1, err := s.makeTerminal(pack1, sid)
|
|
c.Assert(err, IsNil)
|
|
defer ws1.Close()
|
|
err = s.waitForRawEvent(ws1, 5*time.Second)
|
|
c.Assert(err, IsNil)
|
|
|
|
// Create a new user "bar", open a terminal to the session created above,
|
|
// and wait for it to be ready.
|
|
pack2 := s.authPack(c, "bar")
|
|
ws2, err := s.makeTerminal(pack2, sid)
|
|
c.Assert(err, IsNil)
|
|
defer ws2.Close()
|
|
err = s.waitForRawEvent(ws2, 5*time.Second)
|
|
c.Assert(err, IsNil)
|
|
|
|
// Look at the audit events for the first terminal. It should have two
|
|
// resize events from the second terminal (80x25 default then 100x100). Only
|
|
// the second terminal will get these because resize events are not sent
|
|
// back to the originator.
|
|
err = s.waitForResizeEvent(ws1, 5*time.Second)
|
|
c.Assert(err, IsNil)
|
|
err = s.waitForResizeEvent(ws1, 5*time.Second)
|
|
c.Assert(err, IsNil)
|
|
|
|
// Look at the stream events for the second terminal. We don't expect to see
|
|
// any resize events yet. It will timeout.
|
|
err = s.waitForResizeEvent(ws2, 1*time.Second)
|
|
c.Assert(err, NotNil)
|
|
|
|
// Resize the second terminal. This should be reflected on the first terminal
|
|
// because resize events are not sent back to the originator.
|
|
params, err := session.NewTerminalParamsFromInt(300, 120)
|
|
c.Assert(err, IsNil)
|
|
data, err := json.Marshal(events.EventFields{
|
|
events.EventType: events.ResizeEvent,
|
|
events.EventNamespace: defaults.Namespace,
|
|
events.SessionEventID: sid.String(),
|
|
events.TerminalSize: params.Serialize(),
|
|
})
|
|
envelope := &Envelope{
|
|
Version: defaults.WebsocketVersion,
|
|
Type: defaults.WebsocketResize,
|
|
Payload: string(data),
|
|
}
|
|
envelopeBytes, err := proto.Marshal(envelope)
|
|
c.Assert(err, IsNil)
|
|
websocket.Message.Send(ws2, envelopeBytes)
|
|
|
|
// This time the first terminal will see the resize event.
|
|
err = s.waitForResizeEvent(ws1, 5*time.Second)
|
|
c.Assert(err, IsNil)
|
|
|
|
// The second terminal will not see any resize event. It will timeout.
|
|
err = s.waitForResizeEvent(ws2, 1*time.Second)
|
|
c.Assert(err, NotNil)
|
|
}
|
|
|
|
func (s *WebSuite) TestTerminal(c *C) {
|
|
ws, err := s.makeTerminal(s.authPack(c, "foo"))
|
|
c.Assert(err, IsNil)
|
|
defer ws.Close()
|
|
|
|
termHandler := newTerminalHandler()
|
|
stream, err := termHandler.asTerminalStream(ws)
|
|
c.Assert(err, IsNil)
|
|
|
|
_, err = io.WriteString(stream, "echo vinsong\r\n")
|
|
c.Assert(err, IsNil)
|
|
|
|
err = s.waitForOutput(stream, "vinsong")
|
|
c.Assert(err, IsNil)
|
|
}
|
|
|
|
func (s *WebSuite) TestWebAgentForward(c *C) {
|
|
ws, err := s.makeTerminal(s.authPack(c, "foo"))
|
|
c.Assert(err, IsNil)
|
|
defer ws.Close()
|
|
|
|
termHandler := newTerminalHandler()
|
|
stream, err := termHandler.asTerminalStream(ws)
|
|
c.Assert(err, IsNil)
|
|
|
|
_, err = io.WriteString(stream, "echo $SSH_AUTH_SOCK\r\n")
|
|
c.Assert(err, IsNil)
|
|
|
|
err = s.waitForOutput(stream, "/")
|
|
c.Assert(err, IsNil)
|
|
}
|
|
|
|
func (s *WebSuite) TestActiveSessions(c *C) {
|
|
sid := session.NewID()
|
|
pack := s.authPack(c, "foo")
|
|
|
|
ws, err := s.makeTerminal(pack, sid)
|
|
c.Assert(err, IsNil)
|
|
defer ws.Close()
|
|
|
|
termHandler := newTerminalHandler()
|
|
stream, err := termHandler.asTerminalStream(ws)
|
|
c.Assert(err, IsNil)
|
|
|
|
// To make sure we have a session.
|
|
_, err = io.WriteString(stream, "echo vinsong\r\n")
|
|
c.Assert(err, IsNil)
|
|
|
|
// Make sure server has replied.
|
|
err = s.waitForOutput(stream, "vinsong")
|
|
c.Assert(err, IsNil)
|
|
|
|
// Make sure this session appears in the list of active sessions.
|
|
var sessResp *siteSessionsGetResponse
|
|
for i := 0; i < 10; i++ {
|
|
// Get site nodes and make sure the node has our active party.
|
|
re, err := pack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites", s.server.ClusterName(), "sessions"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
c.Assert(json.Unmarshal(re.Bytes(), &sessResp), IsNil)
|
|
c.Assert(len(sessResp.Sessions), Equals, 1)
|
|
|
|
// Sessions do not appear momentarily as there's async heartbeat
|
|
// procedure.
|
|
time.Sleep(250 * time.Millisecond)
|
|
}
|
|
|
|
c.Assert(len(sessResp.Sessions), Equals, 1)
|
|
c.Assert(sessResp.Sessions[0].ID, Equals, sid)
|
|
}
|
|
|
|
func (s *WebSuite) TestCloseConnectionsOnLogout(c *C) {
|
|
sid := session.NewID()
|
|
pack := s.authPack(c, "foo")
|
|
|
|
ws, err := s.makeTerminal(pack, sid)
|
|
c.Assert(err, IsNil)
|
|
defer ws.Close()
|
|
|
|
termHandler := newTerminalHandler()
|
|
stream, err := termHandler.asTerminalStream(ws)
|
|
c.Assert(err, IsNil)
|
|
|
|
// to make sure we have a session
|
|
_, err = io.WriteString(stream, "expr 137 + 39\r\n")
|
|
c.Assert(err, IsNil)
|
|
|
|
// make sure server has replied
|
|
out := make([]byte, 100)
|
|
stream.Read(out)
|
|
|
|
_, err = pack.clt.Delete(
|
|
context.Background(),
|
|
pack.clt.Endpoint("webapi", "sessions"))
|
|
c.Assert(err, IsNil)
|
|
|
|
// wait until we timeout or detect that connection has been closed
|
|
after := time.After(5 * time.Second)
|
|
errC := make(chan error)
|
|
go func() {
|
|
for {
|
|
_, err := stream.Read(out)
|
|
if err != nil {
|
|
errC <- err
|
|
}
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-after:
|
|
c.Fatalf("timeout")
|
|
case err := <-errC:
|
|
c.Assert(err, Equals, io.EOF)
|
|
}
|
|
}
|
|
|
|
func (s *WebSuite) TestCreateSession(c *C) {
|
|
pack := s.authPack(c, "foo")
|
|
|
|
sess := session.Session{
|
|
TerminalParams: session.TerminalParams{W: 300, H: 120},
|
|
Login: s.user,
|
|
}
|
|
|
|
re, err := pack.clt.PostJSON(
|
|
context.Background(),
|
|
pack.clt.Endpoint("webapi", "sites", s.server.ClusterName(), "sessions"),
|
|
siteSessionGenerateReq{Session: sess},
|
|
)
|
|
c.Assert(err, IsNil)
|
|
|
|
var created *siteSessionGenerateResponse
|
|
c.Assert(json.Unmarshal(re.Bytes(), &created), IsNil)
|
|
c.Assert(created.Session.ID, Not(Equals), "")
|
|
}
|
|
|
|
func (s *WebSuite) TestPlayback(c *C) {
|
|
pack := s.authPack(c, "foo")
|
|
sid := session.NewID()
|
|
ws, err := s.makeTerminal(pack, sid)
|
|
c.Assert(err, IsNil)
|
|
defer ws.Close()
|
|
}
|
|
|
|
func (s *WebSuite) TestNewU2FUser(c *C) {
|
|
// configure cluster authentication preferences
|
|
cap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{
|
|
Type: teleport.Local,
|
|
SecondFactor: teleport.U2F,
|
|
U2F: &services.U2F{
|
|
AppID: "https://" + s.server.ClusterName(),
|
|
Facets: []string{"https://" + s.server.ClusterName()},
|
|
},
|
|
})
|
|
c.Assert(err, IsNil)
|
|
err = s.server.AuthServer.AuthServer.SetAuthPreference(cap)
|
|
c.Assert(err, IsNil)
|
|
|
|
token, err := s.server.Auth().CreateSignupToken(services.UserV1{Name: "bob", AllowedLogins: []string{s.user}}, 0)
|
|
c.Assert(err, IsNil)
|
|
|
|
clt := s.client()
|
|
re, err := clt.Get(context.Background(), clt.Endpoint("webapi", "u2f", "signuptokens", token), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
var u2fRegReq u2f.RegisterRequest
|
|
c.Assert(json.Unmarshal(re.Bytes(), &u2fRegReq), IsNil)
|
|
|
|
u2fRegResp, err := s.mockU2F.RegisterResponse(&u2fRegReq)
|
|
c.Assert(err, IsNil)
|
|
|
|
tempPass := "abc123"
|
|
|
|
re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "users"), createNewU2FUserReq{
|
|
InviteToken: token,
|
|
Pass: tempPass,
|
|
U2FRegisterResponse: *u2fRegResp,
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
var rawSess *createSessionResponseRaw
|
|
c.Assert(json.Unmarshal(re.Bytes(), &rawSess), IsNil)
|
|
cookies := re.Cookies()
|
|
c.Assert(len(cookies), Equals, 1)
|
|
|
|
// now make sure we are logged in by calling authenticated method
|
|
// we need to supply both session cookie and bearer token for
|
|
// request to succeed
|
|
jar, err := cookiejar.New(nil)
|
|
c.Assert(err, IsNil)
|
|
|
|
clt = s.client(roundtrip.BearerAuth(rawSess.Token), roundtrip.CookieJar(jar))
|
|
jar.SetCookies(s.url(), re.Cookies())
|
|
|
|
re, err = clt.Get(context.Background(), clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
var clusters *ui.AvailableClusters
|
|
c.Assert(json.Unmarshal(re.Bytes(), &clusters), IsNil)
|
|
|
|
// in absence of session cookie or bearer auth the same request fill fail
|
|
|
|
// no session cookie:
|
|
clt = s.client(roundtrip.BearerAuth(rawSess.Token))
|
|
re, err = clt.Get(context.Background(), clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, NotNil)
|
|
c.Assert(trace.IsAccessDenied(err), Equals, true)
|
|
|
|
// no bearer token:
|
|
clt = s.client(roundtrip.CookieJar(jar))
|
|
re, err = clt.Get(context.Background(), clt.Endpoint("webapi", "sites"), url.Values{})
|
|
c.Assert(err, NotNil)
|
|
c.Assert(trace.IsAccessDenied(err), Equals, true)
|
|
}
|
|
|
|
func (s *WebSuite) TestU2FLogin(c *C) {
|
|
// configure cluster authentication preferences
|
|
cap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{
|
|
Type: teleport.Local,
|
|
SecondFactor: teleport.U2F,
|
|
U2F: &services.U2F{
|
|
AppID: "https://" + s.server.ClusterName(),
|
|
Facets: []string{"https://" + s.server.ClusterName()},
|
|
},
|
|
})
|
|
c.Assert(err, IsNil)
|
|
err = s.server.Auth().SetAuthPreference(cap)
|
|
c.Assert(err, IsNil)
|
|
|
|
token, err := s.server.Auth().CreateSignupToken(services.UserV1{Name: "bob", AllowedLogins: []string{s.user}}, 0)
|
|
c.Assert(err, IsNil)
|
|
|
|
u2fRegReq, err := s.proxyClient.GetSignupU2FRegisterRequest(token)
|
|
c.Assert(err, IsNil)
|
|
|
|
u2fRegResp, err := s.mockU2F.RegisterResponse(u2fRegReq)
|
|
c.Assert(err, IsNil)
|
|
|
|
tempPass := "abc123"
|
|
|
|
_, err = s.proxyClient.CreateUserWithU2FToken(token, tempPass, *u2fRegResp)
|
|
c.Assert(err, IsNil)
|
|
|
|
// normal login
|
|
clt := s.client()
|
|
re, err := clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.U2fSignRequestReq{
|
|
User: "bob",
|
|
Pass: tempPass,
|
|
})
|
|
c.Assert(err, IsNil)
|
|
var u2fSignReq u2f.SignRequest
|
|
c.Assert(json.Unmarshal(re.Bytes(), &u2fSignReq), IsNil)
|
|
|
|
u2fSignResp, err := s.mockU2F.SignResponse(&u2fSignReq)
|
|
c.Assert(err, IsNil)
|
|
|
|
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), u2fSignResponseReq{
|
|
User: "bob",
|
|
U2FSignResponse: *u2fSignResp,
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
// bad login: corrupted sign responses, should fail
|
|
|
|
re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.U2fSignRequestReq{
|
|
User: "bob",
|
|
Pass: tempPass,
|
|
})
|
|
c.Assert(err, IsNil)
|
|
c.Assert(json.Unmarshal(re.Bytes(), &u2fSignReq), IsNil)
|
|
|
|
u2fSignResp, err = s.mockU2F.SignResponse(&u2fSignReq)
|
|
c.Assert(err, IsNil)
|
|
|
|
// corrupted KeyHandle
|
|
u2fSignRespCopy := u2fSignResp
|
|
u2fSignRespCopy.KeyHandle = u2fSignRespCopy.KeyHandle + u2fSignRespCopy.KeyHandle
|
|
|
|
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), u2fSignResponseReq{
|
|
User: "bob",
|
|
U2FSignResponse: *u2fSignRespCopy,
|
|
})
|
|
c.Assert(err, NotNil)
|
|
|
|
// corrupted SignatureData
|
|
u2fSignRespCopy = u2fSignResp
|
|
u2fSignRespCopy.SignatureData = u2fSignRespCopy.SignatureData[:10] + u2fSignRespCopy.SignatureData[20:]
|
|
|
|
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), u2fSignResponseReq{
|
|
User: "bob",
|
|
U2FSignResponse: *u2fSignRespCopy,
|
|
})
|
|
c.Assert(err, NotNil)
|
|
|
|
// corrupted ClientData
|
|
u2fSignRespCopy = u2fSignResp
|
|
u2fSignRespCopy.ClientData = u2fSignRespCopy.ClientData[:10] + u2fSignRespCopy.ClientData[20:]
|
|
|
|
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), u2fSignResponseReq{
|
|
User: "bob",
|
|
U2FSignResponse: *u2fSignRespCopy,
|
|
})
|
|
c.Assert(err, NotNil)
|
|
|
|
// bad login: counter not increasing, should fail
|
|
|
|
s.mockU2F.SetCounter(0)
|
|
|
|
re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.U2fSignRequestReq{
|
|
User: "bob",
|
|
Pass: tempPass,
|
|
})
|
|
c.Assert(err, IsNil)
|
|
c.Assert(json.Unmarshal(re.Bytes(), &u2fSignReq), IsNil)
|
|
|
|
u2fSignResp, err = s.mockU2F.SignResponse(&u2fSignReq)
|
|
c.Assert(err, IsNil)
|
|
|
|
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), u2fSignResponseReq{
|
|
User: "bob",
|
|
U2FSignResponse: *u2fSignResp,
|
|
})
|
|
c.Assert(err, NotNil)
|
|
}
|
|
|
|
// TestPing ensures that a response is returned by /webapi/ping
|
|
// and that that response body contains authentication information.
|
|
func (s *WebSuite) TestPing(c *C) {
|
|
wc := s.client()
|
|
|
|
re, err := wc.Get(context.Background(), wc.Endpoint("webapi", "ping"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
|
|
var out *client.PingResponse
|
|
c.Assert(json.Unmarshal(re.Bytes(), &out), IsNil)
|
|
|
|
preference, err := s.server.Auth().GetAuthPreference()
|
|
c.Assert(err, IsNil)
|
|
|
|
c.Assert(out.Auth.Type, Equals, preference.GetType())
|
|
c.Assert(out.Auth.SecondFactor, Equals, preference.GetSecondFactor())
|
|
}
|
|
|
|
func (s *WebSuite) TestMultipleConnectors(c *C) {
|
|
wc := s.client()
|
|
|
|
// create two oidc connectors, one named "foo" and another named "bar"
|
|
oidcConnectorSpec := services.OIDCConnectorSpecV2{
|
|
RedirectURL: "https://localhost:3080/v1/webapi/oidc/callback",
|
|
ClientID: "000000000000-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com",
|
|
ClientSecret: "AAAAAAAAAAAAAAAAAAAAAAAA",
|
|
IssuerURL: "https://oidc.example.com",
|
|
Display: "Login with Example",
|
|
Scope: []string{"group"},
|
|
ClaimsToRoles: []services.ClaimMapping{
|
|
{
|
|
Claim: "group",
|
|
Value: "admin",
|
|
Roles: []string{"admin"},
|
|
},
|
|
},
|
|
}
|
|
err := s.server.Auth().UpsertOIDCConnector(services.NewOIDCConnector("foo", oidcConnectorSpec))
|
|
c.Assert(err, IsNil)
|
|
err = s.server.Auth().UpsertOIDCConnector(services.NewOIDCConnector("bar", oidcConnectorSpec))
|
|
c.Assert(err, IsNil)
|
|
|
|
// set the auth preferences to oidc with no connector name
|
|
authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{
|
|
Type: "oidc",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
err = s.server.Auth().SetAuthPreference(authPreference)
|
|
c.Assert(err, IsNil)
|
|
|
|
// hit the ping endpoint to get the auth type and connector name
|
|
re, err := wc.Get(context.Background(), wc.Endpoint("webapi", "ping"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
var out *client.PingResponse
|
|
c.Assert(json.Unmarshal(re.Bytes(), &out), IsNil)
|
|
|
|
// make sure the connector name we got back was the first connector
|
|
// in the backend, in this case it's "bar"
|
|
oidcConnectors, err := s.server.Auth().GetOIDCConnectors(false)
|
|
c.Assert(err, IsNil)
|
|
c.Assert(out.Auth.OIDC.Name, Equals, oidcConnectors[0].GetName())
|
|
|
|
// update the auth preferences and this time specify the connector name
|
|
authPreference, err = services.NewAuthPreference(services.AuthPreferenceSpecV2{
|
|
Type: "oidc",
|
|
ConnectorName: "foo",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
err = s.server.Auth().SetAuthPreference(authPreference)
|
|
c.Assert(err, IsNil)
|
|
|
|
// hit the ping endpoing to get the auth type and connector name
|
|
re, err = wc.Get(context.Background(), wc.Endpoint("webapi", "ping"), url.Values{})
|
|
c.Assert(err, IsNil)
|
|
c.Assert(json.Unmarshal(re.Bytes(), &out), IsNil)
|
|
|
|
// make sure the connector we get back is "foo"
|
|
c.Assert(out.Auth.OIDC.Name, Equals, "foo")
|
|
}
|
|
|
|
// TestConstructSSHResponse checks if the secret package uses AES-GCM to
|
|
// encrypt and decrypt data that passes through the ConstructSSHResponse
|
|
// function.
|
|
func (s *WebSuite) TestConstructSSHResponse(c *C) {
|
|
key, err := secret.NewKey()
|
|
c.Assert(err, IsNil)
|
|
|
|
u, err := url.Parse("http://www.example.com/callback")
|
|
c.Assert(err, IsNil)
|
|
query := u.Query()
|
|
query.Set("secret_key", key.String())
|
|
u.RawQuery = query.Encode()
|
|
|
|
rawresp, err := ConstructSSHResponse(AuthParams{
|
|
Username: "foo",
|
|
Cert: []byte{0x00},
|
|
TLSCert: []byte{0x01},
|
|
ClientRedirectURL: u.String(),
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
c.Assert(rawresp.Query().Get("secret"), Equals, "")
|
|
c.Assert(rawresp.Query().Get("secret_key"), Equals, "")
|
|
c.Assert(rawresp.Query().Get("response"), Not(Equals), "")
|
|
|
|
plaintext, err := key.Open([]byte(rawresp.Query().Get("response")))
|
|
c.Assert(err, IsNil)
|
|
|
|
var resp *auth.SSHLoginResponse
|
|
err = json.Unmarshal(plaintext, &resp)
|
|
c.Assert(err, IsNil)
|
|
c.Assert(resp.Username, Equals, "foo")
|
|
c.Assert(resp.Cert, DeepEquals, []byte{0x00})
|
|
c.Assert(resp.TLSCert, DeepEquals, []byte{0x01})
|
|
}
|
|
|
|
// TestConstructSSHResponseLegacy checks if the secret package uses NaCl to
|
|
// encrypt and decrypt data that passes through the ConstructSSHResponse
|
|
// function.
|
|
func (s *WebSuite) TestConstructSSHResponseLegacy(c *C) {
|
|
key, err := lemma_secret.NewKey()
|
|
c.Assert(err, IsNil)
|
|
|
|
lemma, err := lemma_secret.New(&lemma_secret.Config{KeyBytes: key})
|
|
c.Assert(err, IsNil)
|
|
|
|
u, err := url.Parse("http://www.example.com/callback")
|
|
c.Assert(err, IsNil)
|
|
query := u.Query()
|
|
query.Set("secret", lemma_secret.KeyToEncodedString(key))
|
|
u.RawQuery = query.Encode()
|
|
|
|
rawresp, err := ConstructSSHResponse(AuthParams{
|
|
Username: "foo",
|
|
Cert: []byte{0x00},
|
|
TLSCert: []byte{0x01},
|
|
ClientRedirectURL: u.String(),
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
c.Assert(rawresp.Query().Get("secret"), Equals, "")
|
|
c.Assert(rawresp.Query().Get("secret_key"), Equals, "")
|
|
c.Assert(rawresp.Query().Get("response"), Not(Equals), "")
|
|
|
|
var sealedData *lemma_secret.SealedBytes
|
|
err = json.Unmarshal([]byte(rawresp.Query().Get("response")), &sealedData)
|
|
c.Assert(err, IsNil)
|
|
|
|
plaintext, err := lemma.Open(sealedData)
|
|
c.Assert(err, IsNil)
|
|
|
|
var resp *auth.SSHLoginResponse
|
|
err = json.Unmarshal(plaintext, &resp)
|
|
c.Assert(err, IsNil)
|
|
c.Assert(resp.Username, Equals, "foo")
|
|
c.Assert(resp.Cert, DeepEquals, []byte{0x00})
|
|
c.Assert(resp.TLSCert, DeepEquals, []byte{0x01})
|
|
}
|
|
|
|
// TestSearchClusterEvents makes sure web API allows querying events by type.
|
|
func (s *WebSuite) TestSearchClusterEvents(c *C) {
|
|
e1 := events.EventFields{
|
|
events.EventID: uuid.New(),
|
|
events.EventType: "event.1",
|
|
events.EventCode: "event.1",
|
|
events.EventTime: s.clock.Now().Format(time.RFC3339),
|
|
}
|
|
e2 := events.EventFields{
|
|
events.EventID: uuid.New(),
|
|
events.EventType: "event.2",
|
|
events.EventCode: "event.2",
|
|
events.EventTime: s.clock.Now().Format(time.RFC3339),
|
|
}
|
|
e3 := events.EventFields{
|
|
events.EventID: uuid.New(),
|
|
events.EventType: "event.3",
|
|
events.EventCode: "event.3",
|
|
events.EventTime: s.clock.Now().Format(time.RFC3339),
|
|
}
|
|
e4 := events.EventFields{
|
|
events.EventID: uuid.New(),
|
|
events.EventType: "event.3",
|
|
events.EventCode: "event.3",
|
|
events.EventTime: s.clock.Now().Format(time.RFC3339),
|
|
}
|
|
e5 := events.EventFields{
|
|
events.EventID: uuid.New(),
|
|
events.EventType: "event.1",
|
|
events.EventCode: "event.1",
|
|
events.EventTime: s.clock.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
for _, e := range []events.EventFields{e1, e2, e3, e4, e5} {
|
|
c.Assert(s.proxyClient.EmitAuditEvent(events.Event{Name: e.GetType()}, e), IsNil)
|
|
}
|
|
|
|
testCases := []struct {
|
|
// Comment is the test case description.
|
|
Comment string
|
|
// Query is the search query sent to the API.
|
|
Query url.Values
|
|
// Result is the expected returned list of events.
|
|
Result []events.EventFields
|
|
}{
|
|
{
|
|
Comment: "Empty query",
|
|
Query: url.Values{},
|
|
Result: []events.EventFields{e1, e2, e3, e4, e5},
|
|
},
|
|
{
|
|
Comment: "Query by single event type",
|
|
Query: url.Values{"include": []string{"event.1"}},
|
|
Result: []events.EventFields{e1, e5},
|
|
},
|
|
{
|
|
Comment: "Query by two event types",
|
|
Query: url.Values{"include": []string{"event.2;event.3"}},
|
|
Result: []events.EventFields{e2, e3, e4},
|
|
},
|
|
{
|
|
Comment: "Query with limit",
|
|
Query: url.Values{"include": []string{"event.2;event.3"}, "limit": []string{"1"}},
|
|
Result: []events.EventFields{e2},
|
|
},
|
|
}
|
|
|
|
pack := s.authPack(c, "foo")
|
|
for _, tc := range testCases {
|
|
result := s.searchEvents(c, pack.clt, tc.Query, []string{"event.1", "event.2", "event.3"})
|
|
c.Assert(result, DeepEquals, tc.Result, Commentf(tc.Comment))
|
|
}
|
|
}
|
|
|
|
func (s *WebSuite) searchEvents(c *C, clt *client.WebClient, query url.Values, filter []string) (result []events.EventFields) {
|
|
response, err := clt.Get(context.Background(), clt.Endpoint("webapi", "sites", s.server.ClusterName(), "events", "search"), query)
|
|
c.Assert(err, IsNil)
|
|
var out eventsListGetResponse
|
|
c.Assert(json.Unmarshal(response.Bytes(), &out), IsNil)
|
|
for _, event := range out.Events {
|
|
if utils.SliceContainsStr(filter, event.GetType()) {
|
|
result = append(result, event)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
type authProviderMock struct {
|
|
server services.ServerV2
|
|
}
|
|
|
|
func (mock authProviderMock) GetNodes(n string, opts ...services.MarshalOption) ([]services.Server, error) {
|
|
return []services.Server{&mock.server}, nil
|
|
}
|
|
|
|
func (mock authProviderMock) GetSessionEvents(n string, s session.ID, c int, p bool) ([]events.EventFields, error) {
|
|
return []events.EventFields{}, nil
|
|
}
|
|
|
|
func (s *WebSuite) makeTerminal(pack *authPack, opts ...session.ID) (*websocket.Conn, error) {
|
|
var sessionID session.ID
|
|
if len(opts) == 0 {
|
|
sessionID = session.NewID()
|
|
} else {
|
|
sessionID = opts[0]
|
|
}
|
|
|
|
u := url.URL{
|
|
Host: s.url().Host,
|
|
Scheme: client.WSS,
|
|
Path: fmt.Sprintf("/v1/webapi/sites/%v/connect", currentSiteShortcut),
|
|
}
|
|
data, err := json.Marshal(TerminalRequest{
|
|
Server: s.srvID,
|
|
Login: pack.login,
|
|
Term: session.TerminalParams{
|
|
W: 100,
|
|
H: 100,
|
|
},
|
|
SessionID: sessionID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
q := u.Query()
|
|
q.Set("params", string(data))
|
|
q.Set(roundtrip.AccessTokenQueryParam, pack.session.Token)
|
|
u.RawQuery = q.Encode()
|
|
|
|
wscfg, err := websocket.NewConfig(u.String(), "http://localhost")
|
|
wscfg.TlsConfig = &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, cookie := range pack.cookies {
|
|
wscfg.Header.Add("Cookie", cookie.String())
|
|
}
|
|
|
|
ws, err := websocket.DialConfig(wscfg)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return ws, nil
|
|
}
|
|
|
|
func (s *WebSuite) waitForOutput(stream *terminalStream, substr string) error {
|
|
tickerCh := time.Tick(250 * time.Millisecond)
|
|
timeoutCh := time.After(10 * time.Second)
|
|
|
|
for {
|
|
select {
|
|
case <-tickerCh:
|
|
out := make([]byte, 100)
|
|
_, err := stream.Read(out)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if strings.Contains(removeSpace(string(out)), substr) {
|
|
return nil
|
|
}
|
|
case <-timeoutCh:
|
|
return trace.BadParameter("timeout waiting on terminal for output: %v", substr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *WebSuite) waitForRawEvent(ws *websocket.Conn, timeout time.Duration) error {
|
|
timeoutContext, timeoutCancel := context.WithTimeout(context.Background(), timeout)
|
|
defer timeoutCancel()
|
|
doneContext, doneCancel := context.WithCancel(context.Background())
|
|
defer doneCancel()
|
|
|
|
go func() {
|
|
for {
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
var raw []byte
|
|
err := websocket.Message.Receive(ws, &raw)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var envelope Envelope
|
|
err = proto.Unmarshal(raw, &envelope)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if envelope.GetType() == defaults.WebsocketRaw {
|
|
doneCancel()
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-timeoutContext.Done():
|
|
return trace.BadParameter("timeout waiting for resize event")
|
|
case <-doneContext.Done():
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *WebSuite) waitForResizeEvent(ws *websocket.Conn, timeout time.Duration) error {
|
|
timeoutContext, timeoutCancel := context.WithTimeout(context.Background(), timeout)
|
|
defer timeoutCancel()
|
|
doneContext, doneCancel := context.WithCancel(context.Background())
|
|
defer doneCancel()
|
|
|
|
go func() {
|
|
for {
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
var raw []byte
|
|
err := websocket.Message.Receive(ws, &raw)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var envelope Envelope
|
|
err = proto.Unmarshal(raw, &envelope)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if envelope.GetType() != defaults.WebsocketAudit {
|
|
continue
|
|
}
|
|
|
|
var e events.EventFields
|
|
err = json.Unmarshal([]byte(envelope.GetPayload()), &e)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if e.GetType() == events.ResizeEvent {
|
|
doneCancel()
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-timeoutContext.Done():
|
|
return trace.BadParameter("timeout waiting for resize event")
|
|
case <-doneContext.Done():
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *WebSuite) clientNoRedirects(opts ...roundtrip.ClientParam) *client.WebClient {
|
|
hclient := client.NewInsecureWebClient()
|
|
hclient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}
|
|
opts = append(opts, roundtrip.HTTPClient(hclient))
|
|
wc, err := client.NewWebClient(s.url().String(), opts...)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return wc
|
|
}
|
|
|
|
func (s *WebSuite) client(opts ...roundtrip.ClientParam) *client.WebClient {
|
|
opts = append(opts, roundtrip.HTTPClient(client.NewInsecureWebClient()))
|
|
wc, err := client.NewWebClient(s.url().String(), opts...)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return wc
|
|
}
|
|
|
|
func (s *WebSuite) login(clt *client.WebClient, cookieToken string, reqToken string, reqData interface{}) (*roundtrip.Response, error) {
|
|
return httplib.ConvertResponse(clt.RoundTrip(func() (*http.Response, error) {
|
|
data, err := json.Marshal(reqData)
|
|
req, err := http.NewRequest("POST", clt.Endpoint("webapi", "sessions"), bytes.NewBuffer(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addCSRFCookieToReq(req, cookieToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set(csrf.HeaderName, reqToken)
|
|
return clt.HTTPClient().Do(req)
|
|
}))
|
|
}
|
|
|
|
func (s *WebSuite) url() *url.URL {
|
|
u, err := url.Parse("https://" + s.webServer.Listener.Addr().String())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return u
|
|
}
|
|
|
|
func addCSRFCookieToReq(req *http.Request, token string) {
|
|
cookie := &http.Cookie{
|
|
Name: csrf.CookieName,
|
|
Value: token,
|
|
}
|
|
|
|
req.AddCookie(cookie)
|
|
}
|
|
|
|
func removeSpace(in string) string {
|
|
for _, c := range []string{"\n", "\r", "\t"} {
|
|
in = strings.Replace(in, c, " ", -1)
|
|
}
|
|
return strings.TrimSpace(in)
|
|
}
|
|
|
|
func newTerminalHandler() TerminalHandler {
|
|
return TerminalHandler{
|
|
log: logrus.WithFields(logrus.Fields{}),
|
|
encoder: unicode.UTF8.NewEncoder(),
|
|
decoder: unicode.UTF8.NewDecoder(),
|
|
}
|
|
}
|