Authenticate WS connection using token

Safari doesn't support basic authentication for websocket sessions.
This commit introduces a token-based authentication only for websocket
connection.
The token is shared by all clients and that might be not secure. However,
basic authentication itself is insecure and the credential is already
shared by clients, so don't mind.
This commit is contained in:
Iwasaki Yudai 2015-08-31 15:54:34 +09:00
parent e7e607b3d7
commit 0e81c484a9
4 changed files with 47 additions and 34 deletions

View File

@ -29,8 +29,9 @@ type App struct {
command []string
options *Options
upgrader *websocket.Upgrader
server *manners.GracefulServer
upgrader *websocket.Upgrader
server *manners.GracefulServer
authToken string
titleTemplate *template.Template
}
@ -88,19 +89,13 @@ func New(command []string, options *Options) (*App, error) {
WriteBufferSize: 1024,
Subprotocols: []string{"gotty"},
},
authToken: generateRandomString(20),
titleTemplate: titleTemplate,
}, nil
}
func ApplyConfigFile(options *Options, configFilePath string) error {
if err := applyConfigFile(options, configFilePath); err != nil {
return err
}
return nil
}
func applyConfigFile(options *Options, filePath string) error {
func ApplyConfigFile(options *Options, filePath string) error {
filePath = ExpandHomeDir(filePath)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return err
@ -146,19 +141,15 @@ func applyConfigFile(options *Options, filePath string) error {
return nil
}
func ExpandHomeDir(path string) string {
if path[0:2] == "~/" {
return os.Getenv("HOME") + path[1:]
} else {
return path
}
}
func (app *App) Run() error {
if app.options.PermitWrite {
log.Printf("Permitting clients to write input to the PTY.")
}
if app.options.Once {
log.Printf("Once option is provided, accepting only one client")
}
path := ""
if app.options.EnableRandomUrl {
path += "/" + generateRandomString(app.options.RandomUrlLength)
@ -167,31 +158,23 @@ func (app *App) Run() error {
endpoint := net.JoinHostPort(app.options.Address, app.options.Port)
wsHandler := http.HandlerFunc(app.handleWS)
customIndexHandler := http.HandlerFunc(app.handleCustomIndex)
authTokenHandler := http.HandlerFunc(app.handleAuthToken)
staticHandler := http.FileServer(
&assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: "static"},
)
if app.options.Once {
log.Printf("Once option is provided, accepting only one client")
}
var siteMux = http.NewServeMux()
if app.options.IndexFile != "" {
log.Printf("Using index file at " + app.options.IndexFile)
indexHandler := http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, ExpandHomeDir(app.options.IndexFile))
},
)
siteMux.Handle(path+"/", indexHandler)
siteMux.Handle(path+"/", customIndexHandler)
} else {
siteMux.Handle(path+"/", http.StripPrefix(path+"/", staticHandler))
}
siteMux.Handle(path+"/auth_token.js", authTokenHandler)
siteMux.Handle(path+"/js/", http.StripPrefix(path+"/", staticHandler))
siteMux.Handle(path+"/favicon.png", http.StripPrefix(path+"/", staticHandler))
siteMux.Handle(path+"/ws", wsHandler)
siteHandler := http.Handler(siteMux)
@ -200,6 +183,11 @@ func (app *App) Run() error {
siteHandler = wrapBasicAuth(siteHandler, app.options.Credential)
}
wsMux := http.NewServeMux()
wsMux.Handle("/", siteHandler)
wsMux.Handle(path+"/ws", wsHandler)
siteHandler = (http.Handler(wsMux))
siteHandler = wrapLogger(siteHandler)
scheme := "http"
@ -264,6 +252,12 @@ func (app *App) handleWS(w http.ResponseWriter, r *http.Request) {
return
}
_, initMessage, err := conn.ReadMessage()
if err != nil || string(initMessage) != app.authToken {
log.Print("Failed to authenticate websocket connection")
return
}
cmd := exec.Command(app.command[0], app.command[1:]...)
ptyIo, err := pty.Start(cmd)
if err != nil {
@ -283,6 +277,14 @@ func (app *App) handleWS(w http.ResponseWriter, r *http.Request) {
context.goHandleClient()
}
func (app *App) handleCustomIndex(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, ExpandHomeDir(app.options.IndexFile))
}
func (app *App) handleAuthToken(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("var gotty_auth_token = '" + app.authToken + "';"))
}
func (app *App) Exit() (firstCall bool) {
if app.server != nil {
log.Printf("Received Exit command, waiting for all clients to close sessions...")
@ -355,3 +357,11 @@ func listAddresses() (addresses []string) {
return
}
func ExpandHomeDir(path string) string {
if path[0:2] == "~/" {
return os.Getenv("HOME") + path[1:]
} else {
return path
}
}

View File

@ -91,7 +91,7 @@ func staticFaviconPng() (*asset, error) {
return a, nil
}
var _staticIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x74\x90\xc1\x52\xc4\x20\x0c\x86\xef\x3e\x45\xc4\xf1\xe6\x94\x7a\xdd\xd2\x5e\x7d\x81\xbd\x78\x64\x4b\xb6\x64\xa5\xc0\x40\x5c\xed\x38\xbe\xbb\xb0\xd8\x93\xe3\x89\x3f\xf9\xff\x49\x3e\xa2\xee\x4d\x98\x79\x8b\x08\x96\x57\x37\xdd\xa9\xf6\x00\x28\x8b\xda\x54\x51\x24\x13\x3b\x9c\x5e\xc2\xf1\xf8\xaa\x64\x2b\x9a\x91\x79\x2b\xfa\x14\xcc\xf6\x04\x0f\x8c\x69\x25\xaf\x1d\x7c\xc5\x90\x89\x29\xf8\x03\xe8\x53\x0e\xee\x9d\x71\x00\x8b\xb4\x58\x3e\xc0\x73\xdf\x3f\x0e\xf0\x41\x86\xed\x5e\xac\x3a\x2d\x54\xc2\x7d\xfc\x1c\xbe\x95\x6c\x43\xdb\x02\x47\xfe\x0d\x12\xba\x51\xd0\x1c\xbc\x80\x4a\x5a\xf4\xaa\x17\x94\xd1\x2f\x02\x6c\xc2\xf3\x28\xce\xfa\x5a\xfd\xae\xb6\x6e\xf0\x72\xa7\x57\x15\xee\x77\x98\xa1\x2b\x90\x19\xc5\x0e\x2a\x26\x25\x4b\x6f\xff\xcb\x9c\x28\x32\xe4\x34\x8f\xa2\x93\x97\x2c\x6d\xcd\x75\x97\x5c\x63\xcd\xfc\x2f\xb9\x04\xe6\xed\x4f\x52\xc9\xb6\xbb\xc0\xdc\x6e\xfa\x13\x00\x00\xff\xff\x96\x39\xb4\x53\x6b\x01\x00\x00")
var _staticIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x84\x90\xc1\x52\xc3\x20\x10\x86\xef\x3e\xc5\x8a\xe3\xcd\x09\xf1\xda\x90\x5c\x7d\x81\x5e\x3c\x39\x34\x6c\xc3\xb6\x04\x32\xb0\xad\x66\x1c\xdf\x5d\x28\xe6\xa4\x33\x9e\xf8\x77\xf7\x9b\xe5\x03\x75\x6f\xc2\xc8\xeb\x82\x60\x79\x76\xc3\x9d\xaa\x07\x80\xb2\xa8\x4d\x09\x39\x32\xb1\xc3\xe1\x25\xec\xf7\xaf\x4a\xd6\xa2\x0e\x12\xaf\x39\x1f\x82\x59\x9f\xe0\x81\x31\xce\xe4\xb5\x83\xcf\x25\x24\x62\x0a\x7e\x07\xfa\x90\x82\xbb\x30\x76\x60\x91\x26\xcb\x3b\x78\x6e\xdb\xc7\x0e\xde\xc9\xb0\xdd\x8a\x59\xc7\x89\x32\xdc\x2e\x1f\xdd\x97\x92\x75\x69\xbd\xc0\x91\x3f\x43\x44\xd7\x0b\x1a\x83\x17\x50\x4c\x73\x9e\xf5\x84\x72\xf1\x93\x00\x1b\xf1\xd8\x8b\xa3\xbe\x96\x79\x53\x5a\x37\x79\xb9\xd9\xab\x22\xf7\xb3\xcc\xd0\x15\xc8\xf4\x62\x13\x15\x83\x92\xb9\xb7\xbd\x65\x8c\xb4\x30\xa4\x38\xf6\xa2\x91\xa7\x24\x6d\xe1\x9a\x53\x2a\x58\x1d\xfe\x49\xea\x0b\xdb\x37\x0e\x67\xf4\xff\xb3\x79\xeb\x14\x98\xd7\x5f\xa4\x92\xd5\x33\x8b\xdf\xfe\xff\x3b\x00\x00\xff\xff\x35\xaf\x3e\x6a\x97\x01\x00\x00")
func staticIndexHtmlBytes() ([]byte, error) {
return bindataRead(
@ -106,12 +106,12 @@ func staticIndexHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/index.html", size: 363, mode: os.FileMode(436), modTime: time.Unix(1440919621, 0)}
info := bindataFileInfo{name: "static/index.html", size: 407, mode: os.FileMode(436), modTime: time.Unix(1441003850, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _staticJsGottyJs = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x94\x56\x5f\x6f\xe2\x38\x10\x7f\xef\xa7\xb0\xf2\x82\x73\x47\x5d\xb8\x93\xee\x01\xc4\x9d\x74\x55\x57\xda\x3f\xda\xae\x0a\xbb\x7d\xa8\xfa\x60\x92\x01\xb2\x18\x1b\xd9\x4e\x23\x76\xc5\x77\xdf\x99\x10\x20\x04\xa7\x6c\xfd\x10\x19\xcf\xcc\x6f\xfe\x78\x7e\x63\xf8\x2c\xd7\x89\xcf\x8c\xe6\x31\xfb\x79\xc5\x70\xbd\x48\xcb\x16\xde\xaf\xdd\x9d\x96\x53\x05\x29\x1b\xb1\x22\xd3\xa9\x29\x84\x32\x89\x24\x55\xb1\xb6\xc6\x9b\xc4\x28\x36\x1a\xb1\xa8\xd4\x1d\x44\xc3\x83\x71\x6e\x51\xc0\xf8\x09\xc6\x7f\xac\x53\x38\x37\xb8\xb9\xe9\xb0\x01\x6d\x69\x17\xb3\x3f\xcf\x90\x17\xc6\xf9\xc0\xf1\x5a\xfa\x85\x96\x2b\x40\x11\x1a\x77\x8e\xbe\xf6\x91\x38\xf4\xf8\x14\xcd\x8d\xf7\x9b\xe8\xf9\x28\x96\xb9\x37\x0f\x90\x18\xad\x21\xf1\xa8\x72\xdd\x1f\x5e\x1d\x84\x66\x0d\xfa\x91\x0c\xcf\x4a\xb0\xd7\x28\x48\xaa\xa1\x60\x8f\x30\x1d\x9b\x64\x09\x9e\x63\x72\xdd\xa3\xd7\xb8\x82\xdb\x1b\x78\xb0\xab\xda\x51\xe1\x84\xd1\xe4\xa6\xee\x04\x5e\x40\xfb\xba\x27\x5a\x0b\xb2\x14\x29\xcc\x64\xae\xfc\xd8\x1b\x2b\xe7\x50\xf9\x56\xd9\x54\x54\x27\xe2\x13\x16\x44\xf1\x78\x78\xd1\x56\x24\x0a\xa4\xe5\xf5\xf8\x68\x91\x66\x05\xbb\xb3\x9a\xe0\x27\xd3\x3b\xcc\x33\x4d\x31\x07\xff\xc5\xc2\xcc\xf1\x58\x38\xcc\x3d\x72\xa0\xd3\x6b\xd0\x89\x49\x33\x3d\x8f\xba\x2c\xb2\xb2\x88\x82\x96\x46\xef\x91\x1f\x40\xa6\x9b\xb6\x22\xd7\x6b\x97\x19\xd4\x2a\x8d\x33\x23\xd6\xb9\x5b\x9c\xc5\x44\x0b\x65\x46\x7f\x9b\x7c\x84\x8d\xf3\xd6\x2c\xa1\x8e\x8c\x27\x21\xf0\xea\x26\x28\x78\x1e\xf5\x22\xec\x21\x52\x1c\x9e\xe9\x6d\xc3\xee\xc8\x6e\xec\x2d\x66\x8c\xbe\x9a\xee\xdb\x22\x3c\x66\xef\xb2\x1f\x27\x41\x62\xdb\xe4\x2b\xed\xba\xcc\x9a\xc2\x5d\x0a\x37\x28\xa4\x15\xf5\x29\x8f\x0f\xe3\xfb\xcf\xc2\x95\xb1\x65\xb3\x4d\xbb\x36\xad\xb0\xa3\xfa\xaa\x22\x1b\xec\x37\xdd\x8b\x16\x94\xc2\xa0\xfc\xbe\xae\xbb\x6d\x95\xc6\x41\xc9\xf9\x69\xe8\x6e\x76\xbd\xa2\x9d\x97\x4a\xe1\x85\x4c\x8d\xb4\x69\x93\x1b\x4d\xbb\x8a\x2a\x09\x92\xc4\x03\x4f\x4d\x92\xaf\x90\x8d\xd4\xe8\x77\x0a\x68\xfb\xff\xe6\x3d\x76\x89\xaf\xae\x2f\x8a\x6b\x78\xdb\x26\xb3\x57\xe0\xdc\x8e\xa7\xaf\x93\x3b\x95\x5e\xa2\x52\x29\x13\xf4\x43\x38\x95\x25\xc0\xfb\x8d\x60\x5d\x91\xf9\x64\xc1\x8f\x7a\x4f\xbd\xe7\x26\x56\x22\x1d\xb0\x4e\xaf\x33\x68\x29\x87\x11\x85\xcd\x3c\x7c\x9d\xbc\xeb\xff\xc3\x09\x23\xd0\xe8\x53\x0b\x72\x39\x0c\xc0\xf6\xdb\x60\x91\xfd\x8f\xe5\x48\x9e\x64\x5e\xc1\x9b\x71\xff\x0a\xe0\xae\x71\xb0\x80\xc5\x61\x02\x34\x64\xcb\x4e\x5e\x4b\xeb\x5a\xc1\xef\xa7\xdf\x71\x86\x8b\x25\x32\x8f\xd7\x6c\x63\x31\x33\xf6\x4e\x62\xd9\x0e\x77\x80\x2a\x6d\xbc\xc2\x97\xc0\x19\x05\xf8\xb0\xcc\x79\x34\x06\xef\x89\xd5\xc4\x24\xb4\xc1\x6f\x34\x28\x7f\xd4\x63\x7b\x42\xc9\x73\x20\x9c\x43\x69\x1a\x33\x12\xd5\xbb\xbf\x63\xbf\x7d\x4b\xfd\xfe\x0e\xd4\xaf\xf9\xb4\x5d\xae\x60\x00\xfe\x48\xcc\x6d\xa3\xb7\x13\x65\xdc\xe5\xce\xce\x66\x8c\x53\x15\x42\xf5\x2e\xab\x93\xeb\x0b\x04\x3d\x68\xd2\xa4\x5d\x98\xe2\xfe\x05\xac\x92\x1b\x1e\xdd\xee\x32\x43\xd7\xec\x96\x62\x49\xf1\xbd\xd1\xb9\x52\x4d\x82\x9f\xd2\xdb\x6e\x0e\x35\xa9\x7b\x6a\x66\x07\xd6\x1a\x7b\x92\x1d\x1d\x34\xb3\x68\x47\xab\x61\xee\xfe\x45\x1c\xde\xaa\xf2\x5f\x40\xcd\xae\xed\xe5\xa3\xca\x9d\x5e\xe1\xbf\x23\xd6\x6b\x46\x80\x1d\x35\xc9\x56\x60\x72\xcf\x77\x7e\xba\x8d\x7b\xff\x83\xf5\x7b\xbd\x5e\x20\xb6\x6d\xcc\xe3\xab\x5f\x01\x00\x00\xff\xff\x42\x24\x4b\x87\xdd\x09\x00\x00")
var _staticJsGottyJs = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x94\x56\xdd\x8e\xda\x3a\x10\xbe\xdf\xa7\xb0\x72\x83\x73\x0e\x9b\x85\x73\xa4\x5e\x80\x68\xa5\xae\xb6\x52\x7f\xd4\xad\x16\xda\xbd\x58\xad\x56\x26\x19\x48\x8a\xb1\x91\x3d\xd9\x88\x56\xbc\x7b\xc7\x21\x40\x08\xce\xd2\xf5\x45\x64\x3c\xdf\x7c\xf3\xe3\x99\x31\x7c\x96\xab\x18\x33\xad\x78\xc8\x7e\x5f\x30\x5a\xcf\xc2\xb0\x14\x71\x65\x6f\x94\x98\x4a\x48\xd8\x88\x15\x99\x4a\x74\x11\x49\x1d\x0b\x07\x8d\x56\x46\xa3\x8e\xb5\x64\xa3\x11\x0b\x4a\xec\x20\x18\xee\x95\x73\x43\x02\xc6\x8f\x38\xde\xb1\x4e\x61\xed\xe0\xea\xaa\xc3\x06\x6e\xeb\x76\x21\xfb\xf7\x84\x39\xd5\x16\x3d\xc7\x2b\x81\xa9\x12\x4b\x20\x11\x29\x77\x0e\xb6\x76\x9e\x58\xb2\xf8\x10\xcc\x35\xe2\x3a\x78\x3c\x88\x45\x8e\xfa\x0e\x62\xad\x14\xc4\x48\x90\xcb\xfe\xf0\x62\x2f\xd4\x2b\x50\xf7\x4e\xf1\x24\x05\x3b\x44\xe1\xa4\x0a\x0a\x76\x0f\xd3\xb1\x8e\x17\x80\x9c\x82\xeb\x1e\xac\x86\x15\xdd\x4e\x01\xc1\x2c\x6b\x47\x85\x8d\xb4\x72\x66\xea\x46\xe0\x19\x14\xd6\x2d\x55\x48\x0b\x2a\xe1\x65\x04\x4f\xe4\x76\xfa\x84\x7a\x01\xaa\x6e\xc0\xad\xd4\x59\x88\x12\x98\x89\x5c\xe2\x18\xb5\x11\x73\xa8\x7c\x94\xd9\x34\xaa\x4e\xa2\x2f\x94\x38\xc9\xc3\xe1\x59\xdd\x28\x96\x20\x0c\x6f\x9a\x71\xc8\x8a\x76\xab\x35\xa1\x4f\xa6\xb6\x9c\x27\xc8\x68\x0e\xf8\xcd\xc0\xcc\xf2\x90\xa2\x40\x1e\xb8\x50\x2e\x41\xc5\x3a\xc9\xd4\x3c\xe8\xb2\xc0\x88\x22\xf0\x6a\x6a\xb5\x63\xbe\x03\x91\xac\xdb\x2e\xa3\x9e\xe3\x4c\x13\xaa\x54\xce\x74\xb4\xca\x6d\x7a\xe2\x93\x5b\x24\xd3\xea\xc7\xe4\x33\xac\x2d\x1a\xca\x64\x9d\x99\x4e\x7c\xe4\xf5\x7b\x08\x7a\x01\xd5\x9a\x03\x0e\x4f\x70\x1b\xbf\x39\xa7\x37\x46\x43\x11\x93\xad\xa6\xf9\x36\x0f\x0f\xd1\xdb\xec\xd7\x91\x93\x54\x5e\xf9\x52\xd9\x2e\x33\xba\xb0\xe7\xdc\xf5\x0a\xdd\x0a\xfa\x2e\x8e\x4f\xe3\xdb\xaf\x91\x2d\x7d\xcb\x66\xeb\x76\xb4\x5b\x7e\x43\xf5\x55\x79\x36\xd8\x6d\xba\x67\x35\x5c\x08\x83\xf2\xfb\x32\x76\xd3\x2a\x0d\xbd\x92\xd3\x53\xdf\xdd\x6c\x6b\x45\x59\x14\x52\xd2\x85\x4c\xb5\x30\x49\xb3\x37\x9a\x7a\x55\xab\xc4\xd4\x24\x08\x3c\xd1\x71\xbe\xa4\xae\x75\x85\x7e\x23\xc1\x6d\xdf\xaf\x3f\x52\x95\x60\x75\x7d\x41\x58\xe3\xdb\x34\x27\xc0\x12\xac\xdd\xf6\xe9\xcb\x43\x20\x11\x28\x08\x54\xca\x22\xf7\x23\xb2\x32\x8b\x81\xf7\x1b\xce\xda\x22\xc3\x38\xe5\x07\xdc\x43\xef\xb1\xc9\x15\x0b\x0b\xac\xd3\xeb\x0c\x5a\xd2\xa1\xa3\xc2\x64\x08\xdf\x27\x1f\xfa\x6f\xb8\xe3\xf0\x14\xfa\xd4\x80\x58\x0c\x3d\xb4\xfd\x36\x5a\xea\xfe\xfb\x72\x74\x4f\x32\x94\xf0\x6a\xde\xff\x3c\xbc\x2b\x1a\x2c\x60\x68\x98\x80\x1b\xc6\x65\x25\xaf\x84\xb1\xad\xe4\xb7\xd3\x9f\x34\xeb\xa3\x05\x75\x1e\xaf\xe9\x86\xd1\x4c\x9b\x1b\x41\x69\xdb\xdf\x01\x41\xda\xfa\x8a\x5e\x0c\xab\x25\xd0\x03\x34\xe7\xc1\x18\x10\x5d\x57\xbb\x4e\x22\x1d\xfa\x06\x83\xf2\x47\xdd\xb7\x07\x92\x3c\x7a\xdc\xd9\xa7\xa6\x31\x23\x09\xde\xfd\x1b\xfd\xcd\x6b\xf2\xf7\xbf\x27\x7f\xcd\x27\xf0\x7c\x06\x3d\xf4\x87\xc6\xdc\x34\x6a\x3b\x96\xda\x9e\xaf\xec\x6c\xc6\xb8\xcb\x82\x2f\xdf\x65\x76\x72\x75\xa6\x41\xf7\x48\x37\x69\x53\x5d\xdc\x3e\x83\x91\x62\xcd\x83\xeb\x6d\x64\x64\x9a\x5d\x3b\x5f\x12\x7a\x6f\x54\x2e\x65\xb3\xc1\x8f\xdb\xdb\xac\xf7\x39\xa9\x5b\x6a\x46\x07\xc6\x68\x73\x14\x9d\x3b\x68\x46\xd1\xce\x56\xe3\xdc\xfe\xdb\xd8\xbf\x55\xe5\xbf\x85\x9a\x5e\xdb\xcb\xe7\x32\x77\x7c\x85\x6f\x47\xac\xd7\xf4\x80\x2a\x6a\x92\x2d\x41\xe7\xc8\xb7\x76\xba\x8d\x7b\xff\x87\xf5\x7b\xbd\x9e\xc7\xb7\x4d\xc8\xc3\x8b\x3f\x01\x00\x00\xff\xff\x4f\xa7\xa4\xe9\x05\x0a\x00\x00")
func staticJsGottyJsBytes() ([]byte, error) {
return bindataRead(
@ -126,7 +126,7 @@ func staticJsGottyJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/js/gotty.js", size: 2525, mode: os.FileMode(436), modTime: time.Unix(1440919623, 0)}
info := bindataFileInfo{name: "static/js/gotty.js", size: 2565, mode: os.FileMode(436), modTime: time.Unix(1441003850, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}

View File

@ -10,6 +10,8 @@
var term;
ws.onopen = function(event) {
ws.send(gotty_auth_token);
hterm.defaultStorage = new lib.Storage.Local();
hterm.defaultStorage.clear();

View File

@ -8,6 +8,7 @@
<body>
<div id="terminal"></div>
<script src="./js/hterm.js"></script>
<script src="./auth_token.js"></script>
<script src="./js/gotty.js"></script>
</body>
</html>