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