1 package emulatorsvc
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "os"
12 "time"
13
14 "github.com/go-logr/logr"
15 "github.com/shurcooL/graphql"
16 "golang.org/x/oauth2"
17
18 "edge-infra.dev/pkg/edge/client"
19 "edge-infra.dev/pkg/lib/fog"
20 "edge-infra.dev/pkg/lib/uuid"
21 "edge-infra.dev/pkg/sds/emergencyaccess/apierror"
22 apierrorhandler "edge-infra.dev/pkg/sds/emergencyaccess/apierror/handler"
23 "edge-infra.dev/pkg/sds/emergencyaccess/eaconst"
24 eamiddleware "edge-infra.dev/pkg/sds/emergencyaccess/middleware"
25 "edge-infra.dev/pkg/sds/emergencyaccess/msgdata"
26 "edge-infra.dev/pkg/sds/emergencyaccess/types"
27 )
28
29 type sessionIDType string
30
31 const (
32 envGatewayHost = "RCLI_GATEWAY_HOST"
33 sessionID sessionIDType = "X-SessionID"
34 edgeSessionCookieName = "edge-session"
35 )
36
37
38 type gatewayURLs struct {
39 start *url.URL
40 send *url.URL
41 end *url.URL
42 }
43
44
45 type sessionDetails struct {
46 cancelFunc context.CancelFunc
47 context context.Context
48 ID string
49 target types.Target
50 }
51
52 type EmulatorService struct {
53 config *Config
54 client *http.Client
55
56 idToken *oauth2.Token
57 userID string
58
59 session *sessionDetails
60 gatewayURLs *gatewayURLs
61
62 dispChan chan msgdata.CommandResponse
63
64 idleTime time.Time
65 darkmode bool
66 }
67
68 func New(ctx context.Context, config Config) (*EmulatorService, error) {
69 jar, err := newCookieJar()
70 if err != nil {
71 return nil, err
72 }
73
74 if config.Profile.SessionCookie != "" {
75
76
77 cookies := parseCookie(config.Profile.SessionCookie)
78 url, err := url.Parse(config.Profile.API)
79 if err != nil {
80 return nil, err
81 }
82 jar.SetCookies(url, cookies)
83 }
84
85 client := http.Client{
86 Jar: jar,
87 Transport: &client.Transport{
88 T: http.DefaultTransport,
89 Headers: map[string]string{
90 eaconst.APIVersionKey: eaconst.APIVersion,
91 },
92 },
93 }
94
95 es := EmulatorService{config: &config, client: &client}
96 if err := es.setGatewayURLs(ctx); err != nil {
97 return nil, err
98 }
99 return &es, nil
100 }
101
102 func (es *EmulatorService) Env() []string {
103 return []string{
104 "RCLI_API_ENDPOINT" + "=" + es.config.Profile.API,
105 "RCLI_COOKIE" + "=" + es.config.Profile.SessionCookie,
106 }
107 }
108
109
110 func parseCookie(cookieString string) []*http.Cookie {
111 resp := http.Response{
112 Header: http.Header{
113 "Set-Cookie": []string{cookieString},
114 },
115 }
116 return resp.Cookies()
117 }
118
119
120
121 func (es *EmulatorService) Connect(ctx context.Context, projectID string, bannerID string, storeID string, terminalID string) error {
122 seshID := uuid.New().UUID
123 ctx = context.WithValue(ctx, sessionID, seshID)
124 log := fog.FromContext(ctx, "sessionID", seshID)
125 ctx = fog.IntoContext(ctx, log)
126 ctx, cancel := context.WithCancel(ctx)
127 es.session = &sessionDetails{
128 cancelFunc: cancel,
129 context: ctx,
130 ID: seshID,
131 }
132
133 target := types.Target{
134 Projectid: projectID,
135 Bannerid: bannerID,
136 Storeid: storeID,
137 Terminalid: terminalID,
138 }
139
140 req, err := es.createStartSessionRequest(ctx, seshID, target)
141 if err != nil {
142 cancel()
143 return err
144 }
145
146 es.dispChan = make(chan msgdata.CommandResponse)
147 var resp *http.Response
148
149 resp, err = es.client.Do(req)
150 if err != nil {
151 cancel()
152 return err
153 }
154 correlationID := resp.Header.Get(eamiddleware.CorrelationIDKey)
155 log = log.WithValues("correlationID", correlationID)
156
157 if resp.StatusCode != 200 {
158 cancel()
159 err := apierrorhandler.ParseJSONAPIError(resp.Body)
160
161 if _, ok := err.(apierror.APIError); !ok {
162 err = fmt.Errorf("error calling startSession API (%s), status (%s)", req.URL.String(), resp.Status)
163 }
164
165 resp.Body.Close()
166 return err
167 }
168 log.Info("connection to eagateway established")
169
170
171 target.Projectid = resp.Header.Get("X-EA-ProjectID")
172 target.Bannerid = resp.Header.Get("X-EA-BannerID")
173 target.Storeid = resp.Header.Get("X-EA-StoreID")
174 target.Terminalid = resp.Header.Get("X-EA-TerminalID")
175
176 es.session.target = target
177
178 go es.postToDisplayChan(resp, log)
179 es.idleTime = time.Now()
180 return nil
181 }
182
183
184
185 func (es *EmulatorService) postToDisplayChan(resp *http.Response, log logr.Logger) {
186 defer func() {
187
188 time.Sleep(100 * time.Millisecond)
189 resp.Body.Close()
190 close(es.dispChan)
191 }()
192
193 dec := json.NewDecoder(resp.Body)
194 for {
195 select {
196 default:
197 var received types.ConnectionPayload
198 err := dec.Decode(&received)
199 if err == io.EOF {
200 log.Info("Response body closed. Exiting")
201 return
202 }
203 if err != nil {
204 log.Error(err, "error decoding connection payload")
205 return
206 }
207 es.dispChan <- received.Message
208 case <-resp.Request.Context().Done():
209 log.Info("Context Deadline exceeded. Exiting")
210 return
211 }
212 }
213 }
214
215
216
217 func (es *EmulatorService) Send(command string) (string, error) {
218 log := fog.FromContext(es.session.context)
219 es.idleTime = time.Now()
220
221 payload := types.SendPayload{
222 Target: es.session.target,
223 AuthDetails: types.AuthDetails{
224 DarkMode: es.darkmode,
225 },
226 Command: command,
227 SessionID: es.session.ID,
228 }
229 message, err := json.Marshal(payload)
230 if err != nil {
231 return "", err
232 }
233 req, err := http.NewRequestWithContext(es.session.context, http.MethodPost, es.gatewayURLs.send.String(), bytes.NewReader(message))
234 if err != nil {
235 return "", err
236 }
237 es.setAuthHeader(req)
238
239 resp, err := es.client.Do(req)
240 if err != nil {
241 return "", err
242 }
243 correlationID := resp.Header.Get(eamiddleware.CorrelationIDKey)
244 log = log.WithValues("correlationID", correlationID)
245
246 if resp.StatusCode != 200 {
247 err = apierrorhandler.ParseJSONAPIError(resp.Body)
248
249 log.Error(err, "error from eagateway")
250 return correlationID, err
251 }
252 return correlationID, nil
253 }
254
255 func (es *EmulatorService) SetTopicTemplate(topicTemplate string) {
256 fmt.Println(topicTemplate)
257 }
258
259 func (es *EmulatorService) SetSubscriptionTemplate(subscriptionTemplate string) {
260 fmt.Println(subscriptionTemplate)
261 }
262
263 func (es *EmulatorService) RetrieveIdentity(ctx context.Context) error {
264 return es.retrieveBSLToken(ctx)
265 }
266
267
268 type banners struct {
269 BannerEdgeID string
270 }
271
272 func (es *EmulatorService) retrieveBSLToken(ctx context.Context) error {
273 log := fog.FromContext(ctx, "username", es.config.Profile.Username, "api", es.config.Profile.API, "organization", es.config.Profile.Organization)
274 ctx = fog.IntoContext(ctx, log)
275
276 gqlClient := graphql.NewClient(es.config.Profile.API, es.client)
277 var mutation struct {
278 Login struct {
279 Token graphql.String
280 Banners []banners
281 } `graphql:"login(username: $username, password: $password, organization: $organization)"`
282 }
283
284 variables := map[string]interface{}{
285 "username": graphql.String(es.config.Profile.Username),
286 "password": graphql.String(es.config.Profile.Password),
287 "organization": graphql.String(es.config.Profile.Organization),
288 }
289
290 log.Info("querying Login")
291 err := gqlClient.Mutate(ctx, &mutation, variables)
292 if err != nil {
293 return fmt.Errorf("error calling Edge API: %w", err)
294 }
295 log.Info("Login Successful")
296
297 es.userID = es.config.Profile.Username
298 es.idToken = &oauth2.Token{
299 AccessToken: string(mutation.Login.Token),
300 TokenType: "Bearer",
301 }
302
303 url, err := url.Parse(es.config.Profile.API)
304 if err != nil {
305 return err
306 }
307
308
309 cookies := es.client.Jar.Cookies(url)
310 for _, cookie := range cookies {
311
312 if cookie.Name == "edge-session" {
313 es.config.Profile.SessionCookie = cookie.String()
314 }
315 }
316
317 return nil
318 }
319
320 func (es *EmulatorService) GetDisplayChannel() <-chan msgdata.CommandResponse {
321 return es.dispChan
322 }
323
324 func (es *EmulatorService) GetSessionContext() context.Context {
325 return es.session.context
326 }
327
328 func (es *EmulatorService) UserID() string {
329 return es.userID
330 }
331
332 func (es *EmulatorService) IdleTime() time.Duration {
333 return time.Since(es.idleTime)
334 }
335
336
337 func (es *EmulatorService) End() error {
338
339 defer func() {
340 es.darkmode = false
341 es.session.cancelFunc()
342 }()
343 req, err := es.createEndSessionRequest(es.session.context, es.session.ID)
344 log := fog.FromContext(es.session.context)
345 if err != nil {
346 return err
347 }
348 resp, err := es.client.Do(req)
349 if err != nil {
350 return err
351 }
352 correlationID := resp.Header.Get(eamiddleware.CorrelationIDKey)
353 log = log.WithValues("correlationID", correlationID)
354 if resp.StatusCode != 200 {
355 err = apierrorhandler.ParseJSONAPIError(resp.Body)
356 log.Error(err, "error when ending session")
357 return err
358 }
359 log.Info("endSession request completed")
360 return nil
361 }
362
363 func (es EmulatorService) createEndSessionRequest(ctx context.Context, sessionID string) (*http.Request, error) {
364 payload := types.EndSessionPayload{
365 SessionID: sessionID,
366 }
367 msg, err := json.Marshal(payload)
368 if err != nil {
369 return nil, err
370 }
371 req, err := http.NewRequestWithContext(ctx, http.MethodPost, es.gatewayURLs.end.String(), bytes.NewReader(msg))
372 if err != nil {
373 return req, err
374 }
375 es.setAuthHeader(req)
376 return req, nil
377 }
378
379 func (es EmulatorService) createStartSessionRequest(ctx context.Context, sessionID string, target types.Target) (req *http.Request, err error) {
380 payload := types.StartSessionPayload{
381 SessionID: sessionID,
382 Target: target,
383 }
384 message, err := json.Marshal(payload)
385 if err != nil {
386 return nil, err
387 }
388 req, err = http.NewRequestWithContext(ctx, http.MethodPost, es.gatewayURLs.start.String(), bytes.NewReader(message))
389 if err != nil {
390 return nil, err
391 }
392 es.setAuthHeader(req)
393 req.Header.Set("Cache-Control", "no-cache")
394 req.Header.Set("Accept", "text/event-stream")
395 req.Header.Set("Connection", "keep-alive")
396 return req, nil
397 }
398
399 func (es *EmulatorService) setAuthHeader(req *http.Request) {
400
401 if es.idToken != nil && es.idToken.AccessToken != "" {
402 es.idToken.SetAuthHeader(req)
403 }
404 }
405
406 func parseEnvVar(log logr.Logger, envName, defaultVal string) (val string) {
407 val = os.Getenv(envName)
408 if val == "" {
409 val = defaultVal
410 log.Info(envName+" not set, using default value", "default", val)
411 } else {
412 log.Info("using envar value", envName, val)
413 }
414 return val
415 }
416
417 func (es *EmulatorService) setGatewayURLs(ctx context.Context) error {
418 log := fog.FromContext(ctx)
419
420
421 hostname := es.config.Profile.API
422 host, err := url.Parse(hostname)
423 if err != nil {
424 return fmt.Errorf("error parsing API hostname url in EmulatorService:setGatewayURLs: %v", err)
425 }
426
427
428 host, err = host.Parse("/api/ea/")
429 if err != nil {
430 return fmt.Errorf("error parsing common path segment: %w", err)
431 }
432
433
434 hostname = parseEnvVar(log, envGatewayHost, "")
435 host, err = host.Parse(hostname)
436 if err != nil {
437 return fmt.Errorf("error parsing RCLI_GATEWAY_HOST URL: %w", err)
438 }
439
440 start, err := host.Parse("startSession")
441 if err != nil {
442 return fmt.Errorf("error parsing startSession url in EmulatorService:setGatewayURLs: %v", err)
443 }
444 send, err := host.Parse("sendCommand")
445 if err != nil {
446 return fmt.Errorf("error parsing sendCommand url in EmulatorService:setGatewayURLs: %v", err)
447 }
448 end, err := host.Parse("endSession")
449 if err != nil {
450 return fmt.Errorf("error parsing endSession url in EmulatorService:setGatewayURLs: %v", err)
451 }
452 es.gatewayURLs = &gatewayURLs{start: start, send: send, end: end}
453
454 return nil
455 }
456 func (es *EmulatorService) SetDarkmode(val bool) {
457 es.darkmode = val
458 }
459
460 func (es *EmulatorService) Darkmode() bool {
461 return es.darkmode
462 }
463
464 func (es *EmulatorService) EnablePerSessionSubscription() {}
465 func (es *EmulatorService) DisablePerSessionSubscription() {}
466
View as plain text