text:
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
#   name = "github.com/user/project"
#   version = "1.0.0"
#
# [[constraint]]
#   name = "github.com/user/project2"
#   branch = "dev"
#   source = "github.com/myfork/project2"
#
# [[override]]
#  name = "github.com/x/y"
#  version = "2.4.0"

[[constraint]]
  name = "github.com/digitalocean/godo"
  version = "1.1.1"

[[constraint]]
  name = "github.com/spf13/cobra"
  version = "0.0.2"

[[constraint]]
  branch = "master"
  name = "golang.org/x/crypto"

[[constraint]]
  branch = "master"
  name = "golang.org/x/oauth2"


text:
This repository is no longer maintained.

dosxvpn

Easily deploy your own personal VPN server with DNS adblocking running on DigitalOcean.

---

![](/static/images/overview.gif?raw=true)

## Key Features
* Personal IPsec-based VPN ([strongSwan](https://strongswan.org/)).
* Ad blocking DNS setup by default ([Pi-hole](https://pi-hole.net/)).
* Generates profiles for sharing VPN with OSX/iPhone and Android.
* No additional software required for OSX/iPhone - uses native VPN client.
* Simple Web or CLI installation methods.
* Automated OS and VPN software updates.
* Dual stack IPv4/IPv6 enabled.

## Web Installer (OSX)
1. Download the latest pre-built app from the [GitHub Releases](https://github.com/dan-v/dosxvpn/releases) page. Note: only OSX 10.12+ is supported.
2. Open the app and run through the web based installation wizard to setup a new VPN.

## CLI Usage (OSX)
1. Download the latest pre-built cli from the [GitHub Releases](https://github.com/dan-v/dosxvpn/releases) page. Note: only OSX 10.12+ is supported.
2. Make the binary executable
  ```sh
  chmod +x dosxvpn
  ```
3. Create an API token (https://cloud.digitalocean.com/settings/api/tokens) and export it
  ```sh
  export DIGITALOCEAN_ACCESS_TOKEN=<token>
  ```
4. See help for all options
  ```sh
  ./dosxvpn -h
  ```

### CLI Examples
* Deploy a new VPN droplet and configure OSX VPN
  ```sh
  ./dosxvpn deploy --region sfo2 --auto-configure
  ```
* List dosxvpn VPN droplets
  ```sh
  ./dosxvpn ls
  ```
* Remove dosxvpn VPN droplet and OSX VPN profile
  ```sh
  ./dosxvpn rm --name <name> --remove-profile
  ```

## FAQ
1. Should I use dosxvpn? That's up to you. Use at your own risk.
2. Why is this better than using public VPN provider XYZ? While most VPN providers will provide a secure connection to their endpoints, you may not be interested in putting blind faith in their claims that they will not log or track your activity online.
3. How is this different than [algo](https://github.com/trailofbits/algo)?
   1) Installation - is simple and has no additional system dependencies.
   2) Updates: dosxvpn handles updates of both the OS and VPN. This means any critical security updates or bug fixes will automatically be applied for you.
4. How much does this cost? This launches a 512MB DigitalOcean droplet that costs $5/month currently.
5. What is the bandwidth limit? The 512MB DigitalOcean droplet has a 1TB bandwidth limit. This does not appear to be strictly enforced.
6. Where does dosxvpn store VPN configuration files? You can find all deployed VPN configuration files in your ~/.dosxvpn directory.
7. How do I SSH into the deployed droplet? Assuming you had public SSH keys uploaded to your DigitalOcean account when the VPN was deployed, all of those keys should be authorized for access. You can SSH using any of those keys: `ssh -i <ssh-private-key> core@<vpn-ip>`. If you had no SSH keys uploaded to your DigitalOcean account, then a temporary key was autogenerated for you and you will need to redeploy if you want SSH access.
8. What is the password to login to Pi-hole? The password is `dosxvpn`.
9. Are you going to support other VPS providers? Not right now.
10. Will this make me completely anonymous? No, absolutely not. All of your traffic is going through a VPS which could be traced back to your account. You can also be tracked still with [browser fingerprinting](https://panopticlick.eff.org/), etc. Your [IP address may still leak](https://ipleak.net/) due to WebRTC, Flash, etc.
11. How do I uninstall this thing on OSX? You can uninstall through the Web interface, which will also remove the running droplet in your DigitalOcean account. Alternatively go to System Preferences->Network, click on dosxvpn-* and click the '-' button in the bottom left to delete the VPN. Don't forget to also remove the droplet that is deployed in your DigitalOcean account.

# Powered By
* [strongSwan](https://strongswan.org/) - IPsec-based VPN software
* [CoreOS](https://coreos.com/) - used for running containers and automatic OS updates capabilities
* [Pi-hole](https://pi-hole.net/) - used for DNS adblocking
* [Platypus](http://www.sveinbjorn.org/platypus) - used to build the native OSX app
* [godo](https://github.com/digitalocean/godo) - DigitalOcean Go API client

# Acknowledgements
* [trailofbits/algo](https://github.com/trailofbits/algo) - strongSwan configuration is borrowed from this project
* [jbowens/dochaincore](https://github.com/jbowens/dochaincore) - web deployment code is borrowed from this project
* [vimagick/strongswan](https://github.com/vimagick/dockerfiles/tree/master/strongswan) - using a forked version of this docker image for VPN server

# Building Source
1. Install dependency [platypus cli](http://www.sveinbjorn.org/platypus)
  ```sh
  brew install platypus
  ```

2. Fetch the project with `go get`:
  ```sh
  go get github.com/dan-v/dosxvpn
  cd $GOPATH/src/github.com/dan-v/dosxvpn
  ```

2. Run make to build
  ```sh
  make
  ```

repo_name: dan-v/dosxvpn

repo_language: Go

mime_type: text/x-shellscript


text:
package web

import (
	"bytes"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"html/template"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"time"

	"github.com/dan-v/dosxvpn/deploy"
	"github.com/dan-v/dosxvpn/doclient"
)

type handler struct {
	oAuthClientID string
	token         string
	state         string
	host          string
	indexTmpl     *template.Template
	callbackTmpl  *template.Template
	dashboardTmpl *template.Template
	deleteTmpl    *template.Template
	regionTmpl    *template.Template
	progressTmpl  *template.Template
	completeTmpl  *template.Template
	deployment    *deploy.Deployment
}

func webHandler(host, oAuthClientID string) http.Handler {
	h := &handler{
		host:          host,
		oAuthClientID: oAuthClientID,
		indexTmpl:     template.Must(template.New("index page").Parse(indexPageHTML)),
		callbackTmpl:  template.Must(template.New("callback").Parse(callbackHTML)),
		dashboardTmpl: template.Must(template.New("dashboard").Parse(dashboardPageHTML)),
		deleteTmpl:    template.Must(template.New("delete").Parse(deletePageHTML)),
		progressTmpl:  template.Must(template.New("progress").Parse(progressPageHTML)),
		completeTmpl:  template.Must(template.New("complete").Parse(completePageHTML)),
	}
	h.generateState()
	mux := http.NewServeMux()
	mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
	mux.HandleFunc("/", h.index)
	mux.HandleFunc("/callback", h.callback)
	mux.HandleFunc("/dashboard", h.dashboardPage)
	mux.HandleFunc("/delete", h.delete)
	mux.HandleFunc("/install", h.progressPage)
	mux.HandleFunc("/status/", h.status)
	mux.HandleFunc("/complete", h.completePage)
	mux.HandleFunc("/download", h.download)
	mux.HandleFunc("/exit", h.exit)
	return mux
}

func (h *handler) index(rw http.ResponseWriter, req *http.Request) {
	vals := make(url.Values)
	vals.Set("response_type", "token")
	vals.Set("client_id", h.oAuthClientID)
	vals.Set("state", h.state)
	vals.Set("scope", "read write")
	vals.Set("redirect_uri", h.host+"/callback")
	u := url.URL{
		Scheme:   "https",
		Host:     "cloud.digitalocean.com",
		Path:     "/v1/oauth/authorize",
		RawQuery: vals.Encode(),
	}

	tmplData := struct {
		InstallLink string
	}{
		InstallLink: u.String(),
	}
	h.indexTmpl.Execute(rw, tmplData)
}

func (h *handler) callback(rw http.ResponseWriter, req *http.Request) {
	err := h.callbackTmpl.Execute(rw, nil)
	if err != nil {
		fmt.Fprintf(os.Stderr, "executing template: %s", err.Error())
	}
}

func (h *handler) dashboardPage(rw http.ResponseWriter, req *http.Request) {
	if h.token == "" {
		token, state := req.FormValue("access_token"), req.FormValue("state")
		if token == "" || state != h.state {
			http.Error(rw, "invalid oauth2 grant", http.StatusBadRequest)
			return
		}
		h.token = token
	}

	client := doclient.New(h.token)
	regions, err := client.ListRegions()
	if err != nil {
		http.Error(rw, fmt.Sprintf("failed to list regions: %v", err), http.StatusBadRequest)
		return
	}

	vpnList, err := deploy.ListVpns(h.token)
	if err != nil {
		http.Error(rw, "failed to list droplets", http.StatusBadRequest)
		return
	}

	tmplData := struct {
		Regions map[string]string
		VPNList []string
	}{
		Regions: regions,
		VPNList: vpnList,
	}

	err = h.dashboardTmpl.Execute(rw, tmplData)
	if err != nil {
		fmt.Fprintf(os.Stderr, "executing template: %s", err.Error())
	}
}

func (h *handler) delete(rw http.ResponseWriter, req *http.Request) {
	droplet := req.FormValue("droplet")
	if droplet == "" {
		rw.Write([]byte("Need to specify droplet"))
		return
	}
	_, err := deploy.RemoveVPN(h.token, droplet, true)
	if err != nil {
		rw.Write([]byte(fmt.Sprintf("Failed to remove VPN: %v", err)))
		return
	}

	remaining := false
	for i := 0; i < 5; i++ {
		vpns, _ := deploy.ListVpns(h.token)
		if err != nil {
			for _, vpn := range vpns {
				if vpn == droplet {
					remaining = true
				}
			}
		}
		if !remaining {
			break
		}
	}
	if !remaining {
		rw.Write([]byte("success"))
	} else {
		rw.Write([]byte("failure"))
	}
}

func (h *handler) region(rw http.ResponseWriter, req *http.Request) {
	token, state := req.FormValue("access_token"), req.FormValue("state")
	if token == "" || state != h.state {
		http.Error(rw, "invalid oauth2 grant", http.StatusBadRequest)
		return
	}
	h.token = token

	client := doclient.New(token)
	regions, err := client.ListRegions()
	if err != nil {
		http.Error(rw, "failed to list regions", http.StatusBadRequest)
	}

	tmplData := struct {
		Regions map[string]string
	}{
		Regions: regions,
	}

	err = h.regionTmpl.Execute(rw, tmplData)
	if err != nil {
		fmt.Fprintf(os.Stderr, "executing template: %s", err.Error())
	}
}

func (h *handler) progressPage(rw http.ResponseWriter, req *http.Request) {
	region := req.FormValue("region")
	var err error
	h.deployment, err = deploy.New(h.token, region, true)
	if err != nil {
		return
	}
	go h.deployment.Run()

	err = h.progressTmpl.Execute(rw, nil)
	if err != nil {
		fmt.Fprintf(os.Stderr, "executing template: %s", err.Error())
	}
}

func (h *handler) status(rw http.ResponseWriter, req *http.Request) {
	var buf bytes.Buffer
	_ = json.NewEncoder(&buf).Encode(h.deployment)

	rw.Header().Set("Content-Type", "application/json")
	rw.Write(buf.Bytes())
}

func (h *handler) completePage(rw http.ResponseWriter, req *http.Request) {
	tmplData := struct {
		FinalIP  string
		Password string
	}{
		FinalIP:  h.deployment.VPNIPAddress,
		Password: h.deployment.VpnPassword,
	}

	err := h.completeTmpl.Execute(rw, tmplData)
	if err != nil {
		fmt.Fprintf(os.Stderr, "executing template: %s", err.Error())
	}
}

func (h *handler) download(rw http.ResponseWriter, req *http.Request) {
	fileType := req.FormValue("type")
	name := h.deployment.Name
	if fileType == "apple" {
		savePath := filepath.Join(deploy.FilepathDosxvpnConfigDir, fmt.Sprintf(deploy.FilenameAppleConfig, name))
		data, err := ioutil.ReadFile(savePath)
		if err != nil {
			log.Println(err)
		}
		http.ServeContent(rw, req, name+".apple.mobileconfig", time.Now(), bytes.NewReader(data))
	} else if fileType == "android" {
		savePath := filepath.Join(deploy.FilepathDosxvpnConfigDir, fmt.Sprintf(deploy.FilenameAndroidConfig, name))
		data, err := ioutil.ReadFile(savePath)
		if err != nil {
			log.Println(err)
		}
		http.ServeContent(rw, req, name+".android.sswan", time.Now(), bytes.NewReader(data))
	} else {
		http.Error(rw, "invalid type to download", http.StatusBadRequest)
	}
}

func (h *handler) generateState() error {
	b := make([]byte, 16)
	_, err := rand.Read(b)
	if err != nil {
		return err
	}
	h.state = hex.EncodeToString(b)
	return nil
}

func (h *handler) exit(rw http.ResponseWriter, req *http.Request) {
	os.Exit(0)
}


text:
package web

const indexPageHTML = `<!doctype html>

		<title>dosxvpn</title>
		<link href="/static/images/favicon.png" rel="icon" type="image/png">
		<link rel="stylesheet" href="/static/css/style.css">
		<link rel="stylesheet" href="/static/css/bootstrap/bootstrap.css">
		<link rel="stylesheet" href="/static/css/fontawesome/css/font-awesome.min.css">

dsxvpn

			<form class="vertical-form sign-in">

This installer will create a fully configured VPN on a new 512MB droplet in your DigitalOcean account.

				 Authenticate

			</form>

				Don't have an account?
				Sign Up

`
const callbackHTML = `
<!DOCTYPE html>

		<title>dosxvpn</title>
		<link href="/static/images/favicon.png" rel="icon" type="image/png">
		<link rel="stylesheet" href="/static/css/style.css">
		<link rel="stylesheet" href="/static/css/bootstrap/bootstrap.css">
		<link rel="stylesheet" href="/static/css/fontawesome/css/font-awesome.min.css">

`
const dashboardPageHTML = `
<!DOCTYPE html>

		<title>dosxvpn</title>
		<link href="/static/images/favicon.png" rel="icon" type="image/png">
		<link rel="stylesheet" href="/static/css/style.css">
		<link rel="stylesheet" href="/static/css/bootstrap/bootstrap.css">
		<link rel="stylesheet" href="/static/css/fontawesome/css/font-awesome.min.css">

dsxvpn

	  <form id="regions" class="vertical-form sign-in">

Select Region:
			<select id="region-dropdown" name="region-dropdown">
				{{ range $key, $value := .Regions }}
					<option value="{{ $key }}">{{ $value }}</option>
				{{ end }}
			</select>

		<button type="submit" class="go-btn">Deploy New VPN</button>
	  </form>

	  {{ if .VPNList }}
	  <form id="advanced" class="vertical-form sign-in">

Manage Existing VPNs

		{{ range $index, $value := .VPNList }}
			<button id="{{ $value }}" onClick="removeVPN({{ $value }})" class="btn-del">Remove {{ $value }}</button>
		{{ end }}

Loading...

	  </form>
	  {{end}}

`
const deletePageHTML = `
<!DOCTYPE html>

		<title>dosxvpn</title>
		<link href="/static/images/favicon.png" rel="icon" type="image/png">
		<link rel="stylesheet" href="/static/css/style.css">
		<link rel="stylesheet" href="/static/css/bootstrap/bootstrap.css">
		<link rel="stylesheet" href="/static/css/fontawesome/css/font-awesome.min.css">

`
const progressPageHTML = `
<!DOCTYPE html>

		<title>dosxvpn</title>
		<link href="/static/images/favicon.png" rel="icon" type="image/png">
		<link rel="stylesheet" href="/static/css/style.css">
		<link rel="stylesheet" href="/static/css/bootstrap/bootstrap.css">
		<link rel="stylesheet" href="/static/css/fontawesome/css/font-awesome.min.css">

dsxvpn

	  <form class="vertical-form sign-in">

Initializing droplet..

	  </form>

`
const completePageHTML = `
<!DOCTYPE html>

		<title>dosxvpn</title>
		<link href="/static/images/favicon.png" rel="icon" type="image/png">
		<link rel="stylesheet" href="/static/css/style.css">
		<link rel="stylesheet" href="/static/css/bootstrap/bootstrap.css">
		<link rel="stylesheet" href="/static/css/fontawesome/css/font-awesome.min.css">

dsxvpn

	  <form class="vertical-form sign-in">

Install Complete

VPN IP: {{ .FinalIP }}

Show Certificate Password

{{ .Password }}

						 Download

						 Download

						 Adblock Settings

						 Back

						Exit

	  </form>

`


text:
package web

import (
	"log"
	"net/http"
	"os"
	"os/exec"
	"os/user"
	"time"
)

const (
	ListenPort           = "8999"
	DigitalOceanClientID = "e731e4858af83d074073a9bb8507c5aed08611121b90ed6d602a6d4ce43d5c8c"
)

func Run(cleanup bool) {
	// see https://github.com/sveinbjornt/Platypus/issues/57
	if cleanup {
		go cleanupWorkaround()
	}

	user, err := user.Current()
	if err != nil {
		log.Fatal(err)
	}

	host := "http://localhost:" + ListenPort
	exec.Command("sudo", "-u", user.Username, "open", host).Start()
	handler := webHandler(host, DigitalOceanClientID)
	err = http.ListenAndServe("127.0.0.1:"+ListenPort, handler)
	if err != nil {
		log.Fatal(err)
	}
}

func cleanupWorkaround() {
	for {
		_, err := exec.Command("pgrep", "-f", "-a", "Contents/MacOS/dosxvpn").Output()
		if err != nil {
			os.Exit(1)
		}
		time.Sleep(time.Second * 5)
	}
}


text:
package deploy

import (
	"log"
	"strings"

	"github.com/dan-v/dosxvpn/doclient"
	"github.com/dan-v/dosxvpn/vpn"
)

func RemoveVPN(token, name string, removeProfile bool) ([]string, error) {
	log.Printf("Listing droplets..")
	client := doclient.New(token)
	droplets, err := client.ListDroplets()
	if err != nil {
		return nil, err
	}

	log.Printf("Listing firewalls..")
	firewalls, err := client.ListFirewalls()
	if err == nil {
		for _, firewall := range firewalls {
			if strings.Contains(firewall.Name, name) {
				err := client.DeleteFirewall(firewall.ID)
				if err != nil {
					log.Println("Failed to remove firewall", firewall.Name, err)
				}
			}
		}
	}

	log.Printf("Removing droplet..")
	removedDroplets := make([]string, 0)
	for _, droplet := range droplets {
		if strings.Contains(droplet.Name, name) {
			err := client.DeleteDroplet(droplet.ID)
			if err != nil {
				log.Println("Failed to remove droplet", droplet.Name, err)
			}
			removedDroplets = append(removedDroplets, droplet.Name)
		}
	}

	if removeProfile {
		log.Printf("Removing OSX VPN profile for %s", name)
		err = vpn.OSXRemoveVPN(name)
		if err != nil {
			log.Printf("Failed to remove OSX VPN profile for %s. %v", name, err)
		}
		log.Printf("Finished removing OSX VPN profile for %s", name)
	}

	return removedDroplets, nil
}


text:
package deploy

import (
	"sort"
	"strings"

	"github.com/dan-v/dosxvpn/doclient"
)

func ListVpns(token string) ([]string, error) {
	client := doclient.New(token)
	droplets, err := client.ListDroplets()
	if err != nil {
		return nil, err
	}

	allDroplets := make([]string, 0)
	for _, droplet := range droplets {
		if strings.Contains(droplet.Name, DropletBaseName) {
			allDroplets = append(allDroplets, droplet.Name)
		}
	}
	sort.Strings(allDroplets)
	return allDroplets, nil
}


text:
package deploy

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"time"

	"github.com/dan-v/dosxvpn/genconfig"

	"github.com/dan-v/dosxvpn/doclient"
	"github.com/dan-v/dosxvpn/services"
	"github.com/dan-v/dosxvpn/services/coreos"
	"github.com/dan-v/dosxvpn/services/dosxvpn"
	"github.com/dan-v/dosxvpn/services/pihole"
	"github.com/dan-v/dosxvpn/sshclient"
	"github.com/dan-v/dosxvpn/vpn"
)

const (
	DropletBaseName     = "dosxvpn"
	DropletImage        = "coreos-beta"
	DropletSize         = "s-1vcpu-1gb"
	AutogeneratedSSHKey = "dosxvpn"
)

var (
	FilepathDosxvpnConfigDir = filepath.Join(userHomeDir(), ".dosxvpn")
	FilenameAppleConfig      = "%s.apple.mobileconfig"
	FilenameAndroidConfig    = "%s.android.sswan"
	FilenamePrivateKey       = "%s.client.cert.p12"
	FilenameCACert           = "%s.ca.cert.pem"
	FilenameServerCert       = "%s.server.cert.pem"
	VpnFiles                 = map[string]string{
		"/etc/ipsec.d/client.cert.p12":       FilenamePrivateKey,
		"/etc/ipsec.d/cacerts/ca.cert.pem":   FilenameCACert,
		"/etc/ipsec.d/certs/server.cert.pem": FilenameServerCert,
	}
)

type Deployment struct {
	Region          string
	AutoConfigure   bool
	Name            string
	Token           string
	services        []services.Service
	userData        string
	dropletIP       string
	dropletID       int
	doClient        *doclient.Client
	sshClient       *sshclient.Client
	VpnPassword     string
	Status          string `json:"status"`
	VPNIPAddress    string `json:"ip_address"`
	InitialPublicIP string `json:"initial_ip"`
	FinalPublicIP   string `json:"final_ip"`
}

func New(token, region string, autoConfigure bool) (*Deployment, error) {
	deploy := &Deployment{
		Name:          DropletBaseName + "-" + randomString(3) + "-" + region,
		Token:         token,
		Region:        region,
		AutoConfigure: autoConfigure,
		doClient:      doclient.New(token),
		services: []services.Service{
			&coreos.Service{}, &dosxvpn.Service{}, &pihole.Service{},
		},
		Status: "pending auth",
	}
	var err error
	deploy.sshClient, err = sshclient.New()
	if err != nil {
		return nil, err
	}
	deploy.userData, err = services.GenerateCloudConfig(deploy.sshClient.KeyPair.AuthorizedKey, deploy.services)
	if err != nil {
		return nil, err
	}
	return deploy, nil
}

func (d *Deployment) Run() error {
	log.Println("Getting initial IP...")
	initialPublicIP, _ := getPublicIp()
	d.InitialPublicIP = initialPublicIP
	log.Println("Initial IP is:", d.InitialPublicIP)

	log.Println("Getting account SSH keys...")
	accountSSHKeys, err := d.doClient.GetAccountSSHKeys()
	if err != nil {
		log.Fatal(err)
	}
	if len(accountSSHKeys) == 0 {
		log.Println("Did not find an SSH key. Generating an SSH key for account...")
		_, err := d.doClient.CreateSSHKey(AutogeneratedSSHKey, d.sshClient.GetPublicKey())
		if err != nil {
			log.Fatal(err)
		}
	}
	log.Println("Finished getting account SSH keys...")

	log.Println("Creating droplet...")
	dropletID, err := d.doClient.CreateDroplet(d.Name, d.Region, DropletSize, d.userData, DropletImage)
	if err != nil {
		log.Fatal(err)
	}
	d.dropletID = dropletID
	log.Printf("Finished creating droplet %s", d.Name)

	log.Println("Waiting for droplet to get IP...")
	dropletIP, err := d.doClient.WaitForDropletIP(d.dropletID)
	if err != nil {
		log.Fatal(err)
	}
	d.dropletIP = dropletIP
	log.Printf("Droplet now has IP %s...", d.dropletIP)
	d.VPNIPAddress = d.dropletIP

	log.Println("Creating firewall...")
	for attempt := 0; attempt < 5; attempt++ {
		time.Sleep(time.Duration(attempt) * time.Second)

		err = d.doClient.CreateFirewall(d.Name, d.dropletID)
		if err != nil {
			continue
		}
		if attempt >= 5 {
			log.Fatalf("Timeout waiting to create firewall for droplet %v", dropletID)
		}
	}
	log.Println("Finished creating firewall...")

	d.Status = "waiting for ssh"
	log.Println("Waiting for SSH to start...")
	err = waitForSSH(d.dropletIP)
	if err != nil {
		log.Fatal(err)
	}
	log.Println("SSH is now online...")

	log.Println("Waiting for VPN to become active...")
	_, err = d.sshClient.Run("core", d.dropletIP, "until docker logs dosxvpn &>/dev/null; do sleep 2; done; sleep 5;")
	if err != nil {
		log.Fatal(err)
	}
	log.Println("VPN is now active...")

	log.Println("Getting/generating VPN files...")
	privateKeyPasswordString, err := d.sshClient.GetFileFromContainer("core", d.dropletIP, "dosxvpn", "/etc/ipsec.d/client.cert.p12.password")
	if err != nil {
		return err
	}
	d.VpnPassword = privateKeyPasswordString

	privateKeyString, err := d.sshClient.GetFileFromContainer("core", d.dropletIP, "dosxvpn", "/etc/ipsec.d/client.cert.p12")
	if err != nil {
		return err
	}
	saveConfig(privateKeyString, filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenamePrivateKey, d.Name)))

	caCertString, err := d.sshClient.GetFileFromContainer("core", d.dropletIP, "dosxvpn", "/etc/ipsec.d/cacerts/ca.cert.pem")
	if err != nil {
		return err
	}
	saveConfig(caCertString, filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenameCACert, d.Name)))

	serverCertString, err := d.sshClient.GetFileFromContainer("core", d.dropletIP, "dosxvpn", "/etc/ipsec.d/certs/server.cert.pem")
	if err != nil {
		return err
	}
	saveConfig(serverCertString, filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenameServerCert, d.Name)))

	appleConfigString, err := genconfig.GenerateAppleConfig(d.dropletIP, d.Name, privateKeyPasswordString, privateKeyString, caCertString, serverCertString)
	if err != nil {
		return err
	}
	saveConfig(appleConfigString, filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenameAppleConfig, d.Name)))

	androidConfigString, err := genconfig.GenerateAndroidConfig(d.dropletIP, d.Name, privateKeyString, caCertString)
	if err != nil {
		return err
	}
	saveConfig(androidConfigString, filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenameAndroidConfig, d.Name)))

	log.Println("Finished getting/generating VPN files...")

	if d.AutoConfigure {
		d.Status = "adding vpn to osx"
		log.Println("Adding VPN to OSX...")
		err = vpn.OSXAddVPN(filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenameAppleConfig, d.Name)))
		if err != nil {
			log.Println("Failed to add VPN to OSX.", err)
		}
		log.Println("Done Adding VPN to OSX...")

		d.Status = "waiting for ip address change"
		for j := 0; j < 10; j++ {
			time.Sleep(time.Second * 5)
			newIP, err := getPublicIp()
			if err == nil && newIP != "" && newIP != initialPublicIP {
				newIP, _ := getPublicIp()
				d.FinalPublicIP = newIP
				break
			}
		}
		d.Status = "done"
	}

	log.Println("##############################")
	log.Println("VPN IP:", d.dropletIP)
	log.Println("Apple Config:", filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenameAppleConfig, d.Name)))
	log.Println("Android Config:", filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenameAndroidConfig, d.Name)))
	log.Println("Client Private Key:", filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenamePrivateKey, d.Name)))
	log.Println("Client Private Key Passphrase:", privateKeyPasswordString)
	log.Println("CA Certificate:", filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenameCACert, d.Name)))
	log.Println("Server Certificate:", filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(FilenameServerCert, d.Name)))
	log.Println("##############################")

	return nil
}

func (d *Deployment) getAndSaveConfigFiles(ip, name string) error {
	for remoteFile, localFile := range VpnFiles {
		fileString, err := d.sshClient.GetFileFromContainer("core", ip, "dosxvpn", remoteFile)
		if err != nil {
			return err
		}
		saveConfig(fileString, filepath.Join(FilepathDosxvpnConfigDir, fmt.Sprintf(localFile, name)))
	}
	return nil
}

func randomString(n int) string {
	rand.Seed(time.Now().UTC().UnixNano())
	return strconv.Itoa(rand.Int())[:n]
}

func saveConfig(configFileString, saveFile string) (string, error) {
	os.MkdirAll(FilepathDosxvpnConfigDir, os.ModePerm)
	err := ioutil.WriteFile(saveFile, []byte(configFileString), 0644)
	if err != nil {
		return "", err
	}
	return saveFile, nil
}

func getPublicIp() (string, error) {
	resp, err := http.Get("http://checkip.amazonaws.com/")
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	buf, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	return string(bytes.TrimSpace(buf)), nil
}

func waitForPort(host string, port int) error {
	for attempt := uint(0); attempt < 15; attempt++ {
		conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
		if err != nil {
			time.Sleep(5 * time.Second)
			continue
		} else if err != nil {
			return err
		}
		conn.Close()
		return nil
	}
	return fmt.Errorf("timed out waiting for port %d to open", port)
}

func waitForSSH(ip string) error {
	return waitForPort(ip, 22)
}

func userHomeDir() string {
	if runtime.GOOS == "windows" {
		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
		if home == "" {
			home = os.Getenv("USERPROFILE")
		}
		return home
	}
	return os.Getenv("HOME")
}


text:
package genconfig

import (
	"bytes"
	"encoding/base64"
	"text/template"

	uuid "github.com/satori/go.uuid"
)

func GenerateAppleConfig(ip, name, password, privateKey, caCert, serverCert string) (string, error) {
	tmpl := template.Must(template.New("mobileconfig").Parse(mobileConfigTemplate))
	tmplData := struct {
		IP                 string
		Name               string
		PrivateKey         string
		PrivateKeyPassword string
		CACert             string
		ServerCert         string
		UUID1              string
		UUID2              string
		UUID3              string
		UUID4              string
		UUID5              string
		UUID6              string
	}{
		IP:                 ip,
		Name:               name,
		PrivateKeyPassword: password,
		PrivateKey:         base64.StdEncoding.EncodeToString([]byte(privateKey)),
		CACert:             base64.StdEncoding.EncodeToString([]byte(caCert)),
		ServerCert:         base64.StdEncoding.EncodeToString([]byte(serverCert)),
		UUID1:              uuid.NewV4().String(),
		UUID2:              uuid.NewV4().String(),
		UUID3:              uuid.NewV4().String(),
		UUID4:              uuid.NewV4().String(),
		UUID5:              uuid.NewV4().String(),
		UUID6:              uuid.NewV4().String(),
	}
	var buf bytes.Buffer
	err := tmpl.Execute(&buf, tmplData)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}


text:
package genconfig

const androidConfigTemplate = `{
    "uuid": "{{.UUID}}",
    "name": "{{.Name}}",
    "type": "ikev2-cert",
    "remote": {
		"addr": "{{.IP}}"
	},
	"split-tunneling": {
		"block-ipv4": true,
		"block-ipv6": true
    },
    "local": {
        "id": "{{.IP}}",
        "p12": "{{.PrivateKey}}"
    },
    "mtu": 1280
}`


text:
package genconfig

const mobileConfigTemplate = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 <key>PayloadContent</key>
 <array>
  <dict>
   <key>Password</key>
   <string>{{.PrivateKeyPassword}}</string>
   <key>PayloadCertificateFileName</key>
   <string>client.cert.p12</string>
   <key>PayloadContent</key>
   <data>
{{.PrivateKey}}
   </data>
   <key>PayloadDescription</key>
   <string>PKCS#12</string>
   <key>PayloadDisplayName</key>
   <string>client.cert.p12</string>
   <key>PayloadIdentifier</key>
   <string>com.apple.security.pkcs12.{{.UUID1}}</string>
   <key>PayloadType</key>
   <string>com.apple.security.pkcs12</string>
   <key>PayloadUUID</key>
   <string>{{.UUID1}}</string>
   <key>PayloadVersion</key>
   <integer>1</integer>
  </dict>
  <dict>
   <key>PayloadCertificateFileName</key>
   <string>ca.cer</string>
   <key>PayloadContent</key>
   <data>
{{.CACert}}
   </data>
   <key>PayloadDescription</key>
   <string>CA</string>
   <key>PayloadDisplayName</key>
   <string>strongSwan Root CA</string>
   <key>PayloadIdentifier</key>
   <string>com.apple.security.root.{{.UUID2}}</string>
   <key>PayloadType</key>
   <string>com.apple.security.root</string>
   <key>PayloadUUID</key>
   <string>{{.UUID2}}</string>
   <key>PayloadVersion</key>
   <integer>1</integer>
  </dict>
  <dict>
   <key>IKEv2</key>
   <dict>
    <key>OnDemandEnabled</key>
    <integer>1</integer>
    <key>OnDemandRules</key>
    <array>
    <dict>
    <key>Action</key>
    <string>Connect</string>
    </dict>
    </array>
    <key>AuthenticationMethod</key>
    <string>Certificate</string>
    <key>ChildSecurityAssociationParameters</key>
    <dict>
    <key>DiffieHellmanGroup</key>
        <integer>19</integer>
        <key>EncryptionAlgorithm</key>
        <string>AES-128-GCM</string>
        <key>IntegrityAlgorithm</key>
        <string>SHA2-512</string>
        <key>LifeTimeInMinutes</key>
        <integer>20</integer>
    </dict>
    <key>DeadPeerDetectionRate</key>
    <string>Medium</string>
    <key>DisableMOBIKE</key>
    <integer>0</integer>
    <key>DisableRedirect</key>
    <integer>1</integer>
    <key>EnableCertificateRevocationCheck</key>
    <integer>0</integer>
    <key>EnablePFS</key>
    <true/>
    <key>IKESecurityAssociationParameters</key>
    <dict>
        <key>DiffieHellmanGroup</key>
        <integer>19</integer>
        <key>EncryptionAlgorithm</key>
        <string>AES-128-GCM</string>
        <key>IntegrityAlgorithm</key>
        <string>SHA2-512</string>
        <key>LifeTimeInMinutes</key>
        <integer>20</integer>
    </dict>
    <key>LocalIdentifier</key>
    <string>{{.IP}}</string>
    <key>PayloadCertificateUUID</key>
    <string>{{.UUID1}}</string>
    <key>CertificateType</key>
    <string>ECDSA256</string>
    <key>ServerCertificateIssuerCommonName</key>
    <string>{{.IP}}</string>
    <key>RemoteAddress</key>
    <string>{{.IP}}</string>
    <key>RemoteIdentifier</key>
    <string>{{.IP}}</string>
    <key>UseConfigurationAttributeInternalIPSubnet</key>
    <integer>0</integer>
   </dict>
   <key>IPv4</key>
   <dict>
    <key>OverridePrimary</key>
    <integer>1</integer>
   </dict>
   <key>PayloadDescription</key>
   <string>Configures VPN settings</string>
   <key>PayloadDisplayName</key>
   <string>{{.Name}}</string>
   <key>PayloadIdentifier</key>
   <string>com.apple.vpn.managed.{{.UUID4}}</string>
   <key>PayloadType</key>
   <string>com.apple.vpn.managed</string>
   <key>PayloadUUID</key>
   <string>{{.UUID4}}</string>
   <key>PayloadVersion</key>
   <real>1</real>
   <key>Proxies</key>
   <dict>
    <key>HTTPEnable</key>
    <integer>0</integer>
    <key>HTTPSEnable</key>
    <integer>0</integer>
   </dict>
   <key>UserDefinedName</key>
   <string>{{.Name}}</string>
   <key>VPNType</key>
   <string>IKEv2</string>
  </dict>
  <dict>
   <key>PayloadCertificateFileName</key>
   <string>server.cer</string>
   <key>PayloadContent</key>
   <data>
{{.ServerCert}}
   </data>
   <key>PayloadDescription</key>
   <string>PKCS#1</string>
   <key>PayloadDisplayName</key>
   <string>{{.IP}}</string>
   <key>PayloadIdentifier</key>
   <string>com.apple.security.pkcs1.{{.UUID5}}</string>
   <key>PayloadType</key>
   <string>com.apple.security.pkcs1</string>
   <key>PayloadUUID</key>
   <string>{{.UUID5}}</string>
   <key>PayloadVersion</key>
   <integer>1</integer>
  </dict>
 </array>
 <key>PayloadDisplayName</key>
 <string>{{.Name}}</string>
 <key>PayloadIdentifier</key>
 <string>com.github.dan-v.dosxvpn.{{.Name}}</string>
 <key>PayloadRemovalDisallowed</key>
 <false/>
 <key>PayloadType</key>
 <string>Configuration</string>
 <key>PayloadUUID</key>
 <string>{{.UUID6}}</string>
 <key>PayloadVersion</key>
 <integer>1</integer>
</dict>
</plist>`


text:
package genconfig

import (
	"bytes"
	"encoding/base64"
	"text/template"

	uuid "github.com/satori/go.uuid"
)

func GenerateAndroidConfig(ip, name, privateKey, caCert string) (string, error) {
	tmpl := template.Must(template.New("androidconfig").Parse(androidConfigTemplate))
	tmplData := struct {
		IP         string
		Name       string
		PrivateKey string
		CACert     string
		UUID       string
	}{
		IP:         ip,
		Name:       name,
		PrivateKey: base64.StdEncoding.EncodeToString([]byte(privateKey)),
		CACert:     base64.StdEncoding.EncodeToString([]byte(caCert)),
		UUID:       uuid.NewV4().String(),
	}
	var buf bytes.Buffer
	err := tmpl.Execute(&buf, tmplData)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}


text:
package services

import (
	"bytes"
	"text/template"
)

type Service interface {
	UserData() string
}

func GenerateCloudConfig(authorizedKey []byte, services []Service) (string, error) {
	var userData string
	for _, service := range services {
		userData += service.UserData()
	}
	t, err := template.New("userdata").Parse(userData)
	if err != nil {
		return "", err
	}

	type userDataParams struct {
		SSHAuthorizedKey string
	}
	params := userDataParams{
		SSHAuthorizedKey: string(authorizedKey),
	}

	var buf bytes.Buffer
	err = t.Execute(&buf, params)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}


text:
package dosxvpn

type Service struct{}

func (s Service) UserData() string {
	return `
    - name: dosxvpn-sysctl.service
      enable: true
      command: start
      content: |
        [Unit]
        Description=Handles settings for sysctl

        [Service]
        Type=oneshot
        User=root
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.ip_forward=1
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.forwarding=1
        ExecStartPre=/usr/sbin/sysctl -w net.ipv6.conf.all.forwarding=1
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.accept_source_route=0
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.default.accept_source_route=0
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.accept_redirects=0
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.default.accept_redirects=0
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.secure_redirects=0
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.default.secure_redirects=0
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.icmp_ignore_bogus_error_responses=1
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.rp_filter=1
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.default.rp_filter=1
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0
        ExecStartPre=/usr/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0
        ExecStartPre=/usr/bin/echo 1 > /proc/sys/net/ipv4/route/flush
        ExecStartPre=/usr/bin/echo 1 > /proc/sys/net/ipv6/route/flush
        ExecStart=/usr/bin/echo
    - name: dosxvpn-update.service
      content: |
        [Unit]
        Description=Handles updates for dosxvpn

        [Service]
        Type=oneshot
        ExecStartPre=/usr/bin/docker pull dosxvpn/updater:latest
        ExecStart=/usr/bin/docker run --rm --privileged -v /var/run/docker.sock:/var/run/docker.sock dosxvpn/updater:latest
    - name: dosxvpn-update.timer
      enable: true
      command: start
      content: |
        [Unit]
        Description=Run dosxvpn-update on schedule

        [Timer]
        OnCalendar=*-*-* 0/12:00:00
    - name: dosxvpn.service
      command: start
      content: |
        [Unit]
        Description=dosxvpn
        After=docker.service,dummy-interface.service

        [Service]
        User=core
        Restart=always
        TimeoutStartSec=0
        KillMode=none
        EnvironmentFile=/etc/environment
        ExecStartPre=-/usr/bin/docker kill dosxvpn
        ExecStartPre=-/usr/bin/docker rm dosxvpn
        ExecStartPre=/usr/bin/docker pull dosxvpn/strongswan:latest
        ExecStart=/usr/bin/docker run --name dosxvpn -e VPN_DNS="1.1.1.2" -e DUMMY_DEVICE="1.1.1.2/32" -e VPN_DOMAIN=$public_ipv4 --privileged --net=host -v ipsec.d:/etc/ipsec.d -v strongswan.d:/etc/strongswan.d -v /lib/modules:/lib/modules -v /etc/localtime:/etc/localtime dosxvpn/strongswan:latest
        ExecStop=/usr/bin/docker stop dosxvpn
`
}


text:
package coreos

type Service struct{}

func (s Service) UserData() string {
	return `#cloud-config
ssh_authorized_keys:
  - {{.SSHAuthorizedKey}}
write_files:
  - path: /etc/ssh/sshd_config
    permissions: 0600
    owner: root:root
    content: |
      # Use most defaults for sshd configuration.
      UsePrivilegeSeparation sandbox
      Subsystem sftp internal-sftp
      PermitRootLogin no
      AllowUsers core
      PasswordAuthentication no
      ChallengeResponseAuthentication no
  - path: /var/lib/iptables/rules-save
    permissions: 0644
    owner: root:root
    content: |
      *mangle
      :PREROUTING ACCEPT [0:0]
      :INPUT ACCEPT [0:0]
      :FORWARD ACCEPT [0:0]
      :OUTPUT ACCEPT [0:0]
      :POSTROUTING ACCEPT [0:0]
      -A FORWARD -s 192.168.99.0/24 -o eth0 -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360
      COMMIT
      *nat
      :PREROUTING ACCEPT [0:0]
      :POSTROUTING ACCEPT [0:0]
      -A POSTROUTING -s 192.168.99.0/24 -m policy --pol none --dir out -j MASQUERADE
      COMMIT
      *filter
      :INPUT DROP [0:0]
      :FORWARD DROP [0:0]
      :OUTPUT ACCEPT [0:0]
      -A INPUT -i lo -j ACCEPT
      -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
      -A INPUT -p esp -j ACCEPT
      -A INPUT -p ah -j ACCEPT
      -A INPUT -p ipencap -m policy --dir in --pol ipsec --proto esp -j ACCEPT
      -A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT
      -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH
      -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 10 --rttl --name SSH -j DROP
      -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT
      -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT
      -A INPUT -p tcp --destination-port 443 -j REJECT --reject-with tcp-reset
      -A INPUT -p udp --destination-port 80 -j REJECT --reject-with icmp-port-unreachable
      -A INPUT -p udp --destination-port 443 -j REJECT --reject-with icmp-port-unreachable
      -A INPUT -d 1.1.1.2 -p udp -j ACCEPT
      -A INPUT -d 1.1.1.2 -p tcp -j ACCEPT
      -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
      -A FORWARD -m conntrack --ctstate NEW -s 192.168.99.0/24 -m policy --pol ipsec --dir in -j ACCEPT
      COMMIT
  - path: /var/lib/ip6tables/rules-save
    permissions: 0644
    owner: root:root
    content: |
      *nat
      :PREROUTING ACCEPT [0:0]
      :POSTROUTING ACCEPT [0:0]
      -A POSTROUTING -s fd9d:bc11:4020::/48 -m policy --pol none --dir out -j MASQUERADE
      COMMIT
      *filter
      :INPUT DROP [0:0]
      :FORWARD DROP [0:0]
      :OUTPUT ACCEPT [0:0]
      :ICMPV6-CHECK - [0:0]
      :ICMPV6-CHECK-LOG - [0:0]
      -A INPUT -i lo -j ACCEPT
      -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
      -A INPUT -p esp -j ACCEPT
      -A INPUT -m ah -j ACCEPT
      -A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT
      -A INPUT -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT
      -A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT
      -A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT
      -A INPUT -p icmpv6 --icmpv6-type redirect -m hl --hl-eq 255 -j ACCEPT
      -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH
      -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 10 --rttl --name SSH -j DROP
      -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT
      -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT
      -A INPUT -p tcp --destination-port 443 -j REJECT --reject-with tcp-reset
      -A INPUT -p udp --destination-port 80 -j REJECT --reject-with icmp6-port-unreachable
      -A INPUT -p udp --destination-port 443 -j REJECT --reject-with icmp6-port-unreachable
      -A INPUT -d fd9d:bc11:4020::/48 -p udp -j ACCEPT
      -A INPUT -d fd9d:bc11:4020::/48 -p tcp -j ACCEPT
      -A FORWARD -j ICMPV6-CHECK
      -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
      -A FORWARD -m conntrack --ctstate NEW -s fd9d:bc11:4020::/48 -m policy --pol ipsec --dir in -j ACCEPT
      -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-solicitation -j ICMPV6-CHECK-LOG
      -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-advertisement -j ICMPV6-CHECK-LOG
      -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-solicitation -j ICMPV6-CHECK-LOG
      -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-advertisement -j ICMPV6-CHECK-LOG
      -A ICMPV6-CHECK-LOG -j LOG --log-prefix "ICMPV6-CHECK-LOG DROP "
      -A ICMPV6-CHECK-LOG -j DROP
      COMMIT

coreos:
  update:
    reboot-strategy: reboot
  locksmith:
    window-start: 10:00
    window-length: 1h
  units:
    - name: iptables-restore.service
      enable: true
      command: start
    - name: ip6tables-restore.service
      enable: true
      command: start
    - name: dummy-interface.service
      command: start
      enable: true
      content: |
        [Unit]
        Description=Creates a dummy local interface

        [Service]
        User=root
        Type=oneshot
        ExecStartPre=/bin/sh -c "modprobe dummy"
        ExecStartPre=-/bin/sh -c "ip link add dummy0 type dummy"
        ExecStartPre=/bin/sh -c "ip link set dummy0 up"
        ExecStartPre=-/bin/sh -c "ifconfig dummy0 inet6 add fd9d:bc11:4020::/48"
        ExecStartPre=-/bin/sh -c "ifconfig dummy0 1.1.1.2/32"
        ExecStartPre=-/bin/sh -c "ifconfig dummy0 inet6 add fd9d:bc11:4020::/48"
        ExecStartPre=-/bin/sh -c "ifconfig dummy0 1.1.1.2/32"
        ExecStart=/bin/sh -c "echo"
`
}


text:
package pihole

type Service struct{}

func (s Service) UserData() string {
	return `
    - name: pihole-etc-host.service
      command: start
      content: |
        [Unit]
        Description=pihole /etc/hosts entry
        ConditionFirstBoot=true

        [Service]
        User=root
        Type=oneshot
        ExecStart=/bin/sh -c "echo 1.1.1.2         pi.hole >> /etc/hosts"
    - name: pihole.service
      command: start
      content: |
        [Unit]
        Description=pihole
        After=docker.service,dummy-interface.service

        [Service]
        User=core
        Restart=always
        TimeoutStartSec=0
        KillMode=none
        EnvironmentFile=/etc/environment
        ExecStartPre=-/usr/bin/docker kill pihole
        ExecStartPre=-/usr/bin/docker rm pihole
        ExecStartPre=/usr/bin/docker pull pihole/pihole:latest
        ExecStart=/usr/bin/docker run --name pihole --net=host -e DNS1=1.1.1.1 -e ServerIP=1.1.1.2 -e ServerIPv6=fd9d:bc11:4020:: -e WEBPASSWORD=dosxvpn -v pihole-etc:/etc/pihole -v pihole-dnsmasq.d:/etc/dnsmasq.d pihole/pihole:latest
        ExecStop=/usr/bin/docker stop pihole`
}


text:
package sshclient

import (
	"bufio"
	"bytes"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"io"
	"strings"

	"golang.org/x/crypto/ssh"
)

type Client struct {
	KeyPair *SSHKeyPair
}

type SSHKeyPair struct {
	PrivateKey    *rsa.PrivateKey
	PublicKey     ssh.PublicKey
	PrivateKeyPEM []byte
	AuthorizedKey []byte
}

func New() (*Client, error) {
	ssh := &Client{}
	err := ssh.generateKeyPair()
	if err != nil {
		return nil, err
	}
	return ssh, nil
}

func (s *Client) GetPublicKey() string {
	return string(ssh.MarshalAuthorizedKey(s.KeyPair.PublicKey))
}

func (s *Client) openSession(user, host string) (*ssh.Session, error) {
	signer, err := ssh.NewSignerFromKey(s.KeyPair.PrivateKey)
	if err != nil {
		return nil, err
	}
	config := &ssh.ClientConfig{
		User:            user,
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		Auth: []ssh.AuthMethod{
			ssh.PublicKeys(signer),
		},
	}
	client, err := ssh.Dial("tcp", host+":22", config)
	if err != nil {
		return nil, err
	}

	session, err := client.NewSession()
	if err != nil {
		return nil, err
	}

	return session, nil
}

func (s *Client) GetFileFromContainer(user, host, containerName, file string) (string, error) {
	command := fmt.Sprintf("docker exec %s cat %s", containerName, file)
	return s.Run(user, host, command)
}

func (s *Client) Run(user, host, command string) (string, error) {
	session, err := s.openSession(user, host)
	if err != nil {
		return "", err
	}

	rOut, err := session.StdoutPipe()
	if err != nil {
		return "", err
	}
	rErr, err := session.StderrPipe()
	if err != nil {
		return "", err
	}
	combined := io.MultiReader(rOut, rErr)

	err = session.Start(command)
	if err != nil {
		return "", err
	}

	var lines []string
	scanner := bufio.NewScanner(combined)
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	session.Close()

	output := strings.Join(lines, "\n")
	output = strings.TrimSpace(output)
	return output, nil
}

func (s *Client) generateKeyPair() error {
	privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
	if err != nil {
		return err
	}

	var pembuf bytes.Buffer
	err = pem.Encode(&pembuf, &pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
	})
	if err != nil {
		return err
	}

	publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
	if err != nil {
		return err
	}
	authorizedKey := ssh.MarshalAuthorizedKey(publicKey)

	s.KeyPair = &SSHKeyPair{
		PrivateKey:    privateKey,
		PublicKey:     publicKey,
		PrivateKeyPEM: pembuf.Bytes(),
		AuthorizedKey: authorizedKey,
	}
	return nil
}


text:
/* Borrowed from Digital Ocean CSS */
.login * {
  box-sizing: border-box;
  font-family: "proxima-nova", helvetica;
}

.login br.only-mobile {
  display: none;
}

.login .logo {
  display: block;
  height: 75px;
  padding-top: 40px;
  margin: 0 auto 30px;
}

.login .logo.registration-message {
  margin-bottom: 0;
}

.login a, .login a:visited {
  color: inherit;
}

.login ul.notice {
  margin: 0;
  border: 0;
  width: 100%;
  text-align: center;
  list-style-position: inside;
  list-style: none;
  font-weight: 600;
  font-size: 15px;
  padding: 10px;
}

.login ul.notice.errors {
  background-color: #EDE04D;
  color: #212200;
}

.login ul.notice.success {
  background-color: #15CD72;
  color: white;
}

.login ul.notice li {
  padding: 5px 0;
}

.login p.registration-message {
  font-size: 35px;
  line-height: 50px;
  font-weight: 400;
  text-align: center;
  color: white;
  margin: 15px 0 20px;
}

.login .vertical-form {
  margin: 0 auto;
  width: 470px;
  padding: 20px;
  border-radius: 3px;
  background: white;
}

.login .vertical-form legend {
  text-align: center;
  font-weight: 300;
  color: #444;
  margin: 0 auto 45px;
  font-size: 35px;
  line-height: 38px;
  text-transform: none;
  letter-spacing: 0;
}

.login .vertical-form legend:after {
  display: none;
}

.login .vertical-form ::-webkit-input-placeholder {
  color: #afafaf;
  font-size: 17px;
}

.login .vertical-form input[type='text'], .login .vertical-form input[type='email'], .login .vertical-form input[type='password'], .login .vertical-form input[type='number'], .login .vertical-form a[type='submit'], .login .vertical-form button[type='submit'] {
  background: #fcfcfc;
  border: 2px solid #e7e7e7;
  margin: 20px auto;
  padding: 15px;
  width: 100%;
  display: block;
  border-radius: 3px;
  font-size: 18px;
  transition: opacity 0.4s;
  text-align: left;
  height: auto;
  -webkit-appearance: none;
}

.login .vertical-form input[type='text']:focus, .login .vertical-form input[type='email']:focus, .login .vertical-form input[type='password']:focus, .login .vertical-form input[type='number']:focus, .login .vertical-form a[type='submit']:focus, .login .vertical-form button[type='submit']:focus {
  border-color: #E7E7E7;
  box-shadow: none;
}

.login .vertical-form a[type='submit'], .login .vertical-form button[type='submit'] {
  color: white;
  background: #15CD72;
  border: 0;
  font-weight: 600;
  font-size: 17px;
  text-align: center;
  cursor: pointer;
  margin-bottom: 0;
  height: auto;
  -webkit-appearance: none;
}

.login .vertical-form a:hover[type='submit'], .login .vertical-form button:hover[type='submit'] {
  background-color: #0CB863;
  -webkit-transition: background 0.1s;
  -moz-transition: background 0.1s;
  -o-transition: background 0.1s;
  transition: background 0.1s;
  text-decoration: none !important;
}

.login .vertical-form p {
  font-size: 17px;
  line-height: 1.5;
  text-align: center;
  margin: 0;
  margin-top: 10px;
  color: #666;
}

.login .vertical-form .region {
  font-size: 25px;
  line-height: 1.5;
  text-align: center;
  margin: 0;
  margin-top: 10px;
  color: #666;
}

.login .vertical-form p a {
  color: #0069ff;
  text-decoration: none;
}

.login .vertical-form.sign-in, .login .vertical-form.register {
  padding-bottom: 20px;
}

.login .vertical-form.sign-in p, .login .vertical-form.register p {
  padding-top: 8px;
}

.login .vertical-form.tfa-form legend {
  margin-bottom: 15px;
}

.login .vertical-form.tfa-form + .footer p {
  margin-bottom: 5px;
}

.login .footer {
  text-align: center;
  color: white;
  margin: 20px auto;
}

.login .footer a {
  font-weight: 600;
  border: 0;
  padding: 0;
}

.login .footer a:hover {
  color: #E7F1F9;
}

.login .footer p {
  margin-bottom: 40px;
}

@media screen and (max-width: 550px) {
  .login .logo {
    height: 65px;
    padding-top: 30px;
  }

  .login p.oauth {
    margin: 15px 0 25px;
    font-size: 17px;
    line-height: 21px;
    font-weight: 600;
  }

  .login .vertical-form {
    width: 100%;
    border-radius: 0;
    padding: 40px 20px;
  }
}
@media screen and (max-width: 350px) {
  .login br.only-mobile {
    display: inline;
  }
}

#progress-bar {
  height: 30px;
  width: 390px;
  background: rgb(252, 252, 252);
}

#current-progress {
  width: 2%;
  height: 100%;
  background: #008bcf;
}

#region-dropdown {
  font-size: 15px;
  display: inline-block;
  max-width: 100%;
  padding: .375rem 1.75rem .375rem 1.75rem;
  line-height: 1.25;
  color: #000000;
  vertical-align: middle;
  border: 1px solid rgba(0, 31, 255, 0.15);
  border-radius: .25rem;
  -moz-appearance: none;
  -webkit-appearance: none;
}

.login .vertical-form.sign-in p[id="initial-ip"] {
  font-size: 12px;
}
.login .vertical-form.sign-in p[id="final-ip"] {
  font-size: 18px;
}

.login .vertical-form a:hover[class='btn-del'] {
  background-color: #6b6b6b;
  -webkit-transition: background 0.1s;
  -moz-transition: background 0.1s;
  -o-transition: background 0.1s;
  transition: background 0.1s;
}

.login .vertical-form a[class='btn-auth']:focus {
  background-color: #0CB863;
  box-shadow: none;
}

.login .vertical-form a[class='btn-auth'] {
    background: #15CD72;
    color: white !important;
    display: inline-block;
    overflow: visible;
    padding: 0 32px;
    height: 48px;
    border: 0;
    border-radius: 3px;
    color: #333;
    vertical-align: middle;
    text-align: center;
    text-decoration: none;
    text-transform: none;
    white-space: nowrap;
    font: inherit;
    font-weight: 600;
    font-size: 16px;
    line-height: 48px;
    cursor: pointer;
    transition: all .25s cubic-bezier(.645,.045,.355,1);
    -moz-appearance: none;
    -webkit-appearance: none;
}

.login .vertical-form button[class='btn-del']:focus {
  background-color: #ff4242;
  box-shadow: none;
}

.login .vertical-form button[class='btn-del'] {
  color: white;
  background: #8c8c8c;
  font-size: 14px;
  border: 2px solid #e7e7e7;
  margin: 5px auto;
  padding: 5px;
  width: 100%;
  display: block;
  border-radius: 3px;
  transition: opacity 0.4s;
  text-align: center;
  height: auto;
  -webkit-appearance: none;
}

body {
  background: linear-gradient(to bottom, #66a5ff, #0069ff) fixed;
}

.loader {
  display:none;
  margin: 30px auto;
  font-size: 25px;
  width: 1em;
  height: 1em;
  border-radius: 50%;
  position: relative;
  text-indent: -9999em;
  -webkit-animation: load5 1.1s infinite ease;
  animation: load5 1.1s infinite ease;
  -webkit-transform: translateZ(0);
  -ms-transform: translateZ(0);
  transform: translateZ(0);
}
@-webkit-keyframes load5 {
  0%,
  100% {
    box-shadow: 0em -2.6em 0em 0em #000000, 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.5), -1.8em -1.8em 0 0em rgba(0,0,0, 0.7);
  }
  12.5% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.7), 1.8em -1.8em 0 0em #000000, 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.5);
  }
  25% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.5), 1.8em -1.8em 0 0em rgba(0,0,0, 0.7), 2.5em 0em 0 0em #000000, 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
  }
  37.5% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.5), 2.5em 0em 0 0em rgba(0,0,0, 0.7), 1.75em 1.75em 0 0em #000000, 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
  }
  50% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.5), 1.75em 1.75em 0 0em rgba(0,0,0, 0.7), 0em 2.5em 0 0em #000000, -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
  }
  62.5% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.5), 0em 2.5em 0 0em rgba(0,0,0, 0.7), -1.8em 1.8em 0 0em #000000, -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
  }
  75% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.5), -1.8em 1.8em 0 0em rgba(0,0,0, 0.7), -2.6em 0em 0 0em #000000, -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
  }
  87.5% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.5), -2.6em 0em 0 0em rgba(0,0,0, 0.7), -1.8em -1.8em 0 0em #000000;
  }
}
@keyframes load5 {
  0%,
  100% {
    box-shadow: 0em -2.6em 0em 0em #000000, 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.5), -1.8em -1.8em 0 0em rgba(0,0,0, 0.7);
  }
  12.5% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.7), 1.8em -1.8em 0 0em #000000, 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.5);
  }
  25% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.5), 1.8em -1.8em 0 0em rgba(0,0,0, 0.7), 2.5em 0em 0 0em #000000, 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
  }
  37.5% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.5), 2.5em 0em 0 0em rgba(0,0,0, 0.7), 1.75em 1.75em 0 0em #000000, 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
  }
  50% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.5), 1.75em 1.75em 0 0em rgba(0,0,0, 0.7), 0em 2.5em 0 0em #000000, -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
  }
  62.5% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.5), 0em 2.5em 0 0em rgba(0,0,0, 0.7), -1.8em 1.8em 0 0em #000000, -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
  }
  75% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.5), -1.8em 1.8em 0 0em rgba(0,0,0, 0.7), -2.6em 0em 0 0em #000000, -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
  }
  87.5% {
    box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.5), -2.6em 0em 0 0em rgba(0,0,0, 0.7), -1.8em -1.8em 0 0em #000000;
  }
}


text:
$(document).ready(function() {
    var currentPct = 0;
    function updateProgressBar(pct, extraMax, extraDuration) {
        if (currentPct != pct) {
            currentPct = pct;
            $('#current-progress').stop();
            $('#current-progress').animate({width: pct+'%'}, 1000, 'swing', function() {
            if (extraMax && extraDuration) {
                $('#current-progress').animate({width: extraMax+'%'}, extraDuration);
            }
            });
        }
    }

    function updateStatus() {
        $.get("/status/", function(resp) {
            console.log("Status: ", resp);
            if (resp.status == 'pending auth') {
                $('#status-line').text('Provisioning droplet...');
                updateProgressBar(5);
            } else if (resp.status == 'waiting for ssh') {
                $('#status-line').text('Setting up droplet...');
                if (resp.initial_ip) {
                    $('#initial-ip').text("Current Public IP: " + resp.initial_ip);
                    $('#initial-ip').css('display', 'block');
                }
                updateProgressBar(10, 65, 60000);
            } else if (resp.status == 'configuring vpn') {
                $('#status-line').text('Configuring VPN...');
                updateProgressBar(65, 90, 30000);
            } else if (resp.status == 'adding vpn to osx') {
                $('#status-line').text('Adding VPN to OSX...');
                updateProgressBar(93);
            } else if (resp.status == 'waiting for ip address change') {
                $('#status-line').text('Waiting for active VPN connection...');
                updateProgressBar(97);
            } else if (resp.status == 'done') {
                updateProgressBar(100);
                window.location.replace("/complete?final_ip=" + resp.final_ip);
            } else {
                updateProgressBar(0);
                $('#status-line').text('Install failed: ' + resp.status);
                $('#initial-ip').text('');
                $('#initial-ip').wrap('Retry Installation');
                return;
            }
            if (resp.status != 'done') {
                setTimeout(updateStatus, 1000);
            }
        });
    }

    updateStatus();
});


text:
$(document).ready(function() {
    var getUrlParameter = function getUrlParameter(sParam) {
        var sPageURL = decodeURIComponent(window.location.search.substring(1)),
            sURLVariables = sPageURL.split('&'),
            sParameterName,
            i;

        for (i = 0; i < sURLVariables.length; i++) {
            sParameterName = sURLVariables[i].split('=');

            if (sParameterName[0] === sParam) {
                return sParameterName[1] === undefined ? true : sParameterName[1];
            }
        }
    };
    var token = getUrlParameter('access_token');
    var state = getUrlParameter('state');

    $("#regions").submit(function(e){
        return false;
    });

    $("#advanced").submit(function(e){
        return false;
    });

    $('#region-dropdown option[value="sfo2"]').attr("selected", true);

    $('.go-btn').click(function() {
        var selected = $('#region-dropdown option:selected');
        window.location.replace("/install?region=" + selected.val());
    });
});

function removeVPN(name) {
    $('.loader').show();
    console.log("Called: ", name);
    $.get("/delete?droplet=" + name, function(resp) {
        console.log("Delete response: ", resp);
        window.location.replace("/dashboard?delete=true");
        $('.loader').hide();
    });
}


text:
function getParameterByName(name, url) {
    if (!url) {
        url = window.location.hash.replace("#", "?");
    }
    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, " "));
}

$(document).ready(function() {
    var access_token = getParameterByName('access_token');
    var state = getParameterByName('state');
    window.location.replace("/dashboard?access_token=" + access_token + "&state=" + state);
});


text:
$(document).ready(function() {
    var getUrlParameter = function getUrlParameter(sParam) {
        var sPageURL = decodeURIComponent(window.location.search.substring(1)),
            sURLVariables = sPageURL.split('&'),
            sParameterName,
            i;

        for (i = 0; i < sURLVariables.length; i++) {
            sParameterName = sURLVariables[i].split('=');

            if (sParameterName[0] === sParam) {
                return sParameterName[1] === undefined ? true : sParameterName[1];
            }
        }
    };

    $('#showpass').click(function () {
        $('#password').slideToggle();
    });

    $('#exit').click(function() {
        $.get("/exit", function(resp) {
            return;
        });
    });
});


text:
// Copyright 2015 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

// +build !appengine

package appengine

import (
	"golang.org/x/net/context"

	"google.golang.org/appengine/internal"
)

// BackgroundContext returns a context not associated with a request.
// This should only be used when not servicing a request.
// This only works in App Engine "flexible environment".
func BackgroundContext() context.Context {
	return internal.BackgroundContext()
}


text:
// Copyright 2014 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package appengine

import (
	"testing"

	"golang.org/x/net/context"
)

func TestNamespaceValidity(t *testing.T) {
	testCases := []struct {
		namespace string
		ok        bool
	}{
		// data from Python's namespace_manager_test.py
		{"", true},
		{"__a.namespace.123__", true},
		{"-_A....NAMESPACE-_", true},
		{"-", true},
		{".", true},
		{".-", true},

		{"?", false},
		{"+", false},
		{"!", false},
		{" ", false},
	}
	for _, tc := range testCases {
		_, err := Namespace(context.Background(), tc.namespace)
		if err == nil && !tc.ok {
			t.Errorf("Namespace %q should be rejected, but wasn't", tc.namespace)
		} else if err != nil && tc.ok {
			t.Errorf("Namespace %q should be accepted, but wasn't", tc.namespace)
		}
	}
}


text:
// Copyright 2014 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package appengine

import (
	"testing"
)

func TestValidGeoPoint(t *testing.T) {
	testCases := []struct {
		desc string
		pt   GeoPoint
		want bool
	}{
		{
			"valid",
			GeoPoint{67.21, 13.37},
			true,
		},
		{
			"high lat",
			GeoPoint{-90.01, 13.37},
			false,
		},
		{
			"low lat",
			GeoPoint{90.01, 13.37},
			false,
		},
		{
			"high lng",
			GeoPoint{67.21, 182},
			false,
		},
		{
			"low lng",
			GeoPoint{67.21, -181},
			false,
		},
	}

	for _, tc := range testCases {
		if got := tc.pt.Valid(); got != tc.want {
			t.Errorf("%s: got %v, want %v", tc.desc, got, tc.want)
		}
	}
}


text:
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

// Package appengine provides basic functionality for Google App Engine.
//
// For more information on how to write Go apps for Google App Engine, see:
// https://cloud.google.com/appengine/docs/go/
package appengine // import "google.golang.org/appengine"

import (
	"net/http"

	"github.com/golang/protobuf/proto"
	"golang.org/x/net/context"

	"google.golang.org/appengine/internal"
)

// The gophers party all night; the rabbits provide the beats.

// Main is the principal entry point for an app running in App Engine.
//
// On App Engine Flexible it installs a trivial health checker if one isn't
// already registered, and starts listening on port 8080 (overridden by the
// $PORT environment variable).
//
// See https://cloud.google.com/appengine/docs/flexible/custom-runtimes#health_check_requests
// for details on how to do your own health checking.
//
// Main is not yet supported on App Engine Standard.
//
// Main never returns.
//
// Main is designed so that the app's main package looks like this:
//
//      package main
//
//      import (
//              "google.golang.org/appengine"
//
//              _ "myapp/package0"
//              _ "myapp/package1"
//      )
//
//      func main() {
//              appengine.Main()
//      }
//
// The "myapp/packageX" packages are expected to register HTTP handlers
// in their init functions.
func Main() {
	internal.Main()
}

// IsDevAppServer reports whether the App Engine app is running in the
// development App Server.
func IsDevAppServer() bool {
	return internal.IsDevAppServer()
}

// NewContext returns a context for an in-flight HTTP request.
// This function is cheap.
func NewContext(req *http.Request) context.Context {
	return WithContext(context.Background(), req)
}

// WithContext returns a copy of the parent context
// and associates it with an in-flight HTTP request.
// This function is cheap.
func WithContext(parent context.Context, req *http.Request) context.Context {
	return internal.WithContext(parent, req)
}

// TODO(dsymonds): Add a Call function here? Otherwise other packages can't access internal.Call.

// BlobKey is a key for a blobstore blob.
//
// Conceptually, this type belongs in the blobstore package, but it lives in
// the appengine package to avoid a circular dependency: blobstore depends on
// datastore, and datastore needs to refer to the BlobKey type.
type BlobKey string

// GeoPoint represents a location as latitude/longitude in degrees.
type GeoPoint struct {
	Lat, Lng float64
}

// Valid returns whether a GeoPoint is within [-90, 90] latitude and [-180, 180] longitude.
func (g GeoPoint) Valid() bool {
	return -90 <= g.Lat && g.Lat <= 90 && -180 <= g.Lng && g.Lng <= 180
}

// APICallFunc defines a function type for handling an API call.
// See WithCallOverride.
type APICallFunc func(ctx context.Context, service, method string, in, out proto.Message) error

// WithAPICallFunc returns a copy of the parent context
// that will cause API calls to invoke f instead of their normal operation.
//
// This is intended for advanced users only.
func WithAPICallFunc(ctx context.Context, f APICallFunc) context.Context {
	return internal.WithCallOverride(ctx, internal.CallOverrideFunc(f))
}

// APICall performs an API call.
//
// This is not intended for general use; it is exported for use in conjunction
// with WithAPICallFunc.
func APICall(ctx context.Context, service, method string, in, out proto.Message) error {
	return internal.Call(ctx, service, method, in, out)
}


text:
// Copyright 2013 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package appengine

import "golang.org/x/net/context"

// IsTimeoutError reports whether err is a timeout error.
func IsTimeoutError(err error) bool {
	if err == context.DeadlineExceeded {
		return true
	}
	if t, ok := err.(interface {
		IsTimeout() bool
	}); ok {
		return t.IsTimeout()
	}
	return false
}


text:
// Copyright 2012 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package appengine

import (
	"fmt"
	"regexp"

	"golang.org/x/net/context"

	"google.golang.org/appengine/internal"
)

// Namespace returns a replacement context that operates within the given namespace.
func Namespace(c context.Context, namespace string) (context.Context, error) {
	if !validNamespace.MatchString(namespace) {
		return nil, fmt.Errorf("appengine: namespace %q does not match /%s/", namespace, validNamespace)
	}
	return internal.NamespacedContext(c, namespace), nil
}

// validNamespace matches valid namespace names.
var validNamespace = regexp.MustCompile(`^[0-9A-Za-z._-]{0,100}$`)


text:
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package appengine

import (
	"time"

	"golang.org/x/net/context"

	"google.golang.org/appengine/internal"
	pb "google.golang.org/appengine/internal/app_identity"
	modpb "google.golang.org/appengine/internal/modules"
)

// AppID returns the application ID for the current application.
// The string will be a plain application ID (e.g. "appid"), with a
// domain prefix for custom domain deployments (e.g. "example.com:appid").
func AppID(c context.Context) string { return internal.AppID(c) }

// DefaultVersionHostname returns the standard hostname of the default version
// of the current application (e.g. "my-app.appspot.com"). This is suitable for
// use in constructing URLs.
func DefaultVersionHostname(c context.Context) string {
	return internal.DefaultVersionHostname(c)
}

// ModuleName returns the module name of the current instance.
func ModuleName(c context.Context) string {
	return internal.ModuleName(c)
}

// ModuleHostname returns a hostname of a module instance.
// If module is the empty string, it refers to the module of the current instance.
// If version is empty, it refers to the version of the current instance if valid,
// or the default version of the module of the current instance.
// If instance is empty, ModuleHostname returns the load-balancing hostname.
func ModuleHostname(c context.Context, module, version, instance string) (string, error) {
	req := &modpb.GetHostnameRequest{}
	if module != "" {
		req.Module = &module
	}
	if version != "" {
		req.Version = &version
	}
	if instance != "" {
		req.Instance = &instance
	}
	res := &modpb.GetHostnameResponse{}
	if err := internal.Call(c, "modules", "GetHostname", req, res); err != nil {
		return "", err
	}
	return *res.Hostname, nil
}

// VersionID returns the version ID for the current application.
// It will be of the form "X.Y", where X is specified in app.yaml,
// and Y is a number generated when each version of the app is uploaded.
// It does not include a module name.
func VersionID(c context.Context) string { return internal.VersionID(c) }

// InstanceID returns a mostly-unique identifier for this instance.
func InstanceID() string { return internal.InstanceID() }

// Datacenter returns an identifier for the datacenter that the instance is running in.
func Datacenter(c context.Context) string { return internal.Datacenter(c) }

// ServerSoftware returns the App Engine release version.
// In production, it looks like "Google App Engine/X.Y.Z".
// In the development appserver, it looks like "Development/X.Y".
func ServerSoftware() string { return internal.ServerSoftware() }

// RequestID returns a string that uniquely identifies the request.
func RequestID(c context.Context) string { return internal.RequestID(c) }

// AccessToken generates an OAuth2 access token for the specified scopes on
// behalf of service account of this application. This token will expire after
// the returned time.
func AccessToken(c context.Context, scopes ...string) (token string, expiry time.Time, err error) {
	req := &pb.GetAccessTokenRequest{Scope: scopes}
	res := &pb.GetAccessTokenResponse{}

	err = internal.Call(c, "app_identity_service", "GetAccessToken", req, res)
	if err != nil {
		return "", time.Time{}, err
	}
	return res.GetAccessToken(), time.Unix(res.GetExpirationTime(), 0), nil
}

// Certificate represents a public certificate for the app.
type Certificate struct {
	KeyName string
	Data    []byte // PEM-encoded X.509 certificate
}

// PublicCertificates retrieves the public certificates for the app.
// They can be used to verify a signature returned by SignBytes.
func PublicCertificates(c context.Context) ([]Certificate, error) {
	req := &pb.GetPublicCertificateForAppRequest{}
	res := &pb.GetPublicCertificateForAppResponse{}
	if err := internal.Call(c, "app_identity_service", "GetPublicCertificatesForApp", req, res); err != nil {
		return nil, err
	}
	var cs []Certificate
	for _, pc := range res.PublicCertificateList {
		cs = append(cs, Certificate{
			KeyName: pc.GetKeyName(),
			Data:    []byte(pc.GetX509CertificatePem()),
		})
	}
	return cs, nil
}

// ServiceAccount returns a string representing the service account name, in
// the form of an email address (typically app_id@appspot.gserviceaccount.com).
func ServiceAccount(c context.Context) (string, error) {
	req := &pb.GetServiceAccountNameRequest{}
	res := &pb.GetServiceAccountNameResponse{}

	err := internal.Call(c, "app_identity_service", "GetServiceAccountName", req, res)
	if err != nil {
		return "", err
	}
	return res.GetServiceAccountName(), err
}

// SignBytes signs bytes using a private key unique to your application.
func SignBytes(c context.Context, bytes []byte) (keyName string, signature []byte, err error) {
	req := &pb.SignForAppRequest{BytesToSign: bytes}
	res := &pb.SignForAppResponse{}

	if err := internal.Call(c, "app_identity_service", "SignForApp", req, res); err != nil {
		return "", nil, err
	}
	return res.GetKeyName(), res.GetSignatureBytes(), nil
}

func init() {
	internal.RegisterErrorCodeMap("app_identity_service", pb.AppIdentityServiceError_ErrorCode_name)
	internal.RegisterErrorCodeMap("modules", modpb.ModulesServiceError_ErrorCode_name)
}


text:
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

// This file provides error functions for common API failure modes.

package appengine

import (
	"fmt"

	"google.golang.org/appengine/internal"
)

// IsOverQuota reports whether err represents an API call failure
// due to insufficient available quota.
func IsOverQuota(err error) bool {
	callErr, ok := err.(*internal.CallError)
	return ok && callErr.Code == 4
}

// MultiError is returned by batch operations when there are errors with
// particular elements. Errors will be in a one-to-one correspondence with
// the input elements; successful elements will have a nil entry.
type MultiError []error

func (m MultiError) Error() string {
	s, n := "", 0
	for _, e := range m {
		if e != nil {
			if n == 0 {
				s = e.Error()
			}
			n++
		}
	}
	switch n {
	case 0:
		return "(0 errors)"
	case 1:
		return s
	case 2:
		return s + " (and 1 other error)"
	}
	return fmt.Sprintf("%s (and %d other errors)", s, n-1)
}


text:
# Go App Engine packages

[![Build Status](https://travis-ci.org/golang/appengine.svg)](https://travis-ci.org/golang/appengine)

This repository supports the Go runtime on App Engine,
including both the standard App Engine and the
"App Engine flexible environment" (formerly known as "Managed VMs").
It provides APIs for interacting with App Engine services.
Its canonical import path is `google.golang.org/appengine`.

See https://cloud.google.com/appengine/docs/go/
for more information.

File issue reports and feature requests on the [Google App Engine issue
tracker](https://code.google.com/p/googleappengine/issues/entry?template=Go%20defect).

## Directory structure
The top level directory of this repository is the `appengine` package. It
contains the
basic APIs (e.g. `appengine.NewContext`) that apply across APIs. Specific API
packages are in subdirectories (e.g. `datastore`).

There is an `internal` subdirectory that contains service protocol buffers,
plus packages required for connectivity to make API calls. App Engine apps
should not directly import any package under `internal`.

## Updating a Go App Engine app

This section describes how to update an older Go App Engine app to use
these packages. A provided tool, `aefix`, can help automate steps 2 and 3
(run `go get google.golang.org/appengine/cmd/aefix` to install it), but
read the details below since `aefix` can't perform all the changes.

### 1. Update YAML files (App Engine flexible environment / Managed VMs only)

The `app.yaml` file (and YAML files for modules) should have these new lines added:
```
vm: true
```
See https://cloud.google.com/appengine/docs/go/modules/#Go_Instance_scaling_and_class for details.

### 2. Update import paths

The import paths for App Engine packages are now fully qualified, based at `google.golang.org/appengine`.
You will need to update your code to use import paths starting with that; for instance,
code importing `appengine/datastore` will now need to import `google.golang.org/appengine/datastore`.

### 3. Update code using deprecated, removed or modified APIs

Most App Engine services are available with exactly the same API.
A few APIs were cleaned up, and some are not available yet.
This list summarises the differences:

* `appengine.Context` has been replaced with the `Context` type from `golang.org/x/net/context`.
* Logging methods that were on `appengine.Context` are now functions in `google.golang.org/appengine/log`.
* `appengine.Timeout` has been removed. Use `context.WithTimeout` instead.
* `appengine.Datacenter` now takes a `context.Context` argument.
* `datastore.PropertyLoadSaver` has been simplified to use slices in place of channels.
* `delay.Call` now returns an error.
* `search.FieldLoadSaver` now handles document metadata.
* `urlfetch.Transport` no longer has a Deadline field; set a deadline on the
  `context.Context` instead.
* `aetest` no longer declares its own Context type, and uses the standard one instead.
* `taskqueue.QueueStats` no longer takes a maxTasks argument. That argument has been
  deprecated and unused for a long time.
* `appengine.BackendHostname` and `appengine.BackendInstance` were for the deprecated backends feature.
  Use `appengine.ModuleHostname`and `appengine.ModuleName` instead.
* Most of `appengine/file` and parts of `appengine/blobstore` are deprecated.
  Use [Google Cloud Storage](https://godoc.org/cloud.google.com/go/storage) if the
  feature you require is not present in the new
  [blobstore package](https://google.golang.org/appengine/blobstore).
* `appengine/socket` is not required on App Engine flexible environment / Managed VMs.
  Use the standard `net` package instead.


text:
// Copyright 2014 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package memcache

import (
	"fmt"
	"testing"

	"google.golang.org/appengine"
	"google.golang.org/appengine/internal/aetesting"
	pb "google.golang.org/appengine/internal/memcache"
)

var errRPC = fmt.Errorf("RPC error")

func TestGetRequest(t *testing.T) {
	serviceCalled := false
	apiKey := "lyric"

	c := aetesting.FakeSingleContext(t, "memcache", "Get", func(req *pb.MemcacheGetRequest, _ *pb.MemcacheGetResponse) error {
		// Test request.
		if n := len(req.Key); n != 1 {
			t.Errorf("got %d want 1", n)
			return nil
		}
		if k := string(req.Key[0]); k != apiKey {
			t.Errorf("got %q want %q", k, apiKey)
		}

		serviceCalled = true
		return nil
	})

	// Test the "forward" path from the API call parameters to the
	// protobuf request object. (The "backward" path from the
	// protobuf response object to the API call response,
	// including the error response, are handled in the next few
	// tests).
	Get(c, apiKey)
	if !serviceCalled {
		t.Error("Service was not called as expected")
	}
}

func TestGetResponseHit(t *testing.T) {
	key := "lyric"
	value := "Where the buffalo roam"

	c := aetesting.FakeSingleContext(t, "memcache", "Get", func(_ *pb.MemcacheGetRequest, res *pb.MemcacheGetResponse) error {
		res.Item = []*pb.MemcacheGetResponse_Item{
			{Key: []byte(key), Value: []byte(value)},
		}
		return nil
	})
	apiItem, err := Get(c, key)
	if apiItem == nil || apiItem.Key != key || string(apiItem.Value) != value {
		t.Errorf("got %q, %q want {%q,%q}, nil", apiItem, err, key, value)
	}
}

func TestGetResponseMiss(t *testing.T) {
	c := aetesting.FakeSingleContext(t, "memcache", "Get", func(_ *pb.MemcacheGetRequest, res *pb.MemcacheGetResponse) error {
		// don't fill in any of the response
		return nil
	})
	_, err := Get(c, "something")
	if err != ErrCacheMiss {
		t.Errorf("got %v want ErrCacheMiss", err)
	}
}

func TestGetResponseRPCError(t *testing.T) {
	c := aetesting.FakeSingleContext(t, "memcache", "Get", func(_ *pb.MemcacheGetRequest, res *pb.MemcacheGetResponse) error {
		return errRPC
	})

	if _, err := Get(c, "something"); err != errRPC {
		t.Errorf("got %v want errRPC", err)
	}
}

func TestAddRequest(t *testing.T) {
	var apiItem = &Item{
		Key:   "lyric",
		Value: []byte("Oh, give me a home"),
	}

	serviceCalled := false

	c := aetesting.FakeSingleContext(t, "memcache", "Set", func(req *pb.MemcacheSetRequest, _ *pb.MemcacheSetResponse) error {
		// Test request.
		pbItem := req.Item[0]
		if k := string(pbItem.Key); k != apiItem.Key {
			t.Errorf("got %q want %q", k, apiItem.Key)
		}
		if v := string(apiItem.Value); v != string(pbItem.Value) {
			t.Errorf("got %q want %q", v, string(pbItem.Value))
		}
		if p := *pbItem.SetPolicy; p != pb.MemcacheSetRequest_ADD {
			t.Errorf("got %v want %v", p, pb.MemcacheSetRequest_ADD)
		}

		serviceCalled = true
		return nil
	})

	Add(c, apiItem)
	if !serviceCalled {
		t.Error("Service was not called as expected")
	}
}

func TestAddResponseStored(t *testing.T) {
	c := aetesting.FakeSingleContext(t, "memcache", "Set", func(_ *pb.MemcacheSetRequest, res *pb.MemcacheSetResponse) error {
		res.SetStatus = []pb.MemcacheSetResponse_SetStatusCode{pb.MemcacheSetResponse_STORED}
		return nil
	})

	if err := Add(c, &Item{}); err != nil {
		t.Errorf("got %v want nil", err)
	}
}

func TestAddResponseNotStored(t *testing.T) {
	c := aetesting.FakeSingleContext(t, "memcache", "Set", func(_ *pb.MemcacheSetRequest, res *pb.MemcacheSetResponse) error {
		res.SetStatus = []pb.MemcacheSetResponse_SetStatusCode{pb.MemcacheSetResponse_NOT_STORED}
		return nil
	})

	if err := Add(c, &Item{}); err != ErrNotStored {
		t.Errorf("got %v want ErrNotStored", err)
	}
}

func TestAddResponseError(t *testing.T) {
	c := aetesting.FakeSingleContext(t, "memcache", "Set", func(_ *pb.MemcacheSetRequest, res *pb.MemcacheSetResponse) error {
		res.SetStatus = []pb.MemcacheSetResponse_SetStatusCode{pb.MemcacheSetResponse_ERROR}
		return nil
	})

	if err := Add(c, &Item{}); err != ErrServerError {
		t.Errorf("got %v want ErrServerError", err)
	}
}

func TestAddResponseRPCError(t *testing.T) {
	c := aetesting.FakeSingleContext(t, "memcache", "Set", func(_ *pb.MemcacheSetRequest, res *pb.MemcacheSetResponse) error {
		return errRPC
	})

	if err := Add(c, &Item{}); err != errRPC {
		t.Errorf("got %v want errRPC", err)
	}
}

func TestSetRequest(t *testing.T) {
	var apiItem = &Item{
		Key:   "lyric",
		Value: []byte("Where the buffalo roam"),
	}

	serviceCalled := false

	c := aetesting.FakeSingleContext(t, "memcache", "Set", func(req *pb.MemcacheSetRequest, _ *pb.MemcacheSetResponse) error {
		// Test request.
		if n := len(req.Item); n != 1 {
			t.Errorf("got %d want 1", n)
			return nil
		}
		pbItem := req.Item[0]
		if k := string(pbItem.Key); k != apiItem.Key {
			t.Errorf("got %q want %q", k, apiItem.Key)
		}
		if v := string(pbItem.Value); v != string(apiItem.Value) {
			t.Errorf("got %q want %q", v, string(apiItem.Value))
		}
		if p := *pbItem.SetPolicy; p != pb.MemcacheSetRequest_SET {
			t.Errorf("got %v want %v", p, pb.MemcacheSetRequest_SET)
		}

		serviceCalled = true
		return nil
	})

	Set(c, apiItem)
	if !serviceCalled {
		t.Error("Service was not called as expected")
	}
}

func TestSetResponse(t *testing.T) {
	c := aetesting.FakeSingleContext(t, "memcache", "Set", func(_ *pb.MemcacheSetRequest, res *pb.MemcacheSetResponse) error {
		res.SetStatus = []pb.MemcacheSetResponse_SetStatusCode{pb.MemcacheSetResponse_STORED}
		return nil
	})

	if err := Set(c, &Item{}); err != nil {
		t.Errorf("got %v want nil", err)
	}
}

func TestSetResponseError(t *testing.T) {
	c := aetesting.FakeSingleContext(t, "memcache", "Set", func(_ *pb.MemcacheSetRequest, res *pb.MemcacheSetResponse) error {
		res.SetStatus = []pb.MemcacheSetResponse_SetStatusCode{pb.MemcacheSetResponse_ERROR}
		return nil
	})

	if err := Set(c, &Item{}); err != ErrServerError {
		t.Errorf("got %v want ErrServerError", err)
	}
}

func TestNamespaceResetting(t *testing.T) {
	namec := make(chan *string, 1)
	c0 := aetesting.FakeSingleContext(t, "memcache", "Get", func(req *pb.MemcacheGetRequest, res *pb.MemcacheGetResponse) error {
		namec <- req.NameSpace
		return errRPC
	})

	// Check that wrapping c0 in a namespace twice works correctly.
	c1, err := appengine.Namespace(c0, "A")
	if err != nil {
		t.Fatalf("appengine.Namespace: %v", err)
	}
	c2, err := appengine.Namespace(c1, "") // should act as the original context
	if err != nil {
		t.Fatalf("appengine.Namespace: %v", err)
	}

	Get(c0, "key")
	if ns := <-namec; ns != nil {
		t.Errorf(`Get with c0: ns = %q, want nil`, *ns)
	}

	Get(c1, "key")
	if ns := <-namec; ns == nil {
		t.Error(`Get with c1: ns = nil, want "A"`)
	} else if *ns != "A" {
		t.Errorf(`Get with c1: ns = %q, want "A"`, *ns)
	}

	Get(c2, "key")
	if ns := <-namec; ns != nil {
		t.Errorf(`Get with c2: ns = %q, want nil`, *ns)
	}
}

func TestGetMultiEmpty(t *testing.T) {
	serviceCalled := false
	c := aetesting.FakeSingleContext(t, "memcache", "Get", func(req *pb.MemcacheGetRequest, _ *pb.MemcacheGetResponse) error {
		serviceCalled = true
		return nil
	})

	// Test that the Memcache service is not called when
	// GetMulti is passed an empty slice of keys.
	GetMulti(c, []string{})
	if serviceCalled {
		t.Error("Service was called but should not have been")
	}
}