package handlers import ( "bytes" "context" "crypto/tls" "embed" "fmt" "html/template" "io/fs" "io/ioutil" "log" "net/http" "path" "strings" "time" "golang.org/x/crypto/acme/autocert" "golang.org/x/oauth2" gh "github.com/gorilla/handlers" "github.com/gorilla/mux" lru "github.com/hashicorp/golang-lru" "github.com/play-with-docker/play-with-docker/config" "github.com/play-with-docker/play-with-docker/event" "github.com/play-with-docker/play-with-docker/pwd" "github.com/play-with-docker/play-with-docker/pwd/types" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/urfave/negroni" oauth2Github "golang.org/x/oauth2/github" oauth2Google "golang.org/x/oauth2/google" "google.golang.org/api/people/v1" ) var core pwd.PWDApi var e event.EventApi var landings = map[string][]byte{} //go:embed www/* var embeddedFiles embed.FS var staticFiles fs.FS var latencyHistogramVec = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "pwd_handlers_duration_ms", Help: "How long it took to process a specific handler, in a specific host", Buckets: []float64{300, 1200, 5000}, }, []string{"action"}) type HandlerExtender func(h *mux.Router) func init() { prometheus.MustRegister(latencyHistogramVec) staticFiles, _ = fs.Sub(embeddedFiles, "www") } func Bootstrap(c pwd.PWDApi, ev event.EventApi) { core = c e = ev } func Register(extend HandlerExtender) { initPlaygrounds() r := mux.NewRouter() corsRouter := mux.NewRouter() corsHandler := gh.CORS(gh.AllowCredentials(), gh.AllowedHeaders([]string{"x-requested-with", "content-type"}), gh.AllowedMethods([]string{"GET", "POST", "HEAD", "DELETE"}), gh.AllowedOriginValidator(func(origin string) bool { if strings.Contains(origin, "localhost") || strings.HasSuffix(origin, "play-with-docker.com") || strings.HasSuffix(origin, "play-with-kubernetes.com") || strings.HasSuffix(origin, "docker.com") || strings.HasSuffix(origin, "play-with-go.dev") { return true } return false }), gh.AllowedOrigins([]string{})) // Specific routes r.HandleFunc("/ping", Ping).Methods("GET") corsRouter.HandleFunc("/instances/images", GetInstanceImages).Methods("GET") corsRouter.HandleFunc("/sessions/{sessionId}", GetSession).Methods("GET") corsRouter.HandleFunc("/sessions/{sessionId}/close", CloseSession).Methods("POST") corsRouter.HandleFunc("/sessions/{sessionId}", CloseSession).Methods("DELETE") corsRouter.HandleFunc("/sessions/{sessionId}/setup", SessionSetup).Methods("POST") corsRouter.HandleFunc("/sessions/{sessionId}/instances", NewInstance).Methods("POST") corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/uploads", FileUpload).Methods("POST") corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", DeleteInstance).Methods("DELETE") corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/exec", Exec).Methods("POST") corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/fstree", fsTree).Methods("GET") corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/file", file).Methods("GET") r.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/editor", func(rw http.ResponseWriter, r *http.Request) { serveAsset(rw, r, "editor.html") }) r.HandleFunc("/ooc", func(rw http.ResponseWriter, r *http.Request) { serveAsset(rw, r, "occ.html") }).Methods("GET") r.HandleFunc("/503", func(rw http.ResponseWriter, r *http.Request) { serveAsset(rw, r, "503.html") }).Methods("GET") r.HandleFunc("/p/{sessionId}", Home).Methods("GET") r.PathPrefix("/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { serveAsset(rw, r, r.URL.Path[1:]) }) r.HandleFunc("/robots.txt", func(rw http.ResponseWriter, r *http.Request) { serveAsset(rw, r, "robots.txt") }) corsRouter.HandleFunc("/sessions/{sessionId}/ws/", WSH) r.Handle("/metrics", promhttp.Handler()) // Generic routes r.HandleFunc("/", Landing).Methods("GET") corsRouter.HandleFunc("/users/me", LoggedInUser).Methods("GET") r.HandleFunc("/users/{userId:^(?me)}", GetUser).Methods("GET") r.HandleFunc("/oauth/providers", ListProviders).Methods("GET") r.HandleFunc("/oauth/providers/{provider}/login", Login).Methods("GET") r.HandleFunc("/oauth/providers/{provider}/callback", LoginCallback).Methods("GET") r.HandleFunc("/playgrounds", NewPlayground).Methods("PUT") r.HandleFunc("/playgrounds", ListPlaygrounds).Methods("GET") r.HandleFunc("/my/playground", GetCurrentPlayground).Methods("GET") corsRouter.HandleFunc("/", NewSession).Methods("POST") if extend != nil { extend(corsRouter) } n := negroni.Classic() r.PathPrefix("/").Handler(negroni.New(negroni.Wrap(corsHandler(corsRouter)))) n.UseHandler(r) httpServer := http.Server{ Addr: "0.0.0.0:" + config.PortNumber, Handler: n, IdleTimeout: 30 * time.Second, ReadHeaderTimeout: 5 * time.Second, } if config.UseLetsEncrypt { domainCache, err := lru.New(5000) if err != nil { log.Fatalf("Could not start domain cache. Got: %v", err) } certManager := autocert.Manager{ Prompt: autocert.AcceptTOS, HostPolicy: func(ctx context.Context, host string) error { if _, found := domainCache.Get(host); !found { if playground := core.PlaygroundFindByDomain(host); playground == nil { return fmt.Errorf("Playground for domain %s was not found", host) } domainCache.Add(host, true) } return nil }, Cache: autocert.DirCache(config.LetsEncryptCertsDir), } httpServer.TLSConfig = &tls.Config{ GetCertificate: certManager.GetCertificate, } go func() { rr := mux.NewRouter() rr.HandleFunc("/ping", Ping).Methods("GET") rr.Handle("/metrics", promhttp.Handler()) rr.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { target := fmt.Sprintf("https://%s%s", r.Host, r.URL.Path) if len(r.URL.RawQuery) > 0 { target += "?" + r.URL.RawQuery } http.Redirect(rw, r, target, http.StatusMovedPermanently) }) nr := negroni.Classic() nr.UseHandler(rr) log.Println("Starting redirect server") redirectServer := http.Server{ Addr: "0.0.0.0:3001", Handler: certManager.HTTPHandler(nr), IdleTimeout: 30 * time.Second, ReadHeaderTimeout: 5 * time.Second, } log.Fatal(redirectServer.ListenAndServe()) }() log.Println("Listening on port " + config.PortNumber) log.Fatal(httpServer.ListenAndServeTLS("", "")) } else { log.Println("Listening on port " + config.PortNumber) log.Fatal(httpServer.ListenAndServe()) } } func serveAsset(w http.ResponseWriter, r *http.Request, name string) { a, err := fs.ReadFile(staticFiles, name) if err != nil { w.WriteHeader(http.StatusNotFound) return } http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(a)) } func initPlaygrounds() { pgs, err := core.PlaygroundList() if err != nil { log.Fatal("Error getting playgrounds for initialization") } for _, p := range pgs { initAssets(p) initOauthProviders(p) } } func initAssets(p *types.Playground) { if p.AssetsDir == "" { p.AssetsDir = "default" } lpath := path.Join(p.AssetsDir, "landing.html") landing, err := fs.ReadFile(staticFiles, lpath) if err != nil { log.Printf("Could not load %v: %v", lpath, err) return } var b bytes.Buffer t := template.New("landing.html").Delims("[[", "]]") t, err = t.Parse(string(landing)) if err != nil { log.Fatalf("Error parsing template %v", err) } if err := t.Execute(&b, struct{ SegmentId string }{config.SegmentId}); err != nil { log.Fatalf("Error executing template %v", err) } landingBytes, err := ioutil.ReadAll(&b) if err != nil { log.Fatalf("Error reading template bytes %v", err) } landings[p.Id] = landingBytes } func initOauthProviders(p *types.Playground) { config.Providers[p.Id] = map[string]*oauth2.Config{} if p.GithubClientID != "" && p.GithubClientSecret != "" { conf := &oauth2.Config{ ClientID: p.GithubClientID, ClientSecret: p.GithubClientSecret, Scopes: []string{"user:email"}, Endpoint: oauth2Github.Endpoint, } config.Providers[p.Id]["github"] = conf } if p.GoogleClientID != "" && p.GoogleClientSecret != "" { conf := &oauth2.Config{ ClientID: p.GoogleClientID, ClientSecret: p.GoogleClientSecret, Scopes: []string{people.UserinfoEmailScope, people.UserinfoProfileScope}, Endpoint: oauth2Google.Endpoint, } config.Providers[p.Id]["google"] = conf } if p.DockerClientID != "" && p.DockerClientSecret != "" { endpoint := getDockerEndpoint(p) oauth2.RegisterBrokenAuthHeaderProvider(fmt.Sprintf(".%s", endpoint)) conf := &oauth2.Config{ ClientID: p.DockerClientID, ClientSecret: p.DockerClientSecret, Scopes: []string{"openid"}, Endpoint: oauth2.Endpoint{ AuthURL: fmt.Sprintf("https://%s/id/oauth/authorize/", endpoint), TokenURL: fmt.Sprintf("https://%s/id/oauth/token", endpoint), }, } config.Providers[p.Id]["docker"] = conf } }