Add support for openid with github and facebook

This commit is contained in:
Jonathan Leibiusky @xetorthio
2017-10-04 11:40:56 -03:00
parent eebe638227
commit 4c034812d2
25 changed files with 712 additions and 251 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
play-with-docker
node_modules
docker-compose.single.yml
lala

View File

@@ -6,6 +6,12 @@ import (
"os"
"regexp"
"time"
"github.com/gorilla/securecookie"
"golang.org/x/oauth2"
oauth2FB "golang.org/x/oauth2/facebook"
oauth2Github "golang.org/x/oauth2/github"
)
const (
@@ -21,12 +27,17 @@ const (
var NameFilter = regexp.MustCompile(PWDHostPortGroupRegex)
var AliasFilter = regexp.MustCompile(AliasPortGroupRegex)
var PortNumber, Key, Cert, SessionsFile, PWDContainerName, L2ContainerName, L2Subdomain, PWDCName, HashKey, SSHKeyPath, L2RouterIP, DindVolumeSize string
var PortNumber, Key, Cert, SessionsFile, PWDContainerName, L2ContainerName, L2Subdomain, PWDCName, HashKey, SSHKeyPath, L2RouterIP, DindVolumeSize, CookieHashKey, CookieBlockKey string
var UseLetsEncrypt, ExternalDindVolume, NoWindows bool
var LetsEncryptCertsDir string
var LetsEncryptDomains stringslice
var MaxLoadAvg float64
var ForceTLS bool
var Providers map[string]*oauth2.Config
var SecureCookie *securecookie.SecureCookie
var GithubClientID, GithubClientSecret string
var FacebookClientID, FacebookClientSecret string
type stringslice []string
@@ -58,8 +69,46 @@ func ParseFlags() {
flag.BoolVar(&ExternalDindVolume, "external-dind-volume", false, "Use external dind volume")
flag.Float64Var(&MaxLoadAvg, "maxload", 100, "Maximum allowed load average before failing ping requests")
flag.StringVar(&SSHKeyPath, "ssh_key_path", "", "SSH Private Key to use")
flag.StringVar(&CookieHashKey, "cookie-hash-key", "", "Hash key to use to validate cookies")
flag.StringVar(&CookieBlockKey, "cookie-block-key", "", "Block key to use to encrypt cookies")
flag.StringVar(&GithubClientID, "github-client-id", "", "Github OAuth Client ID")
flag.StringVar(&GithubClientSecret, "github-client-secret", "", "Github OAuth Client Secret")
flag.StringVar(&FacebookClientID, "facebook-client-id", "", "Facebook OAuth Client ID")
flag.StringVar(&FacebookClientSecret, "facebook-client-secret", "", "Facebook OAuth Client Secret")
flag.Parse()
SecureCookie = securecookie.New([]byte(CookieHashKey), []byte(CookieBlockKey))
registerOAuthProviders()
}
func registerOAuthProviders() {
Providers = map[string]*oauth2.Config{}
if GithubClientID != "" && GithubClientSecret != "" {
conf := &oauth2.Config{
ClientID: GithubClientID,
ClientSecret: GithubClientSecret,
Scopes: []string{"user:email"},
Endpoint: oauth2Github.Endpoint,
}
Providers["github"] = conf
}
if FacebookClientID != "" && FacebookClientSecret != "" {
conf := &oauth2.Config{
ClientID: FacebookClientID,
ClientSecret: FacebookClientSecret,
Scopes: []string{"email", "public_profile"},
Endpoint: oauth2FB.Endpoint,
}
Providers["facebook"] = conf
}
}
func GetDindImageName() string {
dindImage := os.Getenv("DIND_IMAGE")
defaultDindImageName := "franela/dind"

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"log"
"net/http"
"os"
"time"
"golang.org/x/crypto/acme/autocert"
@@ -16,7 +15,6 @@ import (
"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/templates"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/urfave/negroni"
)
@@ -33,9 +31,6 @@ func Bootstrap(c pwd.PWDApi, ev event.EventApi) {
}
func Register(extend HandlerExtender) {
bypassCaptcha := len(os.Getenv("GOOGLE_RECAPTCHA_DISABLED")) > 0
server, err := socketio.NewServer(nil)
if err != nil {
log.Fatal(err)
@@ -81,17 +76,13 @@ func Register(extend HandlerExtender) {
// Generic routes
r.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
if bypassCaptcha {
http.ServeFile(rw, r, "./www/bypass.html")
} else {
welcome, tmplErr := templates.GetWelcomeTemplate()
if tmplErr != nil {
log.Fatal(tmplErr)
}
rw.Write(welcome)
}
http.ServeFile(rw, r, "./www/landing.html")
}).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")
corsRouter.HandleFunc("/", NewSession).Methods("POST")
if extend != nil {

40
handlers/cookie_id.go Normal file
View File

@@ -0,0 +1,40 @@
package handlers
import (
"net/http"
"github.com/play-with-docker/play-with-docker/config"
)
type CookieID struct {
Id string `json:"id"`
UserName string `json:"user_name"`
UserAvatar string `json:"user_avatar"`
}
func (c *CookieID) SetCookie(rw http.ResponseWriter) error {
if encoded, err := config.SecureCookie.Encode("id", c); err == nil {
cookie := &http.Cookie{
Name: "id",
Value: encoded,
Path: "/",
Secure: config.UseLetsEncrypt,
}
http.SetCookie(rw, cookie)
} else {
return err
}
return nil
}
func ReadCookie(r *http.Request) (*CookieID, error) {
if cookie, err := r.Cookie("id"); err == nil {
value := &CookieID{}
if err = config.SecureCookie.Decode("id", cookie.Value, &value); err == nil {
return value, nil
} else {
return nil, err
}
} else {
return nil, err
}
}

146
handlers/login.go Normal file
View File

@@ -0,0 +1,146 @@
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"golang.org/x/oauth2"
"github.com/google/go-github/github"
"github.com/gorilla/mux"
fb "github.com/huandu/facebook"
"github.com/play-with-docker/play-with-docker/config"
"github.com/play-with-docker/play-with-docker/pwd/types"
)
func ListProviders(rw http.ResponseWriter, req *http.Request) {
providers := []string{}
for name, _ := range config.Providers {
providers = append(providers, name)
}
json.NewEncoder(rw).Encode(providers)
}
func Login(rw http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
providerName := vars["provider"]
provider, found := config.Providers[providerName]
if !found {
log.Printf("Could not find provider %s\n", providerName)
rw.WriteHeader(http.StatusNotFound)
return
}
loginRequest, err := core.UserNewLoginRequest(providerName)
if err != nil {
log.Printf("Could not start a new user login request for provider %s. Got: %v\n", providerName, err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
scheme := "http"
if req.URL.Scheme != "" {
scheme = req.URL.Scheme
}
host := "localhost"
if req.URL.Host != "" {
host = req.URL.Host
}
provider.RedirectURL = fmt.Sprintf("%s://%s/oauth/providers/%s/callback", scheme, host, providerName)
url := provider.AuthCodeURL(loginRequest.Id)
http.Redirect(rw, req, url, http.StatusFound)
}
func LoginCallback(rw http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
providerName := vars["provider"]
provider, found := config.Providers[providerName]
if !found {
log.Printf("Could not find provider %s\n", providerName)
rw.WriteHeader(http.StatusNotFound)
return
}
query := req.URL.Query()
code := query.Get("code")
loginRequestId := query.Get("state")
loginRequest, err := core.UserGetLoginRequest(loginRequestId)
if err != nil {
log.Printf("Could not get login request %s for provider %s. Got: %v\n", loginRequestId, providerName, err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
ctx := req.Context()
tok, err := provider.Exchange(ctx, code)
if err != nil {
log.Printf("Could not exchage code for access token for provider %s. Got: %v\n", providerName, err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
user := &types.User{Provider: providerName}
if providerName == "github" {
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: tok.AccessToken},
)
tc := oauth2.NewClient(ctx, ts)
client := github.NewClient(tc)
u, _, err := client.Users.Get(ctx, "")
if err != nil {
log.Printf("Could not get user from github. Got: %v\n", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
user.ProviderUserId = strconv.Itoa(u.GetID())
user.Name = u.GetName()
user.Avatar = u.GetAvatarURL()
user.Email = u.GetEmail()
} else if providerName == "facebook" {
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: tok.AccessToken},
)
tc := oauth2.NewClient(ctx, ts)
session := &fb.Session{
Version: "v2.10",
HttpClient: tc,
}
p := fb.Params{}
p["fields"] = "email,name,picture"
res, err := session.Get("/me", p)
if err != nil {
log.Printf("Could not get user from facebook. Got: %v\n", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
user.ProviderUserId = res.Get("id").(string)
user.Name = res.Get("name").(string)
user.Avatar = res.Get("picture.data.url").(string)
user.Email = res.Get("email").(string)
}
user, err = core.UserLogin(loginRequest, user)
if err != nil {
log.Printf("Could not login user. Got: %v\n", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
cookieData := CookieID{Id: user.Id, UserName: user.Name, UserAvatar: user.Avatar}
if err := cookieData.SetCookie(rw); err != nil {
log.Printf("Could not encode cookie. Got: %v\n", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
http.Redirect(rw, req, "/", http.StatusFound)
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/play-with-docker/play-with-docker/config"
"github.com/play-with-docker/play-with-docker/provisioner"
"github.com/play-with-docker/play-with-docker/recaptcha"
)
type NewSessionResponse struct {
@@ -20,11 +19,17 @@ type NewSessionResponse struct {
func NewSession(rw http.ResponseWriter, req *http.Request) {
req.ParseForm()
if !recaptcha.IsHuman(req, rw) {
userId := ""
if len(config.Providers) > 0 {
cookie, err := ReadCookie(req)
if err != nil {
// User it not a human
rw.WriteHeader(http.StatusForbidden)
return
}
userId = cookie.Id
}
reqDur := req.Form.Get("session-duration")
stack := req.Form.Get("stack")
@@ -45,7 +50,7 @@ func NewSession(rw http.ResponseWriter, req *http.Request) {
}
duration := config.GetDuration(reqDur)
s, err := core.SessionNew(duration, stack, stackName, imageName)
s, err := core.SessionNew(userId, duration, stack, stackName, imageName)
if err != nil {
if provisioner.OutOfCapacity(err) {
http.Redirect(rw, req, "/ooc", http.StatusFound)

View File

@@ -43,7 +43,7 @@ func TestClientNew(t *testing.T) {
p := NewPWD(_f, _e, _s, sp, ipf)
p.generator = _g
session, err := p.SessionNew(time.Hour, "", "", "")
session, err := p.SessionNew("", time.Hour, "", "", "")
assert.Nil(t, err)
client := p.ClientNew("foobar", session)
@@ -82,7 +82,7 @@ func TestClientCount(t *testing.T) {
p := NewPWD(_f, _e, _s, sp, ipf)
p.generator = _g
session, err := p.SessionNew(time.Hour, "", "", "")
session, err := p.SessionNew("", time.Hour, "", "", "")
assert.Nil(t, err)
p.ClientNew("foobar", session)
@@ -123,7 +123,7 @@ func TestClientResizeViewPort(t *testing.T) {
p := NewPWD(_f, _e, _s, sp, ipf)
p.generator = _g
session, err := p.SessionNew(time.Hour, "", "", "")
session, err := p.SessionNew("", time.Hour, "", "", "")
assert.Nil(t, err)
client := p.ClientNew("foobar", session)
_s.On("ClientFindBySessionId", "aaaabbbbcccc").Return([]*types.Client{client}, nil)

View File

@@ -70,7 +70,7 @@ func TestInstanceNew(t *testing.T) {
p := NewPWD(_f, _e, _s, sp, ipf)
p.generator = _g
session, err := p.SessionNew(time.Hour, "", "", "")
session, err := p.SessionNew("", time.Hour, "", "", "")
assert.Nil(t, err)
expectedInstance := types.Instance{
@@ -138,7 +138,7 @@ func TestInstanceNew_WithNotAllowedImage(t *testing.T) {
p := NewPWD(_f, _e, _s, sp, ipf)
p.generator = _g
session, err := p.SessionNew(time.Hour, "", "", "")
session, err := p.SessionNew("", time.Hour, "", "", "")
assert.Nil(t, err)
@@ -207,7 +207,7 @@ func TestInstanceNew_WithCustomHostname(t *testing.T) {
p := NewPWD(_f, _e, _s, sp, ipf)
p.generator = _g
session, err := p.SessionNew(time.Hour, "", "", "")
session, err := p.SessionNew("", time.Hour, "", "", "")
assert.Nil(t, err)
expectedInstance := types.Instance{

View File

@@ -13,7 +13,7 @@ type Mock struct {
mock.Mock
}
func (m *Mock) SessionNew(duration time.Duration, stack string, stackName, imageName string) (*types.Session, error) {
func (m *Mock) SessionNew(userId string, duration time.Duration, stack string, stackName, imageName string) (*types.Session, error) {
args := m.Called(duration, stack, stackName, imageName)
return args.Get(0).(*types.Session), args.Error(1)
}
@@ -104,3 +104,18 @@ func (m *Mock) ClientCount() int {
args := m.Called()
return args.Int(0)
}
func (m *Mock) UserNewLoginRequest(providerName string) (*types.LoginRequest, error) {
args := m.Called(providerName)
return args.Get(0).(*types.LoginRequest), args.Error(1)
}
func (m *Mock) UserGetLoginRequest(id string) (*types.LoginRequest, error) {
args := m.Called(id)
return args.Get(0).(*types.LoginRequest), args.Error(1)
}
func (m *Mock) UserLogin(loginRequest *types.LoginRequest, user *types.User) (*types.User, error) {
args := m.Called(loginRequest, user)
return args.Get(0).(*types.User), args.Error(1)
}

View File

@@ -72,7 +72,7 @@ func SessionNotEmpty(e error) bool {
}
type PWDApi interface {
SessionNew(duration time.Duration, stack string, stackName, imageName string) (*types.Session, error)
SessionNew(userId string, duration time.Duration, stack string, stackName, imageName string) (*types.Session, error)
SessionClose(session *types.Session) error
SessionGetSmallestViewPort(sessionId string) types.ViewPort
SessionDeployStack(session *types.Session) error
@@ -93,6 +93,10 @@ type PWDApi interface {
ClientResizeViewPort(client *types.Client, cols, rows uint)
ClientClose(client *types.Client)
ClientCount() int
UserNewLoginRequest(providerName string) (*types.LoginRequest, error)
UserGetLoginRequest(id string) (*types.LoginRequest, error)
UserLogin(loginRequest *types.LoginRequest, user *types.User) (*types.User, error)
}
func NewPWD(f docker.FactoryApi, e event.EventApi, s storage.StorageApi, sp provisioner.SessionProvisionerApi, ipf provisioner.InstanceProvisionerFactoryApi) *pwd {

View File

@@ -44,7 +44,7 @@ type SessionSetupInstanceConf struct {
Tls bool `json:"tls"`
}
func (p *pwd) SessionNew(duration time.Duration, stack, stackName, imageName string) (*types.Session, error) {
func (p *pwd) SessionNew(userId string, duration time.Duration, stack, stackName, imageName string) (*types.Session, error) {
defer observeAction("SessionNew", time.Now())
s := &types.Session{}
@@ -53,6 +53,7 @@ func (p *pwd) SessionNew(duration time.Duration, stack, stackName, imageName str
s.ExpiresAt = s.CreatedAt.Add(duration)
s.Ready = true
s.Stack = stack
s.UserId = userId
if s.Stack != "" {
s.Ready = false

View File

@@ -45,7 +45,7 @@ func TestSessionNew(t *testing.T) {
before := time.Now()
s, e := p.SessionNew(time.Hour, "", "", "")
s, e := p.SessionNew("", time.Hour, "", "", "")
assert.Nil(t, e)
assert.NotNil(t, s)
@@ -56,7 +56,7 @@ func TestSessionNew(t *testing.T) {
assert.WithinDuration(t, s.ExpiresAt, before.Add(time.Hour), time.Second)
assert.True(t, s.Ready)
s, _ = p.SessionNew(time.Hour, "stackPath", "stackName", "imageName")
s, _ = p.SessionNew("", time.Hour, "stackPath", "stackName", "imageName")
assert.Equal(t, "stackPath", s.Stack)
assert.Equal(t, "stackName", s.StackName)

View File

@@ -15,6 +15,7 @@ type Session struct {
StackName string `json:"stack_name" bson:"stack_name"`
ImageName string `json:"image_name" bson:"image_name"`
Host string `json:"host" bson:"host"`
UserId string `json:"user_id" bson:"user_id"`
rw sync.Mutex `json:"-"`
}

15
pwd/types/user.go Normal file
View File

@@ -0,0 +1,15 @@
package types
type User struct {
Id string `json:"id" bson:"id"`
Name string `json:"name" bson:"name"`
ProviderUserId string `json:"provider_user_id" bson:"provider_user_id"`
Avatar string `json:"avatar" bson:"avatar"`
Provider string `json:"provider" bson:"provider"`
Email string `json:"email" bson:"email"`
}
type LoginRequest struct {
Id string `json:"id" bson:"id"`
Provider string `json:"provider" bson:"provider"`
}

43
pwd/user.go Normal file
View File

@@ -0,0 +1,43 @@
package pwd
import (
"github.com/play-with-docker/play-with-docker/pwd/types"
"github.com/play-with-docker/play-with-docker/storage"
)
func (p *pwd) UserNewLoginRequest(providerName string) (*types.LoginRequest, error) {
req := &types.LoginRequest{Id: p.generator.NewId(), Provider: providerName}
if err := p.storage.LoginRequestPut(req); err != nil {
return nil, err
}
return req, nil
}
func (p *pwd) UserGetLoginRequest(id string) (*types.LoginRequest, error) {
if req, err := p.storage.LoginRequestGet(id); err != nil {
return nil, err
} else {
return req, nil
}
}
func (p *pwd) UserLogin(loginRequest *types.LoginRequest, user *types.User) (*types.User, error) {
if err := p.storage.LoginRequestDelete(loginRequest.Id); err != nil {
return nil, err
}
u, err := p.storage.UserFindByProvider(user.Provider, user.ProviderUserId)
if err != nil {
if storage.NotFound(err) {
user.Id = p.generator.NewId()
} else {
return nil, err
}
} else {
user.Id = u.Id
}
if err := p.storage.UserPut(user); err != nil {
return nil, err
}
return user, nil
}

View File

@@ -1,90 +0,0 @@
package recaptcha
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/gorilla/securecookie"
"github.com/play-with-docker/play-with-docker/config"
"github.com/rs/xid"
)
func GetGoogleRecaptchaSiteKey() string {
key := os.Getenv("GOOGLE_RECAPTCHA_SITE_KEY")
if key == "" {
// This is a development default. The environment variable should always be set in production.
key = "6LeY_QsUAAAAAOlpVw4MhoLEr50h-dM80oz6M2AX"
}
return key
}
func GetGoogleRecaptchaSiteSecret() string {
key := os.Getenv("GOOGLE_RECAPTCHA_SITE_SECRET")
if key == "" {
// This is a development default. The environment variable should always be set in production.
key = "6LeY_QsUAAAAAHIALCtm0GKfk-UhtXoyJKarnRV8"
}
return key
}
type recaptchaResponse struct {
Success bool `json:"success"`
}
var s = securecookie.New([]byte(config.HashKey), nil).MaxAge(int((1 * time.Hour).Seconds()))
func IsHuman(req *http.Request, rw http.ResponseWriter) bool {
if os.Getenv("GOOGLE_RECAPTCHA_DISABLED") != "" {
return true
}
if cookie, _ := req.Cookie("session_id"); cookie != nil {
var value string
if err := s.Decode("session_id", cookie.Value, &value); err != nil {
fmt.Println(err)
return false
}
return true
}
challenge := req.Form.Get("g-recaptcha-response")
// Of X-Forwarded-For exists, it means we are behind a loadbalancer and we should use the real IP address of the user
ip := req.Header.Get("X-Forwarded-For")
if ip == "" {
// Use the standard remote IP address of the request
ip = req.RemoteAddr
}
parts := strings.Split(ip, ":")
resp, postErr := http.PostForm("https://www.google.com/recaptcha/api/siteverify", url.Values{"secret": {GetGoogleRecaptchaSiteSecret()}, "response": {challenge}, "remoteip": {parts[0]}})
if postErr != nil {
log.Println(postErr)
// If there is a problem to connect to google, assume the user is a human so we don't block real users because of technical issues
return true
}
var r recaptchaResponse
json.NewDecoder(resp.Body).Decode(&r)
if !r.Success {
return false
}
encoded, _ := s.Encode("session_id", xid.New().String())
http.SetCookie(rw, &http.Cookie{
Name: "session_id",
Value: encoded,
Expires: time.Now().Add(1 * time.Hour),
})
return true
}

View File

@@ -2,6 +2,7 @@ package storage
import (
"encoding/json"
"fmt"
"os"
"sync"
@@ -19,10 +20,13 @@ type DB struct {
Instances map[string]*types.Instance `json:"instances"`
Clients map[string]*types.Client `json:"clients"`
WindowsInstances map[string]*types.WindowsInstance `json:"windows_instances"`
LoginRequests map[string]*types.LoginRequest `json:"login_requests"`
Users map[string]*types.User `json:"user"`
WindowsInstancesBySessionId map[string][]string `json:"windows_instances_by_session_id"`
InstancesBySessionId map[string][]string `json:"instances_by_session_id"`
ClientsBySessionId map[string][]string `json:"clients_by_session_id"`
UsersByProvider map[string]string `json:"users_by_providers"`
}
func (store *storage) SessionGet(id string) (*types.Session, error) {
@@ -300,6 +304,56 @@ func (store *storage) ClientFindBySessionId(sessionId string) ([]*types.Client,
return clients, nil
}
func (store *storage) LoginRequestPut(loginRequest *types.LoginRequest) error {
store.rw.Lock()
defer store.rw.Unlock()
store.db.LoginRequests[loginRequest.Id] = loginRequest
return nil
}
func (store *storage) LoginRequestGet(id string) (*types.LoginRequest, error) {
store.rw.Lock()
defer store.rw.Unlock()
if lr, found := store.db.LoginRequests[id]; !found {
return nil, NotFoundError
} else {
return lr, nil
}
}
func (store *storage) LoginRequestDelete(id string) error {
store.rw.Lock()
defer store.rw.Unlock()
delete(store.db.LoginRequests, id)
return nil
}
func (store *storage) UserFindByProvider(providerName, providerUserId string) (*types.User, error) {
store.rw.Lock()
defer store.rw.Unlock()
if userId, found := store.db.UsersByProvider[fmt.Sprintf("%s_%s", providerName, providerUserId)]; !found {
return nil, NotFoundError
} else {
if user, found := store.db.Users[userId]; !found {
return nil, NotFoundError
} else {
return user, nil
}
}
}
func (store *storage) UserPut(user *types.User) error {
store.rw.Lock()
defer store.rw.Unlock()
store.db.UsersByProvider[fmt.Sprintf("%s_%s", user.Provider, user.ProviderUserId)] = user.Id
store.db.Users[user.Id] = user
return nil
}
func (store *storage) load() error {
file, err := os.Open(store.path)
@@ -316,9 +370,12 @@ func (store *storage) load() error {
Instances: map[string]*types.Instance{},
Clients: map[string]*types.Client{},
WindowsInstances: map[string]*types.WindowsInstance{},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{},
InstancesBySessionId: map[string][]string{},
ClientsBySessionId: map[string][]string{},
UsersByProvider: map[string]string{},
}
}

View File

@@ -34,9 +34,12 @@ func TestSessionPut(t *testing.T) {
Instances: map[string]*types.Instance{},
Clients: map[string]*types.Client{},
WindowsInstances: map[string]*types.WindowsInstance{},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{},
InstancesBySessionId: map[string][]string{},
ClientsBySessionId: map[string][]string{},
UsersByProvider: map[string]string{},
}
var loadedDB *DB
@@ -60,9 +63,12 @@ func TestSessionGet(t *testing.T) {
Instances: map[string]*types.Instance{},
Clients: map[string]*types.Client{},
WindowsInstances: map[string]*types.WindowsInstance{},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{},
InstancesBySessionId: map[string][]string{},
ClientsBySessionId: map[string][]string{},
UsersByProvider: map[string]string{},
}
tmpfile, err := ioutil.TempFile("", "pwd")
@@ -96,9 +102,12 @@ func TestSessionGetAll(t *testing.T) {
Instances: map[string]*types.Instance{},
Clients: map[string]*types.Client{},
WindowsInstances: map[string]*types.WindowsInstance{},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{},
InstancesBySessionId: map[string][]string{},
ClientsBySessionId: map[string][]string{},
UsersByProvider: map[string]string{},
}
tmpfile, err := ioutil.TempFile("", "pwd")
@@ -158,9 +167,12 @@ func TestInstanceGet(t *testing.T) {
Instances: map[string]*types.Instance{expectedInstance.Name: expectedInstance},
Clients: map[string]*types.Client{},
WindowsInstances: map[string]*types.WindowsInstance{},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{},
InstancesBySessionId: map[string][]string{expectedInstance.SessionId: []string{expectedInstance.Name}},
ClientsBySessionId: map[string][]string{},
UsersByProvider: map[string]string{},
}
tmpfile, err := ioutil.TempFile("", "pwd")
@@ -209,9 +221,12 @@ func TestInstancePut(t *testing.T) {
Instances: map[string]*types.Instance{i.Name: i},
Clients: map[string]*types.Client{},
WindowsInstances: map[string]*types.WindowsInstance{},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{},
InstancesBySessionId: map[string][]string{i.SessionId: []string{i.Name}},
ClientsBySessionId: map[string][]string{},
UsersByProvider: map[string]string{},
}
var loadedDB *DB
@@ -269,9 +284,12 @@ func TestInstanceFindBySessionId(t *testing.T) {
Instances: map[string]*types.Instance{i1.Name: i1, i2.Name: i2},
Clients: map[string]*types.Client{},
WindowsInstances: map[string]*types.WindowsInstance{},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{},
InstancesBySessionId: map[string][]string{i1.SessionId: []string{i1.Name, i2.Name}},
ClientsBySessionId: map[string][]string{},
UsersByProvider: map[string]string{},
}
tmpfile, err := ioutil.TempFile("", "pwd")
@@ -302,9 +320,12 @@ func TestWindowsInstanceGetAll(t *testing.T) {
Instances: map[string]*types.Instance{},
Clients: map[string]*types.Client{},
WindowsInstances: map[string]*types.WindowsInstance{i1.Id: i1, i2.Id: i2},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{i1.SessionId: []string{i1.Id, i2.Id}},
InstancesBySessionId: map[string][]string{},
ClientsBySessionId: map[string][]string{},
UsersByProvider: map[string]string{},
}
tmpfile, err := ioutil.TempFile("", "pwd")
@@ -354,9 +375,12 @@ func TestWindowsInstancePut(t *testing.T) {
Instances: map[string]*types.Instance{},
Clients: map[string]*types.Client{},
WindowsInstances: map[string]*types.WindowsInstance{i.Id: i},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{i.SessionId: []string{i.Id}},
InstancesBySessionId: map[string][]string{},
ClientsBySessionId: map[string][]string{},
UsersByProvider: map[string]string{},
}
var loadedDB *DB
@@ -413,9 +437,12 @@ func TestClientGet(t *testing.T) {
Instances: map[string]*types.Instance{},
Clients: map[string]*types.Client{c.Id: c},
WindowsInstances: map[string]*types.WindowsInstance{},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{},
InstancesBySessionId: map[string][]string{},
ClientsBySessionId: map[string][]string{c.SessionId: []string{c.Id}},
UsersByProvider: map[string]string{},
}
tmpfile, err := ioutil.TempFile("", "pwd")
@@ -464,9 +491,12 @@ func TestClientPut(t *testing.T) {
Instances: map[string]*types.Instance{},
Clients: map[string]*types.Client{c.Id: c},
WindowsInstances: map[string]*types.WindowsInstance{},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{},
InstancesBySessionId: map[string][]string{},
ClientsBySessionId: map[string][]string{c.SessionId: []string{c.Id}},
UsersByProvider: map[string]string{},
}
var loadedDB *DB
@@ -524,9 +554,12 @@ func TestClientFindBySessionId(t *testing.T) {
Instances: map[string]*types.Instance{},
Clients: map[string]*types.Client{c1.Id: c1, c2.Id: c2},
WindowsInstances: map[string]*types.WindowsInstance{},
LoginRequests: map[string]*types.LoginRequest{},
Users: map[string]*types.User{},
WindowsInstancesBySessionId: map[string][]string{},
InstancesBySessionId: map[string][]string{},
ClientsBySessionId: map[string][]string{c1.SessionId: []string{c1.Id, c2.Id}},
UsersByProvider: map[string]string{},
}
tmpfile, err := ioutil.TempFile("", "pwd")

View File

@@ -82,3 +82,23 @@ func (m *Mock) ClientFindBySessionId(sessionId string) ([]*types.Client, error)
args := m.Called(sessionId)
return args.Get(0).([]*types.Client), args.Error(1)
}
func (m *Mock) LoginRequestPut(loginRequest *types.LoginRequest) error {
args := m.Called(loginRequest)
return args.Error(0)
}
func (m *Mock) LoginRequestGet(id string) (*types.LoginRequest, error) {
args := m.Called(id)
return args.Get(0).(*types.LoginRequest), args.Error(1)
}
func (m *Mock) LoginRequestDelete(id string) error {
args := m.Called(id)
return args.Error(0)
}
func (m *Mock) UserFindByProvider(providerName, providerUserId string) (*types.User, error) {
args := m.Called(providerName, providerUserId)
return args.Get(0).(*types.User), args.Error(1)
}
func (m *Mock) UserPut(user *types.User) error {
args := m.Called(user)
return args.Error(0)
}

View File

@@ -34,4 +34,11 @@ type StorageApi interface {
ClientDelete(id string) error
ClientCount() (int, error)
ClientFindBySessionId(sessionId string) ([]*types.Client, error)
LoginRequestPut(loginRequest *types.LoginRequest) error
LoginRequestGet(id string) (*types.LoginRequest, error)
LoginRequestDelete(id string) error
UserFindByProvider(providerName, providerUserId string) (*types.User, error)
UserPut(user *types.User) error
}

View File

@@ -1,21 +0,0 @@
package templates
import (
"bytes"
"html/template"
"github.com/play-with-docker/play-with-docker/recaptcha"
)
func GetWelcomeTemplate() ([]byte, error) {
welcomeTemplate, tplErr := template.New("welcome").ParseFiles("www/welcome.html")
if tplErr != nil {
return nil, tplErr
}
var b bytes.Buffer
tplExecuteErr := welcomeTemplate.ExecuteTemplate(&b, "GOOGLE_RECAPTCHA_SITE_KEY", recaptcha.GetGoogleRecaptchaSiteKey())
if tplExecuteErr != nil {
return nil, tplExecuteErr
}
return b.Bytes(), nil
}

View File

@@ -78,7 +78,7 @@
});
$scope.showAlert = function(title, content, parent) {
$scope.showAlert = function(title, content, parent, cb) {
$mdDialog.show(
$mdDialog.alert()
.parent(angular.element(document.querySelector(parent || '#popupContainer')))
@@ -86,7 +86,11 @@
.title(title)
.textContent(content)
.ok('Got it!')
);
).finally(function() {
if (cb) {
cb();
}
});
}
$scope.resize = function(geometry) {
@@ -206,7 +210,9 @@
});
socket.on('session end', function() {
$scope.showAlert('Session timed out!', 'Your session has expired and all of your instances have been deleted.', '#sessionEnd')
$scope.showAlert('Session timed out!', 'Your session has expired and all of your instances have been deleted.', '#sessionEnd', function() {
window.location.href = '/';
});
$scope.isAlive = false;
});

79
www/assets/landing.css Normal file
View File

@@ -0,0 +1,79 @@
/* Space out content a bit */
body {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing,
.footer {
padding-right: 1rem;
padding-left: 1rem;
}
/* Custom page header */
.header {
padding-bottom: 1rem;
border-bottom: .05rem solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 3rem;
}
/* Custom page footer */
.footer {
padding-top: 1.5rem;
color: #777;
border-top: .05rem solid #e5e5e5;
}
/* Customize container */
@media (min-width: 48em) {
.container {
max-width: 46rem;
}
}
.container-narrow > hr {
margin: 2rem 0;
}
/* Main marketing message and sign up button */
.jumbotron {
text-align: center;
border-bottom: .05rem solid #e5e5e5;
}
.jumbotron .btn {
padding: .75rem 1.5rem;
font-size: 1.5rem;
}
/* Supporting marketing content */
.marketing {
margin: 3rem 0;
}
.marketing p + h4 {
margin-top: 1.5rem;
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 48em) {
/* Remove the padding we set earlier */
.header,
.marketing,
.footer {
padding-right: 0;
padding-left: 0;
}
/* Space out the masthead */
.header {
margin-bottom: 2rem;
}
/* Remove the bottom border on the jumbotron for visual effect */
.jumbotron {
border-bottom: 0;
}
}

116
www/landing.html Normal file
View File

@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en" ng-app="PWDLanding" ng-controller="LoginController">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular-cookies.js"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Play with Docker</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="/assets/landing.css" rel="stylesheet">
<script>
angular.module('PWDLanding', ['ngCookies'])
.controller('LoginController', ['$cookies', '$scope', '$http', function($cookies, $scope, $http) {
$scope.providers = [];
$scope.loggedIn = $cookies.get('id') !== undefined;
$http({
method: 'GET',
url: '/oauth/providers'
}).then(function(response) {
$scope.providers = response.data;
if ($scope.providers.length == 0) {
$scope.loggedIn = true;
}
}, function(response) {
console.log('ERROR', response);
});
$scope.start = function() {
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
var stack = getParameterByName('stack');
if (stack) {
document.getElementById('stack').value = stack;
}
var stackName = getParameterByName('stack_name');
if (stackName) {
document.getElementById('stack_name').value = stackName;
}
var imageName = getParameterByName('image_name');
if (imageName) {
document.getElementById('image_name').value = imageName;
}
document.getElementById('landingForm').submit();
}
}]);
</script>
</head>
<body>
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills float-right">
<li class="nav-item">
<a class="nav-link" href="https://github.com/play-with-docker/play-with-docker">Contribute</a>
</li>
</ul>
</nav>
</div>
<div class="jumbotron" ng-cloak>
<img src="https://www.docker.com/sites/default/files/Whale%20Logo332_5.png" />
<h1 class="display-3">Play with Docker</h1>
<p class="lead">A simple, interactive and fun playground to learn Docker</p>
<div ng-hide="loggedIn" class="btn-group" role="group">
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Login
</button>
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<a ng-repeat="provider in providers" class="dropdown-item" href="/oauth/providers/{{provider}}/login">{{provider}}</a>
</div>
</div>
<form id="landingForm" method="POST" action="/">
<p ng-show="loggedIn"><a class="btn btn-lg btn-success" href="#" ng-click="start()" role="button">Start</a></p>
<input id="stack" type="hidden" name="stack" value=""/>
<input id="stack_name" type="hidden" name="stack_name" value=""/>
<input id="image_name" type="hidden" name="image_name" value=""/>
</form>
</div>
<div class="row marketing">
<div class="col-lg-12">
<p>Play with Docker (PWD) is a project hacked by <a href="https://www.twitter.com/marcosnils">Marcos Liljedhal</a> and <a href="https://www.twitter.com/xetorthio">Jonathan Leibiusky</a> and sponsored by Docker Inc.</p>
<p>PWD is a Docker playground which allows users to run Docker commands in a matter of seconds. It gives the experience of having a free Alpine Linux Virtual Machine in browser, where you can build and run Docker containers and even create clusters in <a href="https://docs.docker.com/engine/swarm/">Docker Swarm Mode</a>. Under the hood Docker-in-Docker (DinD) is used to give the effect of multiple VMs/PCs. In addition to the playground, PWD also includes a training site composed of a large set of Docker labs and quizzes from beginner to advanced level available at <a href="http://training.play-with-docker.com/">training.play-with-docker.com</a>.</p>
</div>
</div>
<footer class="footer">
<p>&copy; Play with Docker 2017</p>
</footer>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
</body>
</html>

View File

@@ -1,57 +0,0 @@
{{define "GOOGLE_RECAPTCHA_SITE_KEY"}}
<!doctype html>
<html>
<head>
<title>Docker Playground</title>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.css">
<link rel="stylesheet" href="/assets/style.css" />
<script src='https://www.google.com/recaptcha/api.js'></script>
</head>
<body class="welcome">
<div>
<h1>Welcome!</h1>
<h2>Before starting we need to verify you are a human</h2>
<form id="welcomeForm" method="POST" action="/">
<div id="recaptcha" class="g-recaptcha" data-callback="iAmHuman" data-sitekey="{{.}}"></div>
<input id="stack" type="hidden" name="stack" value=""/>
<input id="stack_name" type="hidden" name="stack_name" value=""/>
<input id="image_name" type="hidden" name="image_name" value=""/>
<button id="create" style="display:none;">Create session</button>
</form>
<img src="/assets/full_horizontal.svg" />
</div>
<script>
function iAmHuman(resp) {
document.getElementById('welcomeForm').submit();
}
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
var stack = getParameterByName('stack');
if (stack) {
document.getElementById('stack').value = stack;
}
var stackName = getParameterByName('stack_name');
if (stackName) {
document.getElementById('stack_name').value = stackName;
}
var imageName = getParameterByName('image_name');
if (imageName) {
document.getElementById('image_name').value = imageName;
}
if (document.cookie.indexOf('session_id') > -1) {
document.getElementById('create').style = "";
document.getElementById('recaptcha').style = "display:none;";
}
</script>
</body>
</html>
{{end}}