1package manager
2
3import (
4	"crypto/hmac"
5	"fmt"
6	"net/http"
7	"runtime"
8	"sync"
9
10	"github.com/keybase/client/go/kbhttp"
11	"github.com/keybase/client/go/libkb"
12	"github.com/keybase/client/go/protocol/keybase1"
13	context "golang.org/x/net/context"
14)
15
16type SrvTokenMode int
17
18const (
19	SrvTokenModeDefault   = iota
20	SrvTokenModeUnchecked // use with caution!
21)
22
23type srvEndpoint struct {
24	tokenMode SrvTokenMode
25	serve     func(w http.ResponseWriter, req *http.Request)
26}
27
28type Srv struct {
29	libkb.Contextified
30
31	httpSrv   *kbhttp.Srv
32	endpoints map[string]srvEndpoint
33	token     string
34	startMu   sync.Mutex
35}
36
37func NewSrv(g *libkb.GlobalContext) *Srv {
38	h := &Srv{
39		Contextified: libkb.NewContextified(g),
40		endpoints:    make(map[string]srvEndpoint),
41	}
42	h.initHTTPSrv()
43	h.startHTTPSrv()
44	g.PushShutdownHook(func(mctx libkb.MetaContext) error {
45		h.httpSrv.Stop()
46		return nil
47	})
48	go h.monitorAppState()
49	return h
50}
51
52func (r *Srv) debug(ctx context.Context, msg string, args ...interface{}) {
53	r.G().Log.CDebugf(ctx, "Srv: %s", fmt.Sprintf(msg, args...))
54}
55
56func (r *Srv) initHTTPSrv() {
57	startPort := r.G().GetEnv().GetAttachmentHTTPStartPort()
58	r.httpSrv = kbhttp.NewSrv(r.G().GetLog(), kbhttp.NewRandomPortRangeListenerSource(startPort, 18000))
59}
60
61func (r *Srv) startHTTPSrv() {
62	r.startMu.Lock()
63	defer r.startMu.Unlock()
64	ctx := context.Background()
65	token, _ := libkb.RandHexString("", 32)
66	maxTries := 2
67	success := false
68	for i := 0; i < maxTries; i++ {
69		if err := r.httpSrv.Start(); err != nil {
70			if err == kbhttp.ErrPinnedPortInUse {
71				// If we hit this, just try again and get a different port.
72				// The advantage is that backing in and out of the thread will restore attachments,
73				// whereas if we do nothing you need to bkg/foreground.
74				r.debug(ctx, "startHTTPSrv: pinned port taken error, re-initializing and trying again")
75				r.initHTTPSrv()
76				continue
77			}
78			r.debug(ctx, "startHTTPSrv: failed to start HTTP server: %s", err)
79			break
80		}
81		success = true
82		break
83	}
84	if !success {
85		r.debug(ctx, "startHTTPSrv: exhausted attempts to start HTTP server, giving up")
86		return
87	}
88	for endpoint, serveDesc := range r.endpoints {
89		r.HandleFunc(endpoint, serveDesc.tokenMode, serveDesc.serve)
90	}
91	addr, err := r.httpSrv.Addr()
92	if err != nil {
93		r.debug(ctx, "startHTTPSrv: failed to get address after start?: %s", err)
94	} else {
95		r.debug(ctx, "startHTTPSrv: start success: addr: %s", addr)
96	}
97	r.token = token
98	r.debug(ctx, "startHTTPSrv: addr: %s token: %s", addr, r.token)
99	r.G().NotifyRouter.HandleHTTPSrvInfoUpdate(ctx, keybase1.HttpSrvInfo{
100		Address: addr,
101		Token:   r.token,
102	})
103}
104
105func (r *Srv) monitorAppState() {
106	ctx := context.Background()
107	r.debug(ctx, "monitorAppState: starting up")
108	state := keybase1.MobileAppState_FOREGROUND
109	// We don't need this on Android
110	if runtime.GOOS == "android" {
111		return
112	}
113	for {
114		state = <-r.G().MobileAppState.NextUpdate(&state)
115		switch state {
116		case keybase1.MobileAppState_FOREGROUND, keybase1.MobileAppState_BACKGROUNDACTIVE:
117			r.startHTTPSrv()
118		case keybase1.MobileAppState_BACKGROUND, keybase1.MobileAppState_INACTIVE:
119			r.httpSrv.Stop()
120		}
121	}
122}
123
124func (r *Srv) HandleFunc(endpoint string, tokenMode SrvTokenMode,
125	serve func(w http.ResponseWriter, req *http.Request)) {
126	r.httpSrv.HandleFunc("/"+endpoint, func(w http.ResponseWriter, req *http.Request) {
127		switch tokenMode {
128		case SrvTokenModeDefault:
129			if !hmac.Equal([]byte(req.URL.Query().Get("token")), []byte(r.token)) {
130				r.debug(context.Background(), "HandleFunc: token failed: %s != %s",
131					req.URL.Query().Get("token"), r.token)
132				w.WriteHeader(http.StatusForbidden)
133				return
134			}
135		case SrvTokenModeUnchecked:
136			// serve needs to authenticate on its own
137		}
138		serve(w, req)
139	})
140	r.endpoints[endpoint] = srvEndpoint{
141		tokenMode: tokenMode,
142		serve:     serve,
143	}
144}
145
146func (r *Srv) Active() bool {
147	return r.httpSrv.Active()
148}
149
150func (r *Srv) Addr() (string, error) {
151	r.startMu.Lock()
152	defer r.startMu.Unlock()
153	return r.httpSrv.Addr()
154}
155
156func (r *Srv) Token() string {
157	r.startMu.Lock()
158	defer r.startMu.Unlock()
159	return r.token
160}
161