1/*
2Copyright (c) 2017-2018 VMware, Inc. All Rights Reserved.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package simulator
18
19import (
20	"context"
21	"fmt"
22	"net/http"
23	"strings"
24	"time"
25
26	"github.com/google/uuid"
27	"github.com/vmware/govmomi/object"
28	"github.com/vmware/govmomi/session"
29	"github.com/vmware/govmomi/vim25/methods"
30	"github.com/vmware/govmomi/vim25/mo"
31	"github.com/vmware/govmomi/vim25/soap"
32	"github.com/vmware/govmomi/vim25/types"
33)
34
35type SessionManager struct {
36	mo.SessionManager
37
38	ServiceHostName string
39
40	sessions map[string]Session
41}
42
43func NewSessionManager(ref types.ManagedObjectReference) object.Reference {
44	s := &SessionManager{
45		sessions: make(map[string]Session),
46	}
47	s.Self = ref
48	return s
49}
50
51func createSession(ctx *Context, name string, locale string) types.UserSession {
52	now := time.Now().UTC()
53
54	if locale == "" {
55		locale = session.Locale
56	}
57
58	session := Session{
59		UserSession: types.UserSession{
60			Key:            uuid.New().String(),
61			UserName:       name,
62			FullName:       name,
63			LoginTime:      now,
64			LastActiveTime: now,
65			Locale:         locale,
66			MessageLocale:  locale,
67		},
68		Registry: NewRegistry(),
69	}
70
71	ctx.SetSession(session, true)
72
73	return session.UserSession
74}
75
76func (s *SessionManager) Login(ctx *Context, req *types.Login) soap.HasFault {
77	body := new(methods.LoginBody)
78
79	if req.UserName == "" || req.Password == "" || ctx.Session != nil {
80		body.Fault_ = invalidLogin
81	} else {
82		body.Res = &types.LoginResponse{
83			Returnval: createSession(ctx, req.UserName, req.Locale),
84		}
85	}
86
87	return body
88}
89
90func (s *SessionManager) LoginExtensionByCertificate(ctx *Context, req *types.LoginExtensionByCertificate) soap.HasFault {
91	body := new(methods.LoginExtensionByCertificateBody)
92
93	if ctx.req.TLS == nil || len(ctx.req.TLS.PeerCertificates) == 0 {
94		body.Fault_ = Fault("", new(types.NoClientCertificate))
95		return body
96	}
97
98	if req.ExtensionKey == "" || ctx.Session != nil {
99		body.Fault_ = invalidLogin
100	} else {
101		body.Res = &types.LoginExtensionByCertificateResponse{
102			Returnval: createSession(ctx, req.ExtensionKey, req.Locale),
103		}
104	}
105
106	return body
107}
108
109func (s *SessionManager) LoginByToken(ctx *Context, req *types.LoginByToken) soap.HasFault {
110	body := new(methods.LoginByTokenBody)
111
112	if ctx.Session != nil {
113		body.Fault_ = invalidLogin
114	} else {
115		var subject struct {
116			ID string `xml:"Assertion>Subject>NameID"`
117		}
118
119		if s, ok := ctx.Header.Security.(*Element); ok {
120			_ = s.Decode(&subject)
121		}
122
123		if subject.ID == "" {
124			body.Fault_ = invalidLogin
125			return body
126		}
127
128		body.Res = &types.LoginByTokenResponse{
129			Returnval: createSession(ctx, subject.ID, req.Locale),
130		}
131	}
132
133	return body
134}
135
136func (s *SessionManager) Logout(ctx *Context, _ *types.Logout) soap.HasFault {
137	session := ctx.Session
138	delete(s.sessions, session.Key)
139	pc := Map.content().PropertyCollector
140
141	for ref, obj := range ctx.Session.Registry.objects {
142		if ref == pc {
143			continue // don't unregister the PropertyCollector singleton
144		}
145		if _, ok := obj.(RegisterObject); ok {
146			ctx.Map.Remove(ref) // Remove RegisterObject handlers
147		}
148	}
149
150	ctx.postEvent(&types.UserLogoutSessionEvent{
151		IpAddress: session.IpAddress,
152		UserAgent: session.UserAgent,
153		SessionId: session.Key,
154		LoginTime: &session.LoginTime,
155	})
156
157	return &methods.LogoutBody{Res: new(types.LogoutResponse)}
158}
159
160func (s *SessionManager) TerminateSession(ctx *Context, req *types.TerminateSession) soap.HasFault {
161	body := new(methods.TerminateSessionBody)
162
163	for _, id := range req.SessionId {
164		if id == ctx.Session.Key {
165			body.Fault_ = Fault("", new(types.InvalidArgument))
166			return body
167		}
168		delete(s.sessions, id)
169	}
170
171	body.Res = new(types.TerminateSessionResponse)
172	return body
173}
174
175func (s *SessionManager) SessionIsActive(ctx *Context, req *types.SessionIsActive) soap.HasFault {
176	body := new(methods.SessionIsActiveBody)
177
178	if ctx.Map.IsESX() {
179		body.Fault_ = Fault("", new(types.NotImplemented))
180		return body
181	}
182
183	body.Res = new(types.SessionIsActiveResponse)
184
185	if session, exists := s.sessions[req.SessionID]; exists {
186		body.Res.Returnval = session.UserName == req.UserName
187	}
188
189	return body
190}
191
192func (s *SessionManager) AcquireCloneTicket(ctx *Context, _ *types.AcquireCloneTicket) soap.HasFault {
193	session := *ctx.Session
194	session.Key = uuid.New().String()
195	s.sessions[session.Key] = session
196
197	return &methods.AcquireCloneTicketBody{
198		Res: &types.AcquireCloneTicketResponse{
199			Returnval: session.Key,
200		},
201	}
202}
203
204func (s *SessionManager) CloneSession(ctx *Context, ticket *types.CloneSession) soap.HasFault {
205	body := new(methods.CloneSessionBody)
206
207	session, exists := s.sessions[ticket.CloneTicket]
208
209	if exists {
210		delete(s.sessions, ticket.CloneTicket) // A clone ticket can only be used once
211		session.Key = uuid.New().String()
212		ctx.SetSession(session, true)
213
214		body.Res = &types.CloneSessionResponse{
215			Returnval: session.UserSession,
216		}
217	} else {
218		body.Fault_ = invalidLogin
219	}
220
221	return body
222}
223
224func (s *SessionManager) AcquireGenericServiceTicket(ticket *types.AcquireGenericServiceTicket) soap.HasFault {
225	return &methods.AcquireGenericServiceTicketBody{
226		Res: &types.AcquireGenericServiceTicketResponse{
227			Returnval: types.SessionManagerGenericServiceTicket{
228				Id:       uuid.New().String(),
229				HostName: s.ServiceHostName,
230			},
231		},
232	}
233}
234
235// internalContext is the session for use by the in-memory client (Service.RoundTrip)
236var internalContext = &Context{
237	Context: context.Background(),
238	Session: &Session{
239		UserSession: types.UserSession{
240			Key: uuid.New().String(),
241		},
242		Registry: NewRegistry(),
243	},
244	Map: Map,
245}
246
247var invalidLogin = Fault("Login failure", new(types.InvalidLogin))
248
249// Context provides per-request Session management.
250type Context struct {
251	req *http.Request
252	res http.ResponseWriter
253	svc *Service
254
255	context.Context
256	Session *Session
257	Header  soap.Header
258	Caller  *types.ManagedObjectReference
259	Map     *Registry
260}
261
262// mapSession maps an HTTP cookie to a Session.
263func (c *Context) mapSession() {
264	if cookie, err := c.req.Cookie(soap.SessionCookieName); err == nil {
265		if val, ok := c.svc.sm.sessions[cookie.Value]; ok {
266			c.SetSession(val, false)
267		}
268	}
269}
270
271// SetSession should be called after successful authentication.
272func (c *Context) SetSession(session Session, login bool) {
273	session.UserAgent = c.req.UserAgent()
274	session.IpAddress = strings.Split(c.req.RemoteAddr, ":")[0]
275	session.LastActiveTime = time.Now()
276
277	c.svc.sm.sessions[session.Key] = session
278	c.Session = &session
279
280	if login {
281		http.SetCookie(c.res, &http.Cookie{
282			Name:  soap.SessionCookieName,
283			Value: session.Key,
284		})
285
286		c.postEvent(&types.UserLoginSessionEvent{
287			SessionId: session.Key,
288			IpAddress: session.IpAddress,
289			UserAgent: session.UserAgent,
290			Locale:    session.Locale,
291		})
292	}
293}
294
295// WithLock holds a lock for the given object while then given function is run.
296func (c *Context) WithLock(obj mo.Reference, f func()) {
297	if c.Caller != nil && *c.Caller == obj.Reference() {
298		// Internal method invocation, obj is already locked
299		f()
300		return
301	}
302	Map.WithLock(obj, f)
303}
304
305// postEvent wraps EventManager.PostEvent for internal use, with a lock on the EventManager.
306func (c *Context) postEvent(events ...types.BaseEvent) {
307	m := Map.EventManager()
308	c.WithLock(m, func() {
309		for _, event := range events {
310			m.PostEvent(c, &types.PostEvent{EventToPost: event})
311		}
312	})
313}
314
315// Session combines a UserSession and a Registry for per-session managed objects.
316type Session struct {
317	types.UserSession
318	*Registry
319}
320
321// Put wraps Registry.Put, setting the moref value to include the session key.
322func (s *Session) Put(item mo.Reference) mo.Reference {
323	ref := item.Reference()
324	if ref.Value == "" {
325		ref.Value = fmt.Sprintf("session[%s]%s", s.Key, uuid.New())
326	}
327	s.Registry.setReference(item, ref)
328	return s.Registry.Put(item)
329}
330
331// Get wraps Registry.Get, session-izing singleton objects such as SessionManager and the root PropertyCollector.
332func (s *Session) Get(ref types.ManagedObjectReference) mo.Reference {
333	obj := s.Registry.Get(ref)
334	if obj != nil {
335		return obj
336	}
337
338	// Return a session "view" of certain singleton objects
339	switch ref.Type {
340	case "SessionManager":
341		// Clone SessionManager so the PropertyCollector can properly report CurrentSession
342		m := *Map.SessionManager()
343		m.CurrentSession = &s.UserSession
344
345		// TODO: we could maintain SessionList as part of the SessionManager singleton
346		for _, session := range m.sessions {
347			m.SessionList = append(m.SessionList, session.UserSession)
348		}
349
350		return &m
351	case "PropertyCollector":
352		if ref == Map.content().PropertyCollector {
353			return s.Put(NewPropertyCollector(ref))
354		}
355	}
356
357	return Map.Get(ref)
358}
359