github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/state/apiserver/apiserver.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver 5 6 import ( 7 "crypto/tls" 8 "fmt" 9 "net" 10 "net/http" 11 "sync" 12 "sync/atomic" 13 "time" 14 15 "code.google.com/p/go.net/websocket" 16 "github.com/bmizerany/pat" 17 "github.com/juju/loggo" 18 "github.com/juju/utils" 19 "launchpad.net/tomb" 20 21 "github.com/juju/juju/rpc" 22 "github.com/juju/juju/rpc/jsoncodec" 23 "github.com/juju/juju/state" 24 "github.com/juju/juju/state/api/params" 25 "github.com/juju/juju/state/apiserver/common" 26 ) 27 28 var logger = loggo.GetLogger("juju.state.apiserver") 29 30 // loginRateLimit defines how many concurrent Login requests we will 31 // accept 32 const loginRateLimit = 10 33 34 // Server holds the server side of the API. 35 type Server struct { 36 tomb tomb.Tomb 37 wg sync.WaitGroup 38 state *state.State 39 environUUID string 40 addr net.Addr 41 dataDir string 42 logDir string 43 limiter utils.Limiter 44 validator LoginValidator 45 } 46 47 // LoginValidator functions are used to decide whether login requests 48 // are to be allowed. The validator is called before credentials are 49 // checked. 50 type LoginValidator func(params.Creds) error 51 52 // ServerConfig holds parameters required to set up an API server. 53 type ServerConfig struct { 54 Addr string 55 Cert []byte 56 Key []byte 57 DataDir string 58 LogDir string 59 Validator LoginValidator 60 } 61 62 // NewServer serves the given state by accepting requests on the given 63 // listener, using the given certificate and key (in PEM format) for 64 // authentication. 65 func NewServer(s *state.State, cfg ServerConfig) (*Server, error) { 66 lis, err := net.Listen("tcp", cfg.Addr) 67 if err != nil { 68 return nil, err 69 } 70 logger.Infof("listening on %q", lis.Addr()) 71 tlsCert, err := tls.X509KeyPair(cfg.Cert, cfg.Key) 72 if err != nil { 73 return nil, err 74 } 75 srv := &Server{ 76 state: s, 77 addr: lis.Addr(), 78 dataDir: cfg.DataDir, 79 logDir: cfg.LogDir, 80 limiter: utils.NewLimiter(loginRateLimit), 81 validator: cfg.Validator, 82 } 83 // TODO(rog) check that *srvRoot is a valid type for using 84 // as an RPC server. 85 lis = tls.NewListener(lis, &tls.Config{ 86 Certificates: []tls.Certificate{tlsCert}, 87 }) 88 go srv.run(lis) 89 return srv, nil 90 } 91 92 // Dead returns a channel that signals when the server has exited. 93 func (srv *Server) Dead() <-chan struct{} { 94 return srv.tomb.Dead() 95 } 96 97 // Stop stops the server and returns when all running requests 98 // have completed. 99 func (srv *Server) Stop() error { 100 srv.tomb.Kill(nil) 101 return srv.tomb.Wait() 102 } 103 104 // Kill implements worker.Worker.Kill. 105 func (srv *Server) Kill() { 106 srv.tomb.Kill(nil) 107 } 108 109 // Wait implements worker.Worker.Wait. 110 func (srv *Server) Wait() error { 111 return srv.tomb.Wait() 112 } 113 114 type requestNotifier struct { 115 id int64 116 start time.Time 117 118 mu sync.Mutex 119 tag_ string 120 } 121 122 var globalCounter int64 123 124 func newRequestNotifier() *requestNotifier { 125 return &requestNotifier{ 126 id: atomic.AddInt64(&globalCounter, 1), 127 tag_: "<unknown>", 128 start: time.Now(), 129 } 130 } 131 132 func (n *requestNotifier) login(tag string) { 133 n.mu.Lock() 134 n.tag_ = tag 135 n.mu.Unlock() 136 } 137 138 func (n *requestNotifier) tag() (tag string) { 139 n.mu.Lock() 140 tag = n.tag_ 141 n.mu.Unlock() 142 return 143 } 144 145 func (n *requestNotifier) ServerRequest(hdr *rpc.Header, body interface{}) { 146 if hdr.Request.Type == "Pinger" && hdr.Request.Action == "Ping" { 147 return 148 } 149 // TODO(rog) 2013-10-11 remove secrets from some requests. 150 logger.Debugf("<- [%X] %s %s", n.id, n.tag(), jsoncodec.DumpRequest(hdr, body)) 151 } 152 153 func (n *requestNotifier) ServerReply(req rpc.Request, hdr *rpc.Header, body interface{}, timeSpent time.Duration) { 154 if req.Type == "Pinger" && req.Action == "Ping" { 155 return 156 } 157 logger.Debugf("-> [%X] %s %s %s %s[%q].%s", n.id, n.tag(), timeSpent, jsoncodec.DumpRequest(hdr, body), req.Type, req.Id, req.Action) 158 } 159 160 func (n *requestNotifier) join(req *http.Request) { 161 logger.Infof("[%X] API connection from %s", n.id, req.RemoteAddr) 162 } 163 164 func (n *requestNotifier) leave() { 165 logger.Infof("[%X] %s API connection terminated after %v", n.id, n.tag(), time.Since(n.start)) 166 } 167 168 func (n requestNotifier) ClientRequest(hdr *rpc.Header, body interface{}) { 169 } 170 171 func (n requestNotifier) ClientReply(req rpc.Request, hdr *rpc.Header, body interface{}) { 172 } 173 174 func handleAll(mux *pat.PatternServeMux, pattern string, handler http.Handler) { 175 mux.Get(pattern, handler) 176 mux.Post(pattern, handler) 177 mux.Head(pattern, handler) 178 mux.Put(pattern, handler) 179 mux.Del(pattern, handler) 180 mux.Options(pattern, handler) 181 } 182 183 func (srv *Server) run(lis net.Listener) { 184 defer srv.tomb.Done() 185 defer srv.wg.Wait() // wait for any outstanding requests to complete. 186 srv.wg.Add(1) 187 go func() { 188 <-srv.tomb.Dying() 189 lis.Close() 190 srv.wg.Done() 191 }() 192 srv.wg.Add(1) 193 go func() { 194 err := srv.mongoPinger() 195 srv.tomb.Kill(err) 196 srv.wg.Done() 197 }() 198 // for pat based handlers, they are matched in-order of being 199 // registered, first match wins. So more specific ones have to be 200 // registered first. 201 mux := pat.New() 202 // For backwards compatibility we register all the old paths 203 handleAll(mux, "/environment/:envuuid/log", 204 &debugLogHandler{ 205 httpHandler: httpHandler{state: srv.state}, 206 logDir: srv.logDir}, 207 ) 208 handleAll(mux, "/environment/:envuuid/charms", 209 &charmsHandler{ 210 httpHandler: httpHandler{state: srv.state}, 211 dataDir: srv.dataDir}, 212 ) 213 // TODO: We can switch from handleAll to mux.Post/Get/etc for entries 214 // where we only want to support specific request methods. However, our 215 // tests currently assert that errors come back as application/json and 216 // pat only does "text/plain" responses. 217 handleAll(mux, "/environment/:envuuid/tools", 218 &toolsHandler{httpHandler{state: srv.state}}, 219 ) 220 handleAll(mux, "/environment/:envuuid/api", http.HandlerFunc(srv.apiHandler)) 221 // For backwards compatibility we register all the old paths 222 handleAll(mux, "/log", 223 &debugLogHandler{ 224 httpHandler: httpHandler{state: srv.state}, 225 logDir: srv.logDir}, 226 ) 227 handleAll(mux, "/charms", 228 &charmsHandler{ 229 httpHandler: httpHandler{state: srv.state}, 230 dataDir: srv.dataDir}, 231 ) 232 handleAll(mux, "/tools", 233 &toolsHandler{httpHandler{state: srv.state}}, 234 ) 235 handleAll(mux, "/", http.HandlerFunc(srv.apiHandler)) 236 // The error from http.Serve is not interesting. 237 http.Serve(lis, mux) 238 } 239 240 func (srv *Server) apiHandler(w http.ResponseWriter, req *http.Request) { 241 reqNotifier := newRequestNotifier() 242 reqNotifier.join(req) 243 defer reqNotifier.leave() 244 wsServer := websocket.Server{ 245 Handler: func(conn *websocket.Conn) { 246 srv.wg.Add(1) 247 defer srv.wg.Done() 248 // If we've got to this stage and the tomb is still 249 // alive, we know that any tomb.Kill must occur after we 250 // have called wg.Add, so we avoid the possibility of a 251 // handler goroutine running after Stop has returned. 252 if srv.tomb.Err() != tomb.ErrStillAlive { 253 return 254 } 255 envUUID := req.URL.Query().Get(":envuuid") 256 logger.Tracef("got a request for env %q", envUUID) 257 if err := srv.serveConn(conn, reqNotifier, envUUID); err != nil { 258 logger.Errorf("error serving RPCs: %v", err) 259 } 260 }, 261 } 262 wsServer.ServeHTTP(w, req) 263 } 264 265 // Addr returns the address that the server is listening on. 266 func (srv *Server) Addr() string { 267 return srv.addr.String() 268 } 269 270 func (srv *Server) validateEnvironUUID(envUUID string) error { 271 if envUUID == "" { 272 // We allow the environUUID to be empty for 2 cases 273 // 1) Compatibility with older clients 274 // 2) On first connect. The environment UUID is currently 275 // generated by 'jujud bootstrap-state', and we haven't 276 // threaded that information all the way back to the 'juju 277 // bootstrap' process to be able to cache the value until 278 // after we've connected one time. 279 return nil 280 } 281 if srv.environUUID == "" { 282 env, err := srv.state.Environment() 283 if err != nil { 284 return err 285 } 286 srv.environUUID = env.UUID() 287 } 288 if envUUID != srv.environUUID { 289 return common.UnknownEnvironmentError(envUUID) 290 } 291 return nil 292 } 293 294 func (srv *Server) serveConn(wsConn *websocket.Conn, reqNotifier *requestNotifier, envUUID string) error { 295 codec := jsoncodec.NewWebsocket(wsConn) 296 if loggo.GetLogger("juju.rpc.jsoncodec").EffectiveLogLevel() <= loggo.TRACE { 297 codec.SetLogging(true) 298 } 299 var notifier rpc.RequestNotifier 300 if logger.EffectiveLogLevel() <= loggo.DEBUG { 301 // Incur request monitoring overhead only if we 302 // know we'll need it. 303 notifier = reqNotifier 304 } 305 conn := rpc.NewConn(codec, notifier) 306 err := srv.validateEnvironUUID(envUUID) 307 if err != nil { 308 conn.Serve(&errRoot{err}, serverError) 309 } else { 310 conn.Serve(newStateServer(srv, conn, reqNotifier, srv.limiter), serverError) 311 } 312 conn.Start() 313 select { 314 case <-conn.Dead(): 315 case <-srv.tomb.Dying(): 316 } 317 return conn.Close() 318 } 319 320 func (srv *Server) mongoPinger() error { 321 timer := time.NewTimer(0) 322 session := srv.state.MongoSession() 323 for { 324 select { 325 case <-timer.C: 326 case <-srv.tomb.Dying(): 327 return tomb.ErrDying 328 } 329 if err := session.Ping(); err != nil { 330 logger.Infof("got error pinging mongo: %v", err) 331 return fmt.Errorf("error pinging mongo: %v", err) 332 } 333 timer.Reset(mongoPingInterval) 334 } 335 } 336 337 func serverError(err error) error { 338 if err := common.ServerError(err); err != nil { 339 return err 340 } 341 return nil 342 } 343 344 var logRequests = true