diff --git a/build.sh b/build.sh
index d9810230..afb01940 100755
--- a/build.sh
+++ b/build.sh
@@ -7,13 +7,36 @@
# amd64, arm64, armhf and i386.
#
# [--ups]: Build the ubuntu-personal-store snap as well.
+#
+# [--with-webconf]: Adds the new webconf service to the snapweb snap.
+#
set -e
-if [ "$#" -eq 0 ] || ([ "$#" -eq 1 ] && [ "$1" = "--ups" ]); then
+BUILD_UPS_SNAP=
+WITH_WEBCONF=
+
+while [ "$1" != "" ]; do
+ case $1 in
+ --ups)
+ BUILD_UPS_SNAP=1
+ ;;
+ --with-webconf)
+ WITH_WEBCONF=1
+ ;;
+ amd64|i386|arm64|armhf)
+ architectures+=("$1")
+ ;;
+ *)
+ # print usage
+ head -13 $0 | tail -12
+ exit
+ esac
+ shift
+done
+
+if [ "$architectures" = "" ]; then
architectures=( amd64 arm64 armhf i386 )
-else
- architectures=( "$@" )
fi
AVAHI_VERSION="0.6.31-4ubuntu4snap2"
@@ -59,7 +82,10 @@ gobuild() {
mkdir -p $output_dir
cd $output_dir
- GOARCH=$arch GOARM=7 CGO_ENABLED=1 CC=${plat_abi}-gcc go build -ldflags "-extld=${plat_abi}-gcc -X main.httpAddr=${httpAddr} -X main.httpsAddr=${httpsAddr}" github.com/snapcore/snapweb/cmd/snapweb
+ GOARCH=$arch GOARM=7 CGO_ENABLED=1 CC=${plat_abi}-gcc go build -ldflags "-extld=${plat_abi}-gcc" github.com/snapcore/snapweb/cmd/snapweb
+ if [ $WITH_WEBCONF ]; then
+ GOARCH=$arch GOARM=7 CGO_ENABLED=1 CC=${plat_abi}-gcc go build -ldflags "-extld=${plat_abi}-gcc" github.com/snapcore/snapweb/cmd/webconf
+ fi
GOARCH=$arch GOARM=7 CGO_ENABLED=1 CC=${plat_abi}-gcc go build -o generate-token -ldflags "-extld=${plat_abi}-gcc" $srcdir/cmd/generate-token/main.go
cp generate-token ../../
cd - > /dev/null
@@ -112,7 +138,7 @@ for ARCH in "${architectures[@]}"; do
snapcraft snap $builddir
# TODO: We only support amd64 ups builds for now -Need a cross-compile fix for "after: [desktop-qt5]"
- if [[ $* == *--ups* ]] && [ $ARCH = amd64 ]; then
+ if [ $BUILD_UPS_SNAP ] && [ $ARCH = amd64 ]; then
HTTP_ADDR="127.0.0.1:5200"
HTTPS_ADDR="127.0.0.1:5201"
diff --git a/cmd/snapweb/cert.go b/cmd/snapweb/cert.go
index a4ababb3..d52b9c34 100644
--- a/cmd/snapweb/cert.go
+++ b/cmd/snapweb/cert.go
@@ -32,8 +32,8 @@ import (
"time"
)
-// DumpCertificate create the certificate & key files
-func DumpCertificate() {
+// CreateCertificateIfNeeded creates the certificate & key files, if they don't exist yet
+func CreateCertificateIfNeeded() {
certFilename := filepath.Join(os.Getenv("SNAP_DATA"), "cert.pem")
keyFilename := filepath.Join(os.Getenv("SNAP_DATA"), "key.pem")
diff --git a/cmd/snapweb/cert_test.go b/cmd/snapweb/cert_test.go
index 56019501..6443c530 100644
--- a/cmd/snapweb/cert_test.go
+++ b/cmd/snapweb/cert_test.go
@@ -45,17 +45,17 @@ func (s *CertSuite) TearDownTest(c *C) {
os.RemoveAll(s.snapdata)
}
-func (s *CertSuite) TestDumpCertificate(c *C) {
+func (s *CertSuite) TestCreateCertificateIfNeeded(c *C) {
c.Assert(ioutil.WriteFile(s.certFilename, nil, 0600), IsNil)
c.Assert(ioutil.WriteFile(s.keyFilename, nil, 0600), IsNil)
- DumpCertificate()
+ CreateCertificateIfNeeded()
certData, err := ioutil.ReadFile(s.certFilename)
c.Assert(err, IsNil)
keyData, err := ioutil.ReadFile(s.keyFilename)
c.Assert(err, IsNil)
- DumpCertificate()
+ CreateCertificateIfNeeded()
certData2, err := ioutil.ReadFile(s.certFilename)
c.Assert(err, IsNil)
keyData2, err := ioutil.ReadFile(s.keyFilename)
diff --git a/cmd/snapweb/handlers.go b/cmd/snapweb/handlers.go
index 23c087c6..3ffe214b 100644
--- a/cmd/snapweb/handlers.go
+++ b/cmd/snapweb/handlers.go
@@ -250,7 +250,7 @@ func initURLHandlers(log *log.Logger, config snappy.Config) http.Handler {
handler.HandleFunc("/", makeMainPageHandler())
- return NewFilterHandlerFromConfig(handler, config)
+ return snappy.NewFilterHandlerFromConfig(handler, config)
}
// Name of the cookie transporting the access token
@@ -341,31 +341,5 @@ func redirHandler(config snappy.Config) http.Handler {
http.StatusSeeOther)
})
- return NewFilterHandlerFromConfig(redir, config)
-}
-
-// NewFilterHandlerFromConfig creates a new http.Handler with an integrated NetFilter
-func NewFilterHandlerFromConfig(handler http.Handler, config snappy.Config) http.Handler {
- if config.DisableIPFilter {
- return handler
- }
-
- f := snappy.NewFilter()
-
- for _, net := range config.AllowNetworks {
- f.AllowNetwork(net)
- }
-
- for _, ifname := range config.AllowInterfaces {
- f.AddLocalNetworkForInterface(ifname)
- }
-
- // if nothing was specified, default to allowing all local networks
- if (len(config.AllowNetworks) == 0) &&
- (len(config.AllowInterfaces) == 0) {
- logger.Println("Allowing local network access by default")
- f.AddLocalNetworks()
- }
-
- return f.FilterHandler(handler)
+ return snappy.NewFilterHandlerFromConfig(redir, config)
}
diff --git a/cmd/snapweb/main.go b/cmd/snapweb/main.go
index 5b8dc1b8..0649a1af 100644
--- a/cmd/snapweb/main.go
+++ b/cmd/snapweb/main.go
@@ -22,6 +22,8 @@ import (
"net/http"
"os"
"path/filepath"
+ "strings"
+ "time"
"github.com/snapcore/snapweb/avahi"
"github.com/snapcore/snapweb/snappy/app"
@@ -43,7 +45,26 @@ func init() {
}
}
+func redir(w http.ResponseWriter, req *http.Request) {
+ http.Redirect(w, req,
+ "https://"+strings.Replace(req.Host, httpAddr, httpsAddr, -1),
+ http.StatusSeeOther)
+}
+
+type blockerFn func() bool
+
+func blockOn(managedCondition blockerFn) {
+ snappy.WritePidFile()
+ for managedCondition() == false {
+ snappy.WaitForSigHup()
+ // wait futher more, to let webconf release the 4200 port
+ time.Sleep(1000)
+ }
+}
+
func main() {
+ blockOn(snappy.IsDeviceManaged)
+
// TODO set warning for too hazardous config?
config, err := snappy.ReadConfig()
if err != nil {
@@ -58,7 +79,7 @@ func main() {
logger.Println("Snapweb starting...")
if !config.DisableHTTPS {
- DumpCertificate()
+ CreateCertificateIfNeeded()
go func() {
certFile := filepath.Join(os.Getenv("SNAP_DATA"), "cert.pem")
diff --git a/cmd/snapweb/main_test.go b/cmd/snapweb/main_test.go
new file mode 100644
index 00000000..eae6f794
--- /dev/null
+++ b/cmd/snapweb/main_test.go
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/snapcore/snapweb/snappy/app"
+
+ . "gopkg.in/check.v1"
+)
+
+type MainSuite struct{}
+
+var _ = Suite(&MainSuite{})
+
+var mockManagedState = false
+
+func mockIsDeviceManaged() bool {
+ return mockManagedState
+}
+
+func (s *MainSuite) TestBlockUntilManaged(c *C) {
+ os.Setenv("SNAP_DATA", c.MkDir())
+
+ ready := make(chan bool)
+ done := make(chan bool)
+
+ go func() {
+ ready <- true
+ blockOn(mockIsDeviceManaged)
+ done <- true
+ }()
+
+ // a signal cannot unblock snapweb until the system is managed
+ mockManagedState = false
+ <-ready
+ time.Sleep(1000) // give time to install the signal handler
+ snappy.SendSignalToSnapweb()
+ select {
+ case <-done:
+ c.Fail()
+ default:
+ }
+
+ // try to unblock once the system has changed
+ mockManagedState = true
+ snappy.SendSignalToSnapweb()
+ result := <-done
+
+ c.Assert(result, Equals, true)
+}
diff --git a/cmd/webconf/main.go b/cmd/webconf/main.go
new file mode 100644
index 00000000..1d9821df
--- /dev/null
+++ b/cmd/webconf/main.go
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main
+
+import (
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "path/filepath"
+ "text/template"
+ "time"
+
+ "github.com/snapcore/snapd/dirs"
+
+ "github.com/snapcore/snapweb/avahi"
+ "github.com/snapcore/snapweb/snappy/app"
+)
+
+var logger *log.Logger
+var timer *time.Timer
+
+const (
+ httpAddr string = ":4200"
+)
+
+func init() {
+ logger = log.New(os.Stderr, "webconf: ", log.Ldate|log.Ltime|log.Lshortfile)
+}
+
+type branding struct {
+ Name string
+ Subname string
+}
+
+type templateData struct {
+ Branding branding
+ SnapdVersion string
+}
+
+func makeMainPageHandler() http.HandlerFunc {
+
+ layoutPath := filepath.Join(os.Getenv("SNAP"), "www", "templates", "webconf.html")
+ t, err := template.ParseFiles(layoutPath)
+ if err != nil {
+ logger.Fatalf("%v", err)
+ }
+
+ data := templateData{
+ Branding: branding{
+ Name: "Ubuntu",
+ Subname: "",
+ },
+ SnapdVersion: "snapd",
+ }
+
+ return func(w http.ResponseWriter, r *http.Request) {
+
+ // reset the timer, and give 3 minutes to complete the setup process
+ timer.Reset(time.Second * 180)
+
+ err = t.Execute(w, &data)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ log.Println(err)
+ return
+ }
+
+ }
+}
+
+func doneHandler(server net.Listener) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ snappy.SendSignalToSnapweb()
+ if server != nil {
+ server.Close()
+ }
+ }
+}
+
+func initURLHandlers(log *log.Logger, server net.Listener, config snappy.Config) http.Handler {
+ handler := http.NewServeMux()
+
+ // API
+ handler.Handle("/api/v2/create-user",
+ snappy.MakePassthroughHandler(dirs.SnapdSocket, "/api/v2/create-user"))
+ handler.HandleFunc("/done", doneHandler(server))
+
+ // Resources
+ handler.Handle("/public/", snappy.LoggingHandler(http.FileServer(http.Dir(filepath.Join(os.Getenv("SNAP"), "www")))))
+
+ handler.HandleFunc("/", makeMainPageHandler())
+
+ return snappy.NewFilterHandlerFromConfig(handler, config)
+}
+
+func main() {
+ if snappy.IsDeviceManaged() {
+ log.Println("webconf does not run on managed devices")
+ // this tells systemd to not restart this service
+ os.Exit(0)
+ }
+
+ config, err := snappy.ReadConfig()
+ if err != nil {
+ logger.Fatal("Configuration error", err)
+ }
+
+ go avahi.InitMDNS(logger)
+
+ // open a plain HTTP end-point on the "usual" 4200 port
+ server, err := net.Listen("tcp", httpAddr)
+ if err != nil {
+ logger.Fatalf("%v", err)
+ }
+
+ handler := initURLHandlers(logger, server, config)
+
+ timer = time.NewTimer(time.Second * 120)
+ go func() {
+ <-timer.C
+ logger.Println("disabling webconf automatically after 2 minutes")
+ server.Close()
+ os.Exit(0)
+ }()
+
+ http.Serve(server, handler)
+
+ // prepare to exit, but let snapweb start before that
+ time.Sleep(2 * time.Second)
+}
diff --git a/cmd/webconf/main_test.go b/cmd/webconf/main_test.go
new file mode 100644
index 00000000..d07b9b4d
--- /dev/null
+++ b/cmd/webconf/main_test.go
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main
+
+import (
+ "log"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapweb/snappy/app"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type WebconfSuite struct{}
+
+var _ = Suite(&WebconfSuite{})
+
+func (s *WebconfSuite) SetUpTest(c *C) {
+ cwd, err := os.Getwd()
+ c.Assert(err, IsNil)
+ os.Setenv("SNAP", filepath.Join(cwd, "..", ".."))
+}
+
+func (s *WebconfSuite) TestURLHandlers(c *C) {
+ timer = time.NewTimer(time.Second * 120)
+ handler := initURLHandlers(log.New(os.Stdout, "", 0), nil, snappy.Config{})
+
+ rec := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ req.RemoteAddr = "127.0.0.1:80"
+ c.Assert(err, IsNil)
+
+ handler.ServeHTTP(rec, req)
+ c.Assert(rec.Code, Equals, http.StatusOK)
+
+ body := rec.Body.String()
+ c.Check(strings.Contains(body, "'Ubuntu'"), Equals, true)
+}
+
+func (s *WebconfSuite) TestDoneHandler(c *C) {
+ tmp := c.MkDir()
+ os.Setenv("SNAP_DATA", tmp)
+ snappy.WritePidFile()
+
+ done := false
+ var sigchan chan os.Signal
+ sigchan = make(chan os.Signal, 1)
+ signal.Notify(sigchan, syscall.SIGHUP)
+
+ server, _ := net.Listen("tcp", httpAddr)
+ handler := doneHandler(server)
+
+ req, _ := http.NewRequest("GET", "/done", nil)
+ req.RemoteAddr = "127.0.0.1:80"
+ rec := httptest.NewRecorder()
+
+ handler.ServeHTTP(rec, req)
+
+ select {
+ case <-sigchan:
+ done = true
+ }
+
+ c.Assert(done, Equals, true)
+}
diff --git a/gulpfile.js b/gulpfile.js
index 453d439a..09a02216 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -18,15 +18,23 @@ var uglify = require('gulp-uglify');
var watchify = require('watchify');
gulp.task('js:build', ['js:clean', 'js:lint'], function() {
- return createBundler();
+ return createBundler('./www/src/js/app.js', 'snapweb.js', 'www/public/js');
});
gulp.task('js:clean', function(cb) {
del(['www/public/js'], cb);
});
-function createBundler(watch) {
- var bundler = browserify('./www/src/js/app.js', {
+gulp.task('js:build:webconf', ['js:clean:webconf', 'js:lint'], function() {
+ return createBundler('./www/src/js/webconf-app.js', 'webconf.js', 'www/public/js');
+});
+
+gulp.task('js:clean:webconf', function(cb) {
+ del(['www/webconf/js'], cb);
+});
+
+function createBundler(appSrc, appJs, destDir, watch) {
+ var bundler = browserify(appSrc, {
cache: {},
packageCache: {}
});
@@ -43,22 +51,22 @@ function createBundler(watch) {
} else {
}
- return bundleShared(bundler);
+ return bundleShared(bundler, appJs, destDir);
}
-function bundleShared(bundler) {
+function bundleShared(bundler, appJs, destDir) {
return bundler.bundle()
.on('error', function(err) {
gutil.log(gutil.colors.green('Browserify Error: ' + err));
this.emit('end');
process.exit(1);
})
- .pipe(source('snapweb.js'))
+ .pipe(source(appJs))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true})) // loads map from browserify file
.pipe(process.env.NODE_ENV === 'development'? gutil.noop() : uglify())
.pipe(sourcemaps.write('./')) // writes .map file
- .pipe(gulp.dest('www/public/js/'));
+ .pipe(gulp.dest(destDir));
}
gulp.task('js:lint', function() {
@@ -133,4 +141,7 @@ gulp.task('install', ['default'], function() {
.pipe(gulp.dest('../install'));
});
-gulp.task('default', ['js:build', 'styles', 'images']);
+gulp.task('default',
+ ['js:build',
+ process.env.WITH_WEBCONF === '1' ? 'js:build:webconf' : 'js:clean:webconf',
+ 'styles', 'images']);
diff --git a/pkg/meta/snap.yaml b/pkg/meta/snap.yaml
index 684dc196..1ec44236 100644
--- a/pkg/meta/snap.yaml
+++ b/pkg/meta/snap.yaml
@@ -16,5 +16,10 @@ apps:
daemon: simple
command: snapweb
plugs: [network, network-bind, snapd-control, timeserver-control, timezone-control]
+ webconf:
+ daemon: simple
+ restart-condition: never
+ command: webconf
+ plugs: [network, network-bind, snapd-control, timeserver-control]
generate-token:
command: generate-token
diff --git a/pkg/snapweb b/pkg/snapweb
index 3b200cdf..c5e6bbec 100755
--- a/pkg/snapweb
+++ b/pkg/snapweb
@@ -2,7 +2,7 @@
set -e
case $SNAP_ARCH in
- amd64)
+ amd64|x86_64)
plat_abi=x86_64-linux-gnu
;;
armhf)
diff --git a/pkg/webconf b/pkg/webconf
new file mode 100755
index 00000000..7f6cdb67
--- /dev/null
+++ b/pkg/webconf
@@ -0,0 +1,26 @@
+#!/bin/sh
+set -e
+
+case $SNAP_ARCH in
+ amd64|x86_64)
+ plat_abi=x86_64-linux-gnu
+ ;;
+ armhf)
+ plat_abi=arm-linux-gnueabihf
+ ;;
+ arm64)
+ plat_abi=aarch64-linux-gnu
+ ;;
+ i386)
+ plat_abi=i686-linux-gnu
+ ;;
+ *)
+ echo "unknown platform for snappy-magic: $SNAP_ARCH. remember to file a bug or better yet: fix it :)"
+ exit 1
+ ;;
+esac
+
+exec $SNAP/bin/$plat_abi/webconf
+
+# never reach this
+exit 1
diff --git a/run-spread-tests.sh b/run-spread-tests.sh
index 78b68718..981d8c3e 100755
--- a/run-spread-tests.sh
+++ b/run-spread-tests.sh
@@ -17,6 +17,7 @@
set -e
image_name=ubuntu-core-16.img
+image_name2=ubuntu-core-16-embryonic.img
channel=candidate
spread_opts=
force_new_image=0
@@ -75,6 +76,12 @@ if [ ! -e $SPREAD_QEMU_PATH/$image_name ] || [ $force_new_image -eq 1 ] ; then
mkdir -p $SPREAD_QEMU_PATH
mv -f spread/image/ubuntu-core-16.img $SPREAD_QEMU_PATH/$image_name
fi
+if [ ! -e $SPREAD_QEMU_PATH/$image_name2 ] || [ $force_new_image -eq 1 ] ; then
+ echo "INFO: Creating new qemu test image(2) ..."
+ (cd spread/image ; sudo ./create-image-embryonic.sh $channel)
+ mkdir -p $SPREAD_QEMU_PATH
+ mv -f spread/image/$image_name2 $SPREAD_QEMU_PATH/$image_name2
+fi
# We currently only run spread tests but we could do other things
# here as well like running our snap-lintian tool etc.
diff --git a/snappy/app/configuration.go b/snappy/app/configuration.go
index 0fd562b4..2a80725e 100644
--- a/snappy/app/configuration.go
+++ b/snappy/app/configuration.go
@@ -36,6 +36,7 @@ type Config struct {
DisableIPFilter bool `json:"disableIPFilter,omitempty"`
AllowNetworks []string `json:"allowNetworks,omitempty"`
AllowInterfaces []string `json:"allowInterfaces,omitempty"`
+ WebconfTimeout []string `json:"webconfTimeout,omitempty"`
}
var readFile = ioutil.ReadFile
diff --git a/snappy/app/fake_snapd_client.go b/snappy/app/fake_snapd_client.go
index 9cf16705..95744d7c 100644
--- a/snappy/app/fake_snapd_client.go
+++ b/snappy/app/fake_snapd_client.go
@@ -158,4 +158,13 @@ func (f *FakeSnapdClient) Abort(id string) (*client.Change, error) {
return nil, nil
}
+// SysInfo returns system information
+func (f *FakeSnapdClient) SysInfo() (*client.SysInfo, error) {
+ managed := &client.SysInfo{
+ OnClassic: true,
+ Managed: true,
+ }
+ return managed, nil
+}
+
var _ snapdclient.SnapdClient = (*FakeSnapdClient)(nil)
diff --git a/snappy/app/helpers.go b/snappy/app/helpers.go
new file mode 100644
index 00000000..741c2e70
--- /dev/null
+++ b/snappy/app/helpers.go
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+/* This module gathers a set of helper functions and structures shared
+ by the various commands in snapweb
+*/
+
+package snappy
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "strings"
+ "syscall"
+
+ "github.com/snapcore/snapd/client"
+)
+
+// IsDeviceManaged determines if the device is in the 'managed' state
+func IsDeviceManaged() bool {
+ client := client.New(nil)
+
+ sysInfo, err := client.SysInfo()
+ if err != nil {
+ return false
+ }
+
+ if sysInfo.OnClassic || sysInfo.Managed {
+ return true
+ }
+
+ return false
+}
+
+func unixDialer(socketPath string) func(string, string) (net.Conn, error) {
+ file, err := os.OpenFile(socketPath, os.O_RDWR, 0666)
+ if err == nil {
+ file.Close()
+ }
+
+ return func(_, _ string) (net.Conn, error) {
+ return net.Dial("unix", socketPath)
+ }
+}
+
+// MakePassthroughHandler maps a snapd API to a snapweb HTTP handler
+func MakePassthroughHandler(socketPath string, prefix string) http.HandlerFunc {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c := &http.Client{
+ Transport: &http.Transport{Dial: unixDialer(socketPath)},
+ }
+
+ log.Println(r.Method, r.URL.Path)
+
+ // need to remove the RequestURI field
+ // and remove the /api prefix from snapweb URLs
+ r.URL.Scheme = "http"
+ r.URL.Host = "localhost"
+ r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
+
+ outreq, err := http.NewRequest(r.Method, r.URL.String(), r.Body)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ resp, err := c.Do(outreq)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // Note: the client.Do method above only returns JSON responses
+ // even if it doesn't say so
+ hdr := w.Header()
+ hdr.Set("Content-Type", "application/json")
+ w.WriteHeader(resp.StatusCode)
+
+ io.Copy(w, resp.Body)
+
+ })
+}
+
+// LoggingHandler adds HTTP logs to a handler
+func LoggingHandler(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ log.Println(r.Method, r.URL.Path)
+ h.ServeHTTP(w, r)
+ })
+}
+
+// WritePidFile writes the PID of the current process to snapweb.pid
+func WritePidFile() {
+ var err error
+
+ pidFilePath := filepath.Join(os.Getenv("SNAP_DATA"), "snapweb.pid")
+
+ if f, err := os.OpenFile(pidFilePath, os.O_CREATE|os.O_RDWR, os.ModeTemporary|0640); err == nil {
+ fmt.Fprintf(f, "%d\n", os.Getpid())
+ }
+ if err != nil {
+ log.Println(err)
+ }
+
+}
+
+// WaitForSigHup waits for the reception of the SIGHUP signal
+func WaitForSigHup() {
+ var sigchan chan os.Signal
+ sigchan = make(chan os.Signal, 1)
+ signal.Notify(sigchan, syscall.SIGHUP)
+ defer signal.Stop(sigchan)
+ <-sigchan
+}
+
+// SendSignalToSnapweb informs snapweb that the webconf process is finished, via a SIGHUP signal
+func SendSignalToSnapweb() {
+ var pid int
+
+ pidFilePath := filepath.Join(os.Getenv("SNAP_DATA"), "snapweb.pid")
+
+ if f, err := os.Open(pidFilePath); err == nil {
+ if _, err = fmt.Fscanf(f, "%d\n", &pid); err == nil {
+ p, _ := os.FindProcess(pid)
+ err = p.Signal(syscall.Signal(syscall.SIGHUP))
+ } else {
+ log.Println(err)
+ }
+ } else {
+ log.Println(err)
+ }
+}
diff --git a/snappy/app/helpers_test.go b/snappy/app/helpers_test.go
new file mode 100644
index 00000000..b01cc2a2
--- /dev/null
+++ b/snappy/app/helpers_test.go
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package snappy
+
+import (
+ "bytes"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ . "gopkg.in/check.v1"
+)
+
+type HelpersSuite struct{}
+
+var _ = Suite(&HelpersSuite{})
+
+func (s *HelpersSuite) TestLoggingHandler(c *C) {
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+ logged := LoggingHandler(handler)
+
+ var output bytes.Buffer
+ log.SetOutput(&output)
+ defer func() {
+ log.SetOutput(os.Stdout)
+ }()
+
+ rec := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foo", nil)
+ c.Assert(err, IsNil)
+
+ logged.ServeHTTP(rec, req)
+
+ c.Assert(output.String(), Matches, ".*GET /foo\n")
+}
+
+func (s *HelpersSuite) TestPassthroughHandler(c *C) {
+ socketPath := "/tmp/snapd-test.socket"
+ c.Assert(os.MkdirAll(filepath.Dir(socketPath), 0755), IsNil)
+ l, err := net.Listen("unix", socketPath)
+ if err != nil {
+ c.Fatalf("unable to listen on %q: %v", socketPath, err)
+ }
+
+ f := func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.URL.Path, Equals, "/v2/system-info")
+ c.Check(r.URL.RawQuery, Equals, "")
+
+ fmt.Fprintln(w, `{"type":"sync", "result":{"series":"42"}}`)
+ }
+
+ srv := &httptest.Server{
+ Listener: l,
+ Config: &http.Server{Handler: http.HandlerFunc(f)},
+ }
+ srv.Start()
+ defer srv.Close()
+
+ handler := http.HandlerFunc(MakePassthroughHandler(socketPath, "/api"))
+
+ rec := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/api/v2/system-info", nil)
+ c.Assert(err, IsNil)
+
+ // req.AddCookie(&http.Cookie{Name: SnapwebCookieName, Value: "1234"})
+
+ handler(rec, req)
+ body := rec.Body.String()
+ c.Assert(rec.Code, Equals, http.StatusOK)
+ c.Check(strings.Contains(body, "42"), Equals, true)
+ // TODO: check that we receive Content-Type: json/application
+}
+
+func (s *HelpersSuite) TestSnapwebSignaling(c *C) {
+ os.Setenv("SNAP_DATA", c.MkDir())
+
+ WritePidFile()
+
+ ready := make(chan bool)
+ done := make(chan bool)
+
+ // the thread where we test the function
+ go func() {
+ for {
+ ready <- true
+ WaitForSigHup()
+ // say the test passed
+ done <- true
+ }
+ }()
+
+ // send the signal, *once* the function is ready to be tested
+ <-ready
+ time.Sleep(1000)
+ SendSignalToSnapweb()
+
+ c.Assert(<-done, Equals, true)
+
+ // do it a second time
+ <-ready
+ time.Sleep(1000)
+ SendSignalToSnapweb()
+
+ c.Assert(<-done, Equals, true)
+}
diff --git a/snappy/app/netfilter.go b/snappy/app/netfilter.go
index c551948f..e7f46290 100644
--- a/snappy/app/netfilter.go
+++ b/snappy/app/netfilter.go
@@ -132,3 +132,29 @@ func (f *NetFilter) FilterHandler(handler http.Handler) http.Handler {
})
}
+
+// NewFilterHandlerFromConfig creates a new http.Handler with an integrated NetFilter
+func NewFilterHandlerFromConfig(handler http.Handler, config Config) http.Handler {
+ if config.DisableIPFilter {
+ return handler
+ }
+
+ f := NewFilter()
+
+ for _, net := range config.AllowNetworks {
+ f.AllowNetwork(net)
+ }
+
+ for _, ifname := range config.AllowInterfaces {
+ f.AddLocalNetworkForInterface(ifname)
+ }
+
+ // if nothing was specified, default to allowing all local networks
+ if (len(config.AllowNetworks) == 0) &&
+ (len(config.AllowInterfaces) == 0) {
+ log.Println("Allowing local network access by default")
+ f.AddLocalNetworks()
+ }
+
+ return f.FilterHandler(handler)
+}
diff --git a/spread.yaml b/spread.yaml
index 9cfca85f..86193742 100644
--- a/spread.yaml
+++ b/spread.yaml
@@ -30,6 +30,9 @@ backends:
- ubuntu-core-16:
username: test
password: test
+ - ubuntu-core-16-embryonic:
+ # root user access; not a managed system yet
+ password: test
# Put this somewhere where we have read-write access
path: /home/snapweb
@@ -41,6 +44,7 @@ exclude:
- key.pem
- node_modules
- snapweb
+ - web-conf
- tests
- releases
- generate_token
@@ -58,3 +62,13 @@ suites:
. $TESTSLIB/prepare.sh
restore-each: |
. $TESTSLIB/restore-each.sh
+ spread/webconf/:
+ summary: Full-system tests for the webconf variant of snapweb
+ environment:
+ FIRSTBOOT: yes
+ systems:
+ - ubuntu-core-16-embryonic
+ prepare:
+ . $TESTSLIB/prepare.sh
+ restore-each: |
+ . $TESTSLIB/restore-each.sh
diff --git a/spread/image/create-image-embryonic.sh b/spread/image/create-image-embryonic.sh
new file mode 100755
index 00000000..b68ec05a
--- /dev/null
+++ b/spread/image/create-image-embryonic.sh
@@ -0,0 +1,132 @@
+#!/bin/bash
+#
+# Copyright (C) 2017 Canonical Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+set -e
+
+if [ $(id -u) -ne 0 ] ; then
+ echo "ERROR: needs to be executed as root"
+ exit 1
+fi
+
+channel=candidate
+if [ ! -z "$1" ] ; then
+ channel=$1
+fi
+
+snap=
+if [ ! -z "$2" ] ; then
+ snap=$2
+fi
+
+model=pc
+arch=amd64
+image_name=ubuntu-core-16-embryonic.img
+ubuntu_image_extra_args=
+
+if [ ! -z "$snap" ] ; then
+ ubuntu_image_extra_args="--extra-snaps $snap"
+fi
+
+ubuntu-image \
+ --channel $channel \
+ -o $image_name \
+ --image-size 4G \
+ $ubuntu_image_extra_args \
+ $model.model
+
+kpartx -a $image_name
+sleep 0.5
+
+loop_path=`findfs LABEL=writable`
+tmp_mount=`mktemp -d`
+
+mount $loop_path $tmp_mount
+
+core_snap=$(find $tmp_mount/system-data/var/lib/snapd/snaps -name "core_*.snap")
+tmp_core=`mktemp -d`
+mount $core_snap $tmp_core
+# copy over all systemd units
+mkdir -p $tmp_mount/system-data/etc/systemd
+cp -rav $tmp_core/etc/systemd/* \
+ $tmp_mount/system-data/etc/systemd/
+# copy some more etc files into the writable area
+mkdir -p $tmp_mount/system-data/etc/ssh
+cp -av $tmp_core/etc/passwd $tmp_mount/system-data/etc/
+cp -av $tmp_core/etc/shadow $tmp_mount/system-data/etc/
+cp -av $tmp_core/etc/ssh/sshd_config $tmp_mount/system-data/etc/ssh
+# mkdir -p $tmp_mount/system-data/var/lib/system-image
+# cp -av $tmp_core/etc/system-image/writable-paths $tmp_mount/system-data/var/lib/system-image
+umount $tmp_core
+rm -rf $tmp_core
+
+# allow root user to ssh into the system without password
+# test_pass=`python -c 'import crypt; print crypt.crypt("test","Fx")'`
+# ie, FxhZ/XVdPJNZE
+sed -i 's/root:x:/root:FxhZ\/XVdPJNZE:/' $tmp_mount/system-data/etc/passwd
+sed -i 's/root:x:/root:\!:/' $tmp_mount/system-data/etc/shadow
+sed -i 's/\(PermitRootLogin\)\>.*/\1 yes/' $tmp_mount/system-data/etc/ssh/sshd_config
+# sed -i 's/\(PermitEmptyPasswords\)\>.*/\1 yes/' $tmp_mount/system-data/etc/ssh/sshd_config
+
+if false; then
+ mkdir -p $tmp_mount/system-data/var/lib/extrausers
+ echo 'test:FxhZ/XVdPJNZE:800:800:spread test:/root:/bin/bash' > $tmp_mount/system-data/var/lib/extrausers/passwd
+ echo 'test:!:16891:0:99999:7:::' > $tmp_mount/system-data/var/lib/extrausers/shadow
+ echo 'adm:x:4:syslog,test' > $tmp_mount/system-data/var/lib/extrausers/group
+ echo 'test:x:800:' >> $tmp_mount/system-data/var/lib/extrausers/group
+fi
+
+# Create systemd service to run on firstboot and install our passwd/group overrides
+if true; then
+ mkdir -p $tmp_mount/system-data/etc/systemd/system
+ cat << 'EOF' > $tmp_mount/system-data/etc/systemd/system/spread-access.service
+[Unit]
+Description=Configure spread access
+After=networking.service
+
+[Service]
+Type=oneshot
+ExecStart=/writable/system-data/var/lib/spread-access/run.sh
+RemainAfterExit=no
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+ mkdir $tmp_mount/system-data/var/lib/spread-access
+ cat << 'EOF' > $tmp_mount/system-data/var/lib/spread-access/run.sh
+#!/bin/bash
+
+set -e
+
+echo "Start spread-access $(date -Iseconds --utc)"
+
+# mount our special passwd/shadow files to permit spread access as root
+mount --bind /writable/system-data/etc/passwd /etc/passwd
+mount --bind /writable/system-data/etc/shadow /etc/shadow
+
+EOF
+
+ chmod +x $tmp_mount/system-data/var/lib/spread-access/run.sh
+
+ # install the new service
+ mkdir -p $tmp_mount/system-data/etc/systemd/system/multi-user.target.wants
+ ln -sf /etc/systemd/system/spread-access.service \
+ $tmp_mount/system-data/etc/systemd/system/multi-user.target.wants/spread-access.service
+fi
+
+umount $tmp_mount
+kpartx -d $image_name
+rm -rf $tmp_mount
diff --git a/spread/lib/restore-each.sh b/spread/lib/restore-each.sh
index 94030e1c..80b7c556 100644
--- a/spread/lib/restore-each.sh
+++ b/spread/lib/restore-each.sh
@@ -15,21 +15,23 @@ for snap in /snap/*; do
esac
done
+# Depending on what the test did both services are not meant to be
+# running here.
+systemctl stop snap.snapweb.snapweb.service || true
+
# Cleanup all configuration files from the snap so that we have
# a fresh start for the next test
rm -rf /var/snap/$SNAP_NAME/common/*
rm -rf /var/snap/$SNAP_NAME/current/*
-# Depending on what the test did both services are not meant to be
-# running here.
-systemctl stop snap.snapweb.snapweb.service || true
-
# Ensure we have the same state for snapd as we had before
-systemctl stop snapd.service snapd.socket
-rm -rf /var/lib/snapd/*
-tar xzf $SPREAD_PATH/snapd-state.tar.gz -C /
-rm -rf /root/.snap
-systemctl start snapd.service snapd.socket
+if [ -f $SPREAD_PATH/snapd-state.tar.gz ]; then
+ systemctl stop snapd.service snapd.socket
+ rm -rf /var/lib/snapd/*
+ tar xzf $SPREAD_PATH/snapd-state.tar.gz -C /
+ rm -rf /root/.snap
+ systemctl start snapd.service snapd.socket
+fi
# Start services again now that the system is restored
systemctl start snap.snapweb.snapweb.service
diff --git a/spread/main/netfilter/task.yaml b/spread/main/netfilter/task.yaml
new file mode 100644
index 00000000..4d78d9ae
--- /dev/null
+++ b/spread/main/netfilter/task.yaml
@@ -0,0 +1,25 @@
+summary: Control that remote IP connections are not accepted
+execute: |
+ echo "setting up test interface"
+ modprobe dummy
+ sleep 1
+
+ echo "add a network filter to the configuration"
+ ip link set name eth10 dev dummy0
+ sleep 1
+ ip address add 192.168.100.199/24 dev eth10
+ ip link set eth10 up
+
+ echo "Verifying that a remote request is not accepted"
+ echo "{ \"allowNetworks\": [\"127.0.0.1/24\"] }" > /var/snap/snapweb/common/settings.json
+ systemctl restart snap.snapweb.snapweb.service
+ sleep 1
+ RES=`printf "GET / HTTP/1.1\nHost:localhost\nUser-Agent:nc\n\n" | nc -s 192.168.100.199 127.0.0.1 4200 | grep "HTTP/1.1 403 Forbidden"`
+ test -n "$RES"
+
+ echo "cleanup"
+ ip address del 192.168.100.199/24 dev eth10
+ ip link set eth10 down
+ rm /var/snap/snapweb/common/settings.json
+ systemctl restart snap.snapweb.snapweb.service
+
\ No newline at end of file
diff --git a/spread/main/webconf-not/task.yaml b/spread/main/webconf-not/task.yaml
new file mode 100644
index 00000000..5fa8f33a
--- /dev/null
+++ b/spread/main/webconf-not/task.yaml
@@ -0,0 +1,22 @@
+summary: Verify that firstboot won't run on a managed system
+systems: [-ubuntu-core-16-64, -ubuntu-core-16-arm-64, -ubuntu-core-16-arm-32]
+environment:
+ SEED_DIR: /var/lib/snapd/seed
+prepare: |
+ systemctl stop snapd.service
+ rm -f /var/lib/snapd/state.json
+restore: |
+ systemctl start snapd.service
+execute: |
+ echo "Start the daemon with an empty state, this will make it import "
+ echo "assertions from the $SEED_DIR/assertions subdirectory."
+ systemctl start snapd.service
+
+ echo "The system should be managed already"
+ test `snap managed`
+
+ echo "Verifying that webconf is NOT running"
+ test -z "`ps h -C webconf`"
+
+ echo "Verifying that snapweb is running as normal"
+ test -n "`ps h -C snapweb`"
diff --git a/spread/webconf/netfilter/task.yaml b/spread/webconf/netfilter/task.yaml
new file mode 100644
index 00000000..0a2e438e
--- /dev/null
+++ b/spread/webconf/netfilter/task.yaml
@@ -0,0 +1,25 @@
+summary: Control that remote IP connections are not accepted
+execute: |
+ echo "setting up test interface"
+ modprobe dummy
+ sleep 1
+
+ echo "add a network filter to the configuration"
+ ip link set name eth10 dev dummy0
+ sleep 1
+ ip address add 192.168.100.199/24 dev eth10
+ ip link set eth10 up
+
+ echo "Verifying that a remote request is not accepted"
+ echo "{ \"allowNetworks\": [\"127.0.0.1/24\"] }" > /var/snap/snapweb/common/settings.json
+ systemctl restart snap.snapweb.webconf.service
+ sleep 1
+ RES=`printf "GET / HTTP/1.1\nHost:localhost\nUser-Agent:nc\n\n" | nc -s 192.168.100.199 127.0.0.1 4200 | grep "HTTP/1.1 403 Forbidden"`
+ test -n "$RES"
+
+ echo "cleanup"
+ ip address del 192.168.100.199/24 dev eth10
+ ip link set eth10 down
+ rm /var/snap/snapweb/common/settings.json
+ systemctl restart snap.snapweb.webconf.service
+
\ No newline at end of file
diff --git a/spread/webconf/transition-to-snapweb/task.yaml b/spread/webconf/transition-to-snapweb/task.yaml
new file mode 100644
index 00000000..f65ecdec
--- /dev/null
+++ b/spread/webconf/transition-to-snapweb/task.yaml
@@ -0,0 +1,20 @@
+summary: Verify that webconf will transition to snapweb once done
+execute: |
+ echo "The system should not be managed yet"
+ test `snap managed` = 'false'
+
+ echo "Finish the configuration"
+ snap create-user david.barth@canonical.com
+ printf "GET /done HTTP/1.1\nHost:localhost\nUser-Agent:nc\n\n" | nc localhost 4200
+
+ # wait a bit
+ sleep 2
+
+ echo "Verifying that snapweb now serves on port 4201"
+ printf "GET / HTTP/1.1\nHost:localhost\nUser-Agent:nc\n\n" | nc localhost 4201
+
+ echo "The system should be managed now"
+ test `snap managed` = 'true'
+
+ echo "And webconf does not run anymore"
+ test -z "`ps h -C webconf`"
diff --git a/spread/webconf/webconf-runs/task.yaml b/spread/webconf/webconf-runs/task.yaml
new file mode 100644
index 00000000..8be1261c
--- /dev/null
+++ b/spread/webconf/webconf-runs/task.yaml
@@ -0,0 +1,11 @@
+summary: Verify that webconf will start on an un-managed system by default
+execute: |
+ echo "The system should not be managed yet"
+ test `snap managed` = 'false'
+
+ echo "Verifying that webconf is running"
+ test -n "`ps h -C webconf`"
+
+ echo "Verifying that webconf is controlling port 4200"
+ printf "GET / HTTP/1.1\nHost:localhost\nUser-Agent:nc\n\n" | nc localhost 4200 | grep "script src" | grep webconf.js
+
diff --git a/tests/kvm-exec.sh b/tests/kvm-exec.sh
index 4c111f90..635c4836 100755
--- a/tests/kvm-exec.sh
+++ b/tests/kvm-exec.sh
@@ -34,7 +34,7 @@ start_vm() {
qemu-system-x86_64 \
-enable-kvm -snapshot \
-m 500 \
- -net nic -net user,hostfwd=tcp::$PORT_SSH-:22,hostfwd=tcp::4201-:4201 \
+ -net nic -net user,hostfwd=tcp::$PORT_SSH-:22,hostfwd=tcp::4200-:4200,hostfwd=tcp::4201-:4201 \
-drive file=$IMAGE_BOOTABLE,format=raw \
-pidfile $FILE_PID \
-monitor unix:$FILE_MONITOR,server,nowait \
diff --git a/tests/remote-install-snap.sh b/tests/remote-install-snap.sh
index 4a81da5b..8f132b7f 100755
--- a/tests/remote-install-snap.sh
+++ b/tests/remote-install-snap.sh
@@ -11,7 +11,9 @@ fi
snap_name="${snap##*/}"
-SSH_OPTS="-o StrictHostKeyChecking=no"
+# ssh-keygen -f "~/.ssh/known_hosts" -R [localhost]:8022
+
+SSH_OPTS="-o StrictHostKeyChecking=no -o PreferredAuthentications=\"password\""
SSH_OPTS="$SSH_OPTS -p $port $user@$host"
ssh ${SSH_OPTS} "if [ -d tmpsnaps ]; then rm -rf tmpsnaps; fi; mkdir tmpsnaps;"
diff --git a/www/src/css/styles.scss b/www/src/css/styles.scss
index 504ea036..d1caddd8 100644
--- a/www/src/css/styles.scss
+++ b/www/src/css/styles.scss
@@ -67,3 +67,19 @@ hr {
transform: rotate(360deg);
}
}
+// webconf
+.region-webconf .inner-wrapper {
+ background-color: $color-light;
+}
+.region-webconf .p-card--highlighted {
+ margin-bottom: 1em;
+}
+
+.p-navigation {
+ margin-bottom: 0em;
+}
+
+label {
+ margin-bottom: 0.5em;
+ display: inline-block;
+}
diff --git a/www/src/js/controllers/init.js b/www/src/js/controllers/webconf.js
similarity index 89%
rename from www/src/js/controllers/init.js
rename to www/src/js/controllers/webconf.js
index 113a8d97..02726dde 100644
--- a/www/src/js/controllers/init.js
+++ b/www/src/js/controllers/webconf.js
@@ -3,7 +3,7 @@ var Backbone = require('backbone');
Backbone.$ = $;
var Marionette = require('backbone.marionette');
var Radio = require('backbone.radio');
-var InitLayoutView = require('../views/init.js');
+var InitLayoutView = require('../views/webconf.js');
var CreateUserModel = require('../models/create-user.js');
module.exports = {
diff --git a/www/src/js/models/create-user.js b/www/src/js/models/create-user.js
index bd2623ae..358faa87 100644
--- a/www/src/js/models/create-user.js
+++ b/www/src/js/models/create-user.js
@@ -2,10 +2,9 @@
var Backbone = require('backbone');
var Marionette = require('backbone.marionette');
-var CONFIG = require('../config.js');
module.exports = Backbone.Model.extend({
- url: CONFIG.CREATE_USER,
+ url: '/api/v2/create-user',
// forces POST requests on every model update
isNew: function() {
@@ -21,5 +20,5 @@ module.exports = Backbone.Model.extend({
return 'Invalid email';
}
},
-
+
});
diff --git a/www/src/js/routers/router.js b/www/src/js/routers/router.js
index cb05d7df..51f393cf 100644
--- a/www/src/js/routers/router.js
+++ b/www/src/js/routers/router.js
@@ -4,7 +4,6 @@ var Backbone = require('backbone');
var Marionette = require('backbone.marionette');
var homeController = require('../controllers/home.js');
-// var initController = require('../controllers/init.js');
var searchController = require('../controllers/search.js');
var storeController = require('../controllers/store.js');
var settingsController = require('../controllers/settings.js');
diff --git a/www/src/js/routers/webconf-router.js b/www/src/js/routers/webconf-router.js
new file mode 100644
index 00000000..e68f9bf8
--- /dev/null
+++ b/www/src/js/routers/webconf-router.js
@@ -0,0 +1,17 @@
+// webconf-router.js
+
+var Backbone = require('backbone');
+var Marionette = require('backbone.marionette');
+
+var webconfController = require('../controllers/webconf.js');
+
+module.exports = {
+
+ home: new Marionette.AppRouter({
+ controller: webconfController,
+ appRoutes: {
+ '': 'index'
+ }
+ }),
+
+};
diff --git a/www/src/js/templates/webconf-layout.hbs b/www/src/js/templates/webconf-layout.hbs
new file mode 100644
index 00000000..a778096b
--- /dev/null
+++ b/www/src/js/templates/webconf-layout.hbs
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/www/src/js/templates/first-boot.hbs b/www/src/js/templates/webconf.hbs
similarity index 70%
rename from www/src/js/templates/first-boot.hbs
rename to www/src/js/templates/webconf.hbs
index bae50ab7..ac84342d 100644
--- a/www/src/js/templates/first-boot.hbs
+++ b/www/src/js/templates/webconf.hbs
@@ -1,23 +1,16 @@
-
+
-
Ubuntu Core
-
-
- Setup an administrator account on this all-snap Ubuntu Core system. After this setup process you will have secure web or command access to the system.
-
+
Ubuntu Core
+
+ Setup an administrator account on this all-snap Ubuntu Core system. After this setup process you will have secure web or command access to the system.
+
+
+
@@ -56,7 +48,7 @@
ssh {{username}}@{{ipaddress}}
-
+
Manage your device
diff --git a/www/src/js/views/init.js b/www/src/js/views/create-user.js
similarity index 89%
rename from www/src/js/views/init.js
rename to www/src/js/views/create-user.js
index ac0bf3d3..2afbce55 100644
--- a/www/src/js/views/init.js
+++ b/www/src/js/views/create-user.js
@@ -9,8 +9,8 @@ module.exports = Backbone.Marionette.LayoutView.extend({
className: 'b-layout__container',
ui: {
- statusmessage: '.statusmessage',
- btncreate: '.btn-create',
+ statusmessage: ".statusmessage",
+ btncreate: ".btn-create",
},
events: {
@@ -30,8 +30,8 @@ module.exports = Backbone.Marionette.LayoutView.extend({
this.ui.statusmessage.show();
},
'success': function(model, response) {
- this.model.set({ipaddress: location.hostname});
- this.model.set({username: response.result.username});
+ this.model.set({ ipaddress: location.hostname });
+ this.model.set({ username: response.result.username });
this.$('#firstboot-step-1').hide();
this.$('#firstboot-step-2').show();
},
diff --git a/www/src/js/views/webconf-layout.js b/www/src/js/views/webconf-layout.js
new file mode 100644
index 00000000..03723868
--- /dev/null
+++ b/www/src/js/views/webconf-layout.js
@@ -0,0 +1,53 @@
+// webconf layout view
+var $ = require('jquery');
+var _ = require('lodash');
+var Backbone = require('backbone');
+Backbone.$ = $;
+var Marionette = require('backbone.marionette');
+var React = require('react');
+var ReactDOM = require('react-dom');
+var Radio = require('backbone.radio');
+var FooterView = require('./layout-footer.js');
+var NotificationsView = require('./alerts.js');
+var template = require('../templates/webconf-layout.hbs');
+var chan = Radio.channel('root');
+
+module.exports = Marionette.LayoutView.extend({
+
+ initialize: function() {
+ chan.comply('set:content', this.setContent, this);
+ chan.comply('alert:error', this.alertError, this);
+ },
+
+ el: '.b-layout',
+
+ template : function() {
+ return template();
+ },
+
+ onRender: function() {
+ this.showChildView('footerRegion', new FooterView());
+ },
+
+ setContent: function(content) {
+ var reactElement = content.reactElement || null;
+ if (reactElement !== null) {
+ ReactDOM.render(reactElement, this.$('.b-layout__main').get(0));
+ } else {
+ this.mainRegion.show(content.backboneView);
+ }
+ },
+
+ alertError: function(model) {
+ this.showChildView('alertsRegion', new NotificationsView({
+ model: model
+ }));
+ },
+
+ regions: {
+ bannerRegion: '.b-layout__banner',
+ mainRegion: '.b-layout__main',
+ footerRegion: '.b-layout__footer',
+ alertsRegion: '.b-layout__alerts'
+ }
+});
diff --git a/www/src/js/views/webconf.js b/www/src/js/views/webconf.js
new file mode 100644
index 00000000..6dcfa644
--- /dev/null
+++ b/www/src/js/views/webconf.js
@@ -0,0 +1,73 @@
+var Backbone = require('backbone');
+var Marionette = require('backbone.marionette');
+var template = require('../templates/webconf.hbs');
+
+module.exports = Backbone.Marionette.LayoutView.extend({
+
+ className: 'b-layout__container',
+
+ ui: {
+ statusmessage: '.statusmessage',
+ btncreate: '.btn-create',
+ },
+
+ events: {
+ 'click #btn-create': 'handleCreate',
+ },
+
+ modelEvents: {
+ 'status-update': function(msg) {
+ this.ui.statusmessage.html(msg);
+ this.ui.statusmessage.removeClass('has-error');
+ this.ui.statusmessage.removeClass('has-warning');
+ this.ui.statusmessage.show();
+ },
+ 'invalid': function(model, error) {
+ this.ui.statusmessage.text(error);
+ this.ui.statusmessage.addClass('has-error');
+ this.ui.statusmessage.show();
+ },
+ 'success': function(model, response) {
+ this.model.set({ipaddress: location.hostname});
+ this.model.set({username: response.result.username});
+ this.$('#firstboot-step-1').hide();
+ this.$('#firstboot-step-2').show();
+ },
+ 'change': function() {
+ this.render();
+ },
+ },
+
+ handleCreate: function(event) {
+ event.preventDefault();
+ this.model.set({
+ email: this.$('#emailSSO').val(),
+ sudoer: true,
+ });
+ if (this.model.isValid()) {
+ this.model.trigger('status-update', 'Contacting store...'); // via snapd...
+ this.model.save({}, {
+ success: function(model, response) {
+ model.trigger('success', model, response);
+ },
+ error: function(model, response) {
+ var message = "";
+ var resp = eval(response);
+ if (resp && resp.responseJSON &&
+ resp.responseJSON.result) {
+ message = resp.responseJSON.result.message;
+ } else {
+ message = response.responseText;
+ }
+
+ model.trigger('invalid', model, message);
+ }
+ });
+ }
+ },
+
+ template : function(model) {
+ return template(model);
+ },
+
+});
diff --git a/www/src/js/webconf-app.js b/www/src/js/webconf-app.js
new file mode 100644
index 00000000..c1a343a9
--- /dev/null
+++ b/www/src/js/webconf-app.js
@@ -0,0 +1,26 @@
+// webconf-app.js
+'use strict';
+
+var $ = require('jquery');
+var Backbone = require('backbone');
+Backbone.$ = $;
+var Marionette = require('backbone.marionette');
+var Radio = require('backbone.radio');
+
+if (window.__agent) {
+ window.__agent.start(Backbone, Marionette);
+}
+var LayoutView = require('./views/webconf-layout.js');
+var router = require('./routers/webconf-router.js');
+
+var webconf = new Marionette.Application();
+var layout = new LayoutView();
+layout.render();
+
+$(document).ready(function() {
+ webconf.start();
+});
+
+webconf.on('start', function() {
+ Backbone.history.start({pushState: true});
+});
diff --git a/www/templates/webconf.html b/www/templates/webconf.html
new file mode 100644
index 00000000..60675f2d
--- /dev/null
+++ b/www/templates/webconf.html
@@ -0,0 +1,24 @@
+
+
+
+
web-conf
+
+
+
+
+
+
+
+
+
+
+
diff --git a/www/tests/firstBootSpec.js b/www/tests/webconfSpec.js
similarity index 77%
rename from www/tests/firstBootSpec.js
rename to www/tests/webconfSpec.js
index eed45c3a..bb653b8f 100644
--- a/www/tests/firstBootSpec.js
+++ b/www/tests/webconfSpec.js
@@ -1,33 +1,27 @@
var _ = require('lodash');
var CreateUser = require('../src/js/models/create-user.js');
var Backbone = require('backbone');
-var InitView = require('../src/js/views/init.js');
-var CONF = require('../src/js/config.js');
+var MainView = require('../src/js/views/webconf.js');
-describe('FirstBoot', function() {
+describe('WebConf', function() {
describe('create-user model', function() {
beforeEach(function() {
- jasmine.Ajax.install();
this.model = new CreateUser({});
spyOn(this.model, 'save').and.callThrough();
spyOn(this.model, 'validate').and.callThrough();
});
afterEach(function() {
- jasmine.Ajax.uninstall();
- delete this.model;
});
it('should be an instance of Backbone.Model', function() {
- expect(CreateUser).toBeDefined();
expect(this.model).toEqual(jasmine.any(Backbone.Model));
});
- it('should have urlRoot prop from config', function() {
- var expectedPath = CONF.CREATE_USER;
- expect(this.model.url).toBe(CONF.CREATE_USER);
+ it('should have urlRoot prop', function() {
+ expect(this.model.url).toBe('/api/v2/create-user');
});
it('should block empty or invalid email', function() {
@@ -44,11 +38,11 @@ describe('FirstBoot', function() {
});
});
- describe('InitView', function() {
+ describe('MainView', function() {
beforeEach(function() {
this.model = new CreateUser({});
- this.view = new InitView({
+ this.view = new MainView({
model: this.model
});
this.view.render();
@@ -65,7 +59,7 @@ describe('FirstBoot', function() {
});
it('should be an instance of Backbone.View', function() {
- expect(InitView).toBeDefined();
+ expect(MainView).toBeDefined();
expect(this.view).toEqual(jasmine.any(Backbone.View));
});