Procházet zdrojové kódy

Removed excess code from v1.115 repo

Removed minor code that lead to build error on linux
TC pushbot 5 před 3 roky
rodič
revize
b471dd2af6

+ 0 - 12
src/mod/arsm/README.md

@@ -1,12 +0,0 @@
-# Arozos Remote Support & Management Module
-
-
-## Introduction
-Arozos Remote Support & Management Module or ARSM module, is a module that handles remote maintaince and support
-for downstream arozos powered system on client that is located under NAT routers
-
-## Usage
-This is not a module that can be directly acceessed. See the submodule under this module for more information
-and documentation.
-
-s

+ 0 - 241
src/mod/arsm/aecron/aecron.go

@@ -1,241 +0,0 @@
-package aecron
-
-import (
-	"encoding/json"
-	"io/ioutil"
-	"log"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"runtime"
-	"strings"
-	"time"
-
-	"imuslab.com/arozos/mod/agi"
-	"imuslab.com/arozos/mod/user"
-)
-
-/*
-	ArOZ Emulated Crontab
-	author: tobychui
-
-	This is not actually a crontab but something similar that provide
-	timered operations for executing commands in agi or bash in an interval
-	bases
-
-*/
-
-type Job struct {
-	Name              string //The name of this job
-	Creator           string //The creator of this job. When execute, this user permission will be used
-	Description       string //Job description, can be empty
-	Admin             bool   //If the creator has admin permission during the creation of this job. If this doesn't match with the runtime instance, this job wille be skipped
-	ExecutionInterval int64  //Execuation interval in seconds
-	BaseTime          int64  //Exeuction basetime. The next interval is calculated using (current time - base time ) % execution interval
-	ScriptFile        string //The script file being called. Can be an agi script (.agi / .js) or shell script (.bat or .sh)
-}
-
-type Aecron struct {
-	jobs        []*Job
-	cronfile    string
-	userHandler *user.UserHandler
-	gateway     *agi.Gateway
-	ticker      chan bool
-}
-
-var (
-	logFolder string = "./system/aecron/"
-)
-
-func NewArozEmulatedCrontab(userHandler *user.UserHandler, gateway *agi.Gateway, cronfile string) (*Aecron, error) {
-	if !fileExists(cronfile) {
-		//Cronfile not exists. Create it
-		emptyJobList := []*Job{}
-		ls, _ := json.Marshal(emptyJobList)
-		err := ioutil.WriteFile(cronfile, ls, 0755)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	//Load previous jobs from file
-	jobs, err := loadJobsFromFile(cronfile)
-	if err != nil {
-		return nil, err
-	}
-
-	//Create the ArOZ Emulated Crontask
-	aecron := Aecron{
-		jobs:        jobs,
-		userHandler: userHandler,
-		gateway:     gateway,
-		cronfile:    cronfile,
-	}
-
-	//Create log folder
-	os.MkdirAll(logFolder, 0755)
-
-	//Start the cronjob at 1 minute ticker interval
-	go func() {
-		//Delay start: Wait until seconds = 0
-		for time.Now().Unix()%60 > 0 {
-			time.Sleep(500 * time.Millisecond)
-		}
-		stopChannel := aecron.createTicker(1 * time.Minute)
-		aecron.ticker = stopChannel
-		log.Println("Emulated Crontab Started - Scheduling Tasks")
-	}()
-
-	//Return the crontask
-	return &aecron, nil
-}
-
-//Load a list of jobs from file
-func loadJobsFromFile(cronfile string) ([]*Job, error) {
-	//Try to read the cronfile
-	filecontent, err := ioutil.ReadFile(cronfile)
-	if err != nil {
-		return []*Job{}, err
-	}
-
-	//Phrase the cronfile
-	prevousJobs := []Job{}
-	err = json.Unmarshal(filecontent, &prevousJobs)
-	if err != nil {
-		return []*Job{}, err
-	}
-
-	//Convert the json objets to pointer for easy changing by other process
-	jobsPointers := []*Job{}
-	for _, thisJob := range prevousJobs {
-		var newJobPointer Job = thisJob
-		jobsPointers = append(jobsPointers, &newJobPointer)
-	}
-
-	return jobsPointers, nil
-}
-
-func (a *Aecron) createTicker(duration time.Duration) chan bool {
-	ticker := time.NewTicker(duration)
-	stop := make(chan bool, 1)
-
-	go func() {
-		defer log.Println("Aecron Stopped")
-		for {
-			select {
-			case <-ticker.C:
-				//Run jobs
-				for _, thisJob := range a.jobs {
-					if (time.Now().Unix()-thisJob.BaseTime)%thisJob.ExecutionInterval == 0 {
-						//Execute this job
-						scriptFile := thisJob.ScriptFile
-						if !fileExists(scriptFile) {
-							//This job no longer exists in the file system. Remove it
-							a.RemoveJobFromScheduleList(thisJob.Name)
-						}
-						clonedJobStructure := *thisJob
-						ext := filepath.Ext(scriptFile)
-						if ext == ".js" || ext == ".agi" {
-							//Run using AGI interface in go routine
-							go func(thisJob Job) {
-								userinfo, err := a.userHandler.GetUserInfoFromUsername(thisJob.Creator)
-								if err != nil {
-									//This user not exists. Skip this script
-									cronlog("[ERROR] User not exists: " + thisJob.Creator + ". Skipping scheduled job: " + thisJob.Name + ".")
-									return
-								}
-
-								//Run the script with this user scope
-								resp, err := a.gateway.ExecuteAGIScriptAsUser(thisJob.ScriptFile, userinfo)
-								if err != nil {
-									cronlog("[ERROR] " + thisJob.Name + " " + err.Error())
-								} else {
-									cronlog(thisJob.Name + " " + resp)
-								}
-							}(clonedJobStructure)
-
-						} else if ext == ".bat" || ext == ".sh" {
-							//Run as shell script
-							go func(thisJob Job) {
-								scriptPath := thisJob.ScriptFile
-								if runtime.GOOS == "windows" {
-									scriptPath = strings.ReplaceAll(filepath.ToSlash(scriptPath), "/", "\\")
-								}
-								cmd := exec.Command(scriptPath)
-								out, err := cmd.CombinedOutput()
-								if err != nil {
-									cronlog("[ERROR] " + thisJob.Name + " " + err.Error() + " => " + string(out))
-								}
-								cronlog(thisJob.Name + " " + string(out))
-							}(clonedJobStructure)
-						} else {
-							//Unknown script file. Ignore this
-							log.Println("This extension is not yet supported: ", ext)
-						}
-					}
-				}
-			case <-stop:
-				return
-			}
-		}
-	}()
-
-	return stop
-}
-
-func (a *Aecron) Close() {
-	if a.ticker != nil {
-		//Stop the ticker
-		a.ticker <- true
-	}
-}
-
-func (a *Aecron) GetScheduledJobByName(name string) *Job {
-	for _, thisJob := range a.jobs {
-		if thisJob.Name == name {
-			return thisJob
-		}
-	}
-
-	return nil
-}
-
-func (a *Aecron) RemoveJobFromScheduleList(taskName string) {
-	newJobSlice := []*Job{}
-	for _, j := range a.jobs {
-		if j.Name != taskName {
-			thisJob := j
-			newJobSlice = append(newJobSlice, thisJob)
-		}
-	}
-	a.jobs = newJobSlice
-}
-
-func (a *Aecron) JobExists(name string) bool {
-	targetJob := a.GetScheduledJobByName(name)
-	if targetJob == nil {
-		return false
-	} else {
-		return true
-	}
-}
-
-//Write the output to log file. Default to ./system/aecron/{date}.log
-func cronlog(message string) {
-	currentTime := time.Now()
-	timestamp := currentTime.Format("2006-01-02 15:04:05")
-	message = timestamp + " " + message
-	currentLogFile := filepath.ToSlash(filepath.Clean(logFolder)) + "/" + time.Now().Format("01-02-2006") + ".log"
-	f, err := os.OpenFile(currentLogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
-	if err != nil {
-		//Unable to write to file. Log to STDOUT instead
-		log.Println(message)
-		return
-	}
-	if _, err := f.WriteString(message + "\n"); err != nil {
-		log.Println(message)
-		return
-	}
-	defer f.Close()
-
-}

+ 0 - 171
src/mod/arsm/aecron/common.go

@@ -1,171 +0,0 @@
-package aecron
-
-import (
-	"bufio"
-	"encoding/base64"
-	"errors"
-	"io/ioutil"
-	"log"
-	"net/http"
-	"os"
-	"time"
-)
-
-/*
-	SYSTEM COMMON FUNCTIONS
-
-	This is a system function that put those we usually use function but not belongs to
-	any module / system.
-
-	E.g. fileExists / IsDir etc
-
-*/
-
-/*
-	Basic Response Functions
-
-	Send response with ease
-*/
-//Send text response with given w and message as string
-func sendTextResponse(w http.ResponseWriter, msg string) {
-	w.Write([]byte(msg))
-}
-
-//Send JSON response, with an extra json header
-func sendJSONResponse(w http.ResponseWriter, json string) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Write([]byte(json))
-}
-
-func sendErrorResponse(w http.ResponseWriter, errMsg string) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
-}
-
-func sendOK(w http.ResponseWriter) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Write([]byte("\"OK\""))
-}
-
-/*
-	The paramter move function (mv)
-
-	You can find similar things in the PHP version of ArOZ Online Beta. You need to pass in
-	r (HTTP Request Object)
-	getParamter (string, aka $_GET['This string])
-
-	Will return
-	Paramter string (if any)
-	Error (if error)
-
-*/
-func mv(r *http.Request, getParamter string, postMode bool) (string, error) {
-	if postMode == false {
-		//Access the paramter via GET
-		keys, ok := r.URL.Query()[getParamter]
-
-		if !ok || len(keys[0]) < 1 {
-			//log.Println("Url Param " + getParamter +" is missing")
-			return "", errors.New("GET paramter " + getParamter + " not found or it is empty")
-		}
-
-		// Query()["key"] will return an array of items,
-		// we only want the single item.
-		key := keys[0]
-		return string(key), nil
-	} else {
-		//Access the parameter via POST
-		r.ParseForm()
-		x := r.Form.Get(getParamter)
-		if len(x) == 0 || x == "" {
-			return "", errors.New("POST paramter " + getParamter + " not found or it is empty")
-		}
-		return string(x), nil
-	}
-
-}
-
-func stringInSlice(a string, list []string) bool {
-	for _, b := range list {
-		if b == a {
-			return true
-		}
-	}
-	return false
-}
-
-func fileExists(filename string) bool {
-	_, err := os.Stat(filename)
-	if os.IsNotExist(err) {
-		return false
-	}
-	return true
-}
-
-func isDir(path string) bool {
-	if fileExists(path) == false {
-		return false
-	}
-	fi, err := os.Stat(path)
-	if err != nil {
-		log.Fatal(err)
-		return false
-	}
-	switch mode := fi.Mode(); {
-	case mode.IsDir():
-		return true
-	case mode.IsRegular():
-		return false
-	}
-	return false
-}
-
-func inArray(arr []string, str string) bool {
-	for _, a := range arr {
-		if a == str {
-			return true
-		}
-	}
-	return false
-}
-
-func timeToString(targetTime time.Time) string {
-	return targetTime.Format("2006-01-02 15:04:05")
-}
-
-func loadImageAsBase64(filepath string) (string, error) {
-	if !fileExists(filepath) {
-		return "", errors.New("File not exists")
-	}
-	f, _ := os.Open(filepath)
-	reader := bufio.NewReader(f)
-	content, _ := ioutil.ReadAll(reader)
-	encoded := base64.StdEncoding.EncodeToString(content)
-	return string(encoded), nil
-}
-
-func pushToSliceIfNotExist(slice []string, newItem string) []string {
-	itemExists := false
-	for _, item := range slice {
-		if item == newItem {
-			itemExists = true
-		}
-	}
-
-	if !itemExists {
-		slice = append(slice, newItem)
-	}
-
-	return slice
-}
-
-func removeFromSliceIfExists(slice []string, target string) []string {
-	newSlice := []string{}
-	for _, item := range slice {
-		if item != target {
-			newSlice = append(newSlice, item)
-		}
-	}
-
-	return newSlice
-}

+ 0 - 237
src/mod/arsm/aecron/handlers.go

@@ -1,237 +0,0 @@
-package aecron
-
-import (
-	"encoding/json"
-	"io/ioutil"
-	"net/http"
-	"path/filepath"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/tidwall/pretty"
-)
-
-//List all the jobs related to the given user
-func (a *Aecron) HandleListJobs(w http.ResponseWriter, r *http.Request) {
-	userinfo, err := a.userHandler.GetUserInfoFromRequest(w, r)
-	if err != nil {
-		sendErrorResponse(w, "User not logged in")
-		return
-	}
-
-	//Get username from user info
-	username := userinfo.Username
-	isAdmin := userinfo.IsAdmin()
-
-	//Check if the user request list all
-	listAll := false
-	la, _ := mv(r, "listall", false)
-	if la == "true" && isAdmin {
-		listAll = true
-	}
-
-	//Find the scheduled task that belongs to this user
-	userCreatedJobs := []*Job{}
-
-	for _, thisJob := range a.jobs {
-		if listAll {
-			//List all the user jobs.
-			userCreatedJobs = append(userCreatedJobs, thisJob)
-		} else {
-			//Only list user's job
-			if thisJob.Creator == username {
-				userCreatedJobs = append(userCreatedJobs, thisJob)
-			}
-		}
-
-	}
-
-	//Return the values as json
-	js, _ := json.Marshal(userCreatedJobs)
-	sendJSONResponse(w, string(js))
-}
-
-func (a *Aecron) HandleAddJob(w http.ResponseWriter, r *http.Request) {
-	userinfo, err := a.userHandler.GetUserInfoFromRequest(w, r)
-	if err != nil {
-		sendErrorResponse(w, "User not logged in")
-		return
-	}
-
-	//Get required paramaters
-	taskName, err := mv(r, "name", true)
-	if err != nil {
-		sendErrorResponse(w, "Invalid task name")
-		return
-	}
-
-	//Check taskname length valid
-	if len(taskName) > 32 {
-		sendErrorResponse(w, "Task name must be shorter than 32 characters")
-		return
-	}
-
-	//Check if the name already existsed
-	for _, runningJob := range a.jobs {
-		if runningJob.Name == taskName {
-			sendErrorResponse(w, "Task Name already occupied")
-			return
-		}
-	}
-
-	scriptpath, err := mv(r, "path", true)
-	if err != nil {
-		sendErrorResponse(w, "Invalid script path")
-		return
-	}
-
-	//Can be empty
-	jobDescription, _ := mv(r, "desc", true)
-
-	realScriptPath, err := userinfo.VirtualPathToRealPath(scriptpath)
-	if err != nil {
-		sendErrorResponse(w, err.Error())
-		return
-	}
-
-	//Check if the user has permission to create this script
-	if filepath.Ext(realScriptPath) != ".js" && filepath.Ext(realScriptPath) != ".agi" {
-		//Require admin permission
-		if userinfo.IsAdmin() == false {
-			sendErrorResponse(w, "Admin permission required for scheduling non AGI script")
-			return
-		}
-	}
-
-	interval := int64(86400) //default 1 day in seconds
-	intervalString, err := mv(r, "interval", true)
-	if err != nil {
-		//Default 1 day
-
-	} else {
-		//Parse the intervalString into int
-		intervalInt, err := strconv.ParseInt(intervalString, 10, 64)
-		if err != nil {
-			//Failed to parse interval to int
-			sendErrorResponse(w, "Invalid interval")
-			return
-		}
-
-		interval = intervalInt
-	}
-
-	baseUnixTime := time.Now().Unix()
-	baseTimeString, err := mv(r, "base", true)
-	if err != nil {
-		//Use curent timestamp as base
-
-	} else {
-		baseTimeInt, err := strconv.Atoi(baseTimeString)
-		if err != nil {
-			//Failed to parse interval to int
-			sendErrorResponse(w, "Invalid Base Time")
-			return
-		}
-
-		baseUnixTime = int64(baseTimeInt)
-	}
-
-	//Create a new job
-	newJob := Job{
-		Name:              taskName,
-		Creator:           userinfo.Username,
-		Admin:             userinfo.IsAdmin(),
-		Description:       jobDescription,
-		ExecutionInterval: int64(interval),
-		BaseTime:          baseUnixTime,
-		ScriptFile:        realScriptPath,
-	}
-
-	//Write current job lists to file
-	a.jobs = append(a.jobs, &newJob)
-
-	js, _ := json.MarshalIndent(a.jobs, "", " ")
-
-	ioutil.WriteFile(a.cronfile, js, 0755)
-
-	//OK
-	sendOK(w)
-}
-
-func (a *Aecron) HandleJobRemoval(w http.ResponseWriter, r *http.Request) {
-	userinfo, err := a.userHandler.GetUserInfoFromRequest(w, r)
-	if err != nil {
-		sendErrorResponse(w, "User not logged in")
-		return
-	}
-
-	//Get required paramaters
-	taskName, err := mv(r, "name", true)
-	if err != nil {
-		sendErrorResponse(w, "Invalid task name")
-		return
-	}
-
-	//Check if Job exists
-	if !a.JobExists(taskName) {
-		//Job with that name not exists
-		sendErrorResponse(w, "Job not exists")
-		return
-	}
-
-	targetJob := a.GetScheduledJobByName(taskName)
-
-	//Job exists. Check if the job is created by the user.
-	//User can only remove job created by himself or all job is he is admin
-	allowRemove := false
-	if userinfo.IsAdmin() == false && targetJob.Creator == userinfo.Username {
-		allowRemove = true
-	} else if userinfo.IsAdmin() == true {
-		allowRemove = true
-	}
-
-	if !allowRemove {
-		sendErrorResponse(w, "Permission Denied")
-		return
-	}
-
-	//Ok. Remove Job from the list
-	a.RemoveJobFromScheduleList(taskName)
-
-	//Write current job lists to file
-	js, _ := json.Marshal(a.jobs)
-	js = []byte(pretty.Pretty(js))
-	ioutil.WriteFile(a.cronfile, js, 0755)
-
-	sendOK(w)
-}
-
-func (a *Aecron) HandleShowLog(w http.ResponseWriter, r *http.Request) {
-	filename, _ := mv(r, "filename", false)
-	if filename == "" {
-		//Show index
-		logFiles, _ := filepath.Glob(logFolder + "*.log")
-
-		//Convert all to linux syntax
-		linuxLogFiles := []string{}
-		for _, lf := range logFiles {
-			linuxLogFiles = append(linuxLogFiles, filepath.Base(lf))
-		}
-		js, _ := json.Marshal(linuxLogFiles)
-		sendJSONResponse(w, string(js))
-	} else {
-		//Show log content
-		filename = strings.ReplaceAll(filepath.ToSlash(filename), "/", "")
-		if fileExists(filepath.Join(logFolder, filename)) {
-			logContent, err := ioutil.ReadFile(filepath.Join(logFolder, filename))
-			if err != nil {
-				sendTextResponse(w, "Unable to load log file: "+filename)
-			} else {
-				sendTextResponse(w, string(logContent))
-			}
-		} else {
-			sendTextResponse(w, "Unable to load log file: "+filename)
-		}
-	}
-}

+ 0 - 171
src/mod/arsm/wsterminal/common.go

@@ -1,171 +0,0 @@
-package wsterminal
-
-import (
-	"bufio"
-	"encoding/base64"
-	"errors"
-	"io/ioutil"
-	"log"
-	"net/http"
-	"os"
-	"time"
-)
-
-/*
-	SYSTEM COMMON FUNCTIONS
-
-	This is a system function that put those we usually use function but not belongs to
-	any module / system.
-
-	E.g. fileExists / IsDir etc
-
-*/
-
-/*
-	Basic Response Functions
-
-	Send response with ease
-*/
-//Send text response with given w and message as string
-func sendTextResponse(w http.ResponseWriter, msg string) {
-	w.Write([]byte(msg))
-}
-
-//Send JSON response, with an extra json header
-func sendJSONResponse(w http.ResponseWriter, json string) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Write([]byte(json))
-}
-
-func sendErrorResponse(w http.ResponseWriter, errMsg string) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
-}
-
-func sendOK(w http.ResponseWriter) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Write([]byte("\"OK\""))
-}
-
-/*
-	The paramter move function (mv)
-
-	You can find similar things in the PHP version of ArOZ Online Beta. You need to pass in
-	r (HTTP Request Object)
-	getParamter (string, aka $_GET['This string])
-
-	Will return
-	Paramter string (if any)
-	Error (if error)
-
-*/
-func mv(r *http.Request, getParamter string, postMode bool) (string, error) {
-	if postMode == false {
-		//Access the paramter via GET
-		keys, ok := r.URL.Query()[getParamter]
-
-		if !ok || len(keys[0]) < 1 {
-			//log.Println("Url Param " + getParamter +" is missing")
-			return "", errors.New("GET paramter " + getParamter + " not found or it is empty")
-		}
-
-		// Query()["key"] will return an array of items,
-		// we only want the single item.
-		key := keys[0]
-		return string(key), nil
-	} else {
-		//Access the parameter via POST
-		r.ParseForm()
-		x := r.Form.Get(getParamter)
-		if len(x) == 0 || x == "" {
-			return "", errors.New("POST paramter " + getParamter + " not found or it is empty")
-		}
-		return string(x), nil
-	}
-
-}
-
-func stringInSlice(a string, list []string) bool {
-	for _, b := range list {
-		if b == a {
-			return true
-		}
-	}
-	return false
-}
-
-func fileExists(filename string) bool {
-	_, err := os.Stat(filename)
-	if os.IsNotExist(err) {
-		return false
-	}
-	return true
-}
-
-func isDir(path string) bool {
-	if fileExists(path) == false {
-		return false
-	}
-	fi, err := os.Stat(path)
-	if err != nil {
-		log.Fatal(err)
-		return false
-	}
-	switch mode := fi.Mode(); {
-	case mode.IsDir():
-		return true
-	case mode.IsRegular():
-		return false
-	}
-	return false
-}
-
-func inArray(arr []string, str string) bool {
-	for _, a := range arr {
-		if a == str {
-			return true
-		}
-	}
-	return false
-}
-
-func timeToString(targetTime time.Time) string {
-	return targetTime.Format("2006-01-02 15:04:05")
-}
-
-func loadImageAsBase64(filepath string) (string, error) {
-	if !fileExists(filepath) {
-		return "", errors.New("File not exists")
-	}
-	f, _ := os.Open(filepath)
-	reader := bufio.NewReader(f)
-	content, _ := ioutil.ReadAll(reader)
-	encoded := base64.StdEncoding.EncodeToString(content)
-	return string(encoded), nil
-}
-
-func pushToSliceIfNotExist(slice []string, newItem string) []string {
-	itemExists := false
-	for _, item := range slice {
-		if item == newItem {
-			itemExists = true
-		}
-	}
-
-	if !itemExists {
-		slice = append(slice, newItem)
-	}
-
-	return slice
-}
-
-func removeFromSliceIfExists(slice []string, target string) []string {
-	newSlice := []string{}
-	for _, item := range slice {
-		if item != target {
-			newSlice = append(newSlice, item)
-		}
-	}
-
-	return newSlice
-}

+ 0 - 144
src/mod/arsm/wsterminal/wsterminal.go

@@ -1,144 +0,0 @@
-package wsterminal
-
-import (
-	"encoding/json"
-	"net/http"
-	"sync"
-	"time"
-
-	"github.com/gorilla/websocket"
-	"imuslab.com/arozos/mod/auth"
-)
-
-/*
-	WebSocket Terminal
-	Author: tobychui
-
-	This module is a remote support service that allow
-	reverse ssh like connection using websocket.
-
-	For normal weboscket based shell or WsTTY, see wstty module instead.
-*/
-
-type Connection struct {
-	RemoteName            string
-	RemoteUUID            string
-	RemoteIP              string
-	RemoteToken           string
-	ConnectionStartedTime int64
-	LastOnline            int64
-	connection            *websocket.Conn
-	terminateTicker       chan bool
-}
-
-type Server struct {
-	connectionPool sync.Map
-	upgrader       websocket.Upgrader
-	authAgent      *auth.AuthAgent
-}
-
-type Client struct {
-}
-
-//Create a new Server
-func NewWsTerminalServer(authAgent *auth.AuthAgent) *Server {
-	return &Server{
-		connectionPool: sync.Map{},
-		upgrader:       websocket.Upgrader{},
-		authAgent:      authAgent,
-	}
-}
-
-//List all the active connection that is current connected to this server
-func (s *Server) ListConnections(w http.ResponseWriter, r *http.Request) {
-	activeConnections := []Connection{}
-	s.connectionPool.Range(func(key, value interface{}) bool {
-		activeConnections = append(activeConnections, *value.(*Connection))
-		return true
-	})
-	js, _ := json.Marshal(activeConnections)
-	sendJSONResponse(w, string(js))
-}
-
-//Handle new connections
-func (s *Server) HandleConnection(w http.ResponseWriter, r *http.Request) {
-	//Get the token and validate it
-	token, err := mv(r, "token", false)
-	if err != nil {
-		w.WriteHeader(http.StatusUnauthorized)
-		w.Write([]byte(`401 Unauthorized - Invalid token given`))
-		return
-	}
-
-	//Try to get the uuid from connectio
-	uuid, err := mv(r, "uuid", false)
-	if err != nil {
-		uuid = "unknown"
-	}
-
-	//Valida te the token
-	valid, username := s.authAgent.ValidateAutoLoginToken(token)
-	if !valid {
-		//Invalid token
-		w.WriteHeader(http.StatusUnauthorized)
-		w.Write([]byte(`401 Unauthorized - Invalid token given`))
-		return
-	}
-
-	//Create a connection object
-	thisConnection := Connection{
-		RemoteName:            username,
-		RemoteUUID:            uuid,
-		RemoteIP:              "",
-		RemoteToken:           "",
-		ConnectionStartedTime: time.Now().Unix(),
-		LastOnline:            time.Now().Unix(),
-	}
-
-	//Check if the same connection already exists. If yes, disconnect the old one
-	val, ok := s.connectionPool.Load(username)
-	if ok {
-		//Connection already exists. Disconenct the old one first
-		previousConn := val.(*Connection).connection
-		previousConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
-		time.Sleep(1 * time.Second)
-		previousConn.Close()
-	}
-
-	//Upgrade the current connection
-	c, err := s.upgrader.Upgrade(w, r, nil)
-	if err != nil {
-		w.WriteHeader(http.StatusInternalServerError)
-		w.Write([]byte(`500 Internal Server Error`))
-		return
-	}
-
-	thisConnection.connection = c
-
-	//Create a timer for poking the client and check if it is still online
-	ticker := time.NewTicker(5 * time.Minute)
-	done := make(chan bool)
-
-	thisConnection.terminateTicker = done
-	go func(connectionObject Connection) {
-		for {
-			select {
-			case <-done:
-				//Termination from another thread
-				return
-			case <-ticker.C:
-				//Send a ping signal to the client
-				if err := connectionObject.connection.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
-					//Unable to send ping message. Assume closed. Remove from active connection pool
-					s.connectionPool.Delete(thisConnection.RemoteName)
-					return
-				} else {
-					connectionObject.LastOnline = time.Now().Unix()
-				}
-			}
-		}
-	}(thisConnection)
-
-	//Store the connection object to the connection pool
-	s.connectionPool.Store(username, &thisConnection)
-}

+ 0 - 103
src/mod/disk/hybridBackup/restoreSnapshot.go

@@ -1,103 +0,0 @@
-package hybridBackup
-
-import (
-	"errors"
-	"log"
-	"os"
-	"path/filepath"
-)
-
-/*
-	restoreSnapshot.go
-
-	Restore snapshot for a certain user in the snapshot
-	The steps basically as follows.
-
-	1. Check and validate the snapshot
-	2. Iterate and restore all files contained in that snapshot to source drive if it is owned by the user
-	3. Get the snapshot link file. Restore all files with pointer still exists and owned by the user
-
-*/
-
-//Restore a snapshot by task and name
-func restoreSnapshotByName(backupTask *BackupTask, snapshotName string, username *string) error {
-	//Step 1: Check and validate snapshot
-	snapshotBaseFolder := filepath.Join(backupTask.DiskPath, "/version/", snapshotName)
-	snapshotRestoreDirectory := filepath.ToSlash(filepath.Clean(backupTask.ParentPath))
-	if !fileExists(snapshotBaseFolder) {
-		return errors.New("Given snapshot ID not found")
-	}
-
-	if !fileExists(filepath.Join(snapshotBaseFolder, "snapshot.datalink")) {
-		return errors.New("Snapshot corrupted. snapshot.datalink pointer file not found.")
-	}
-
-	log.Println("[HybridBackup] Restoring from snapshot ID: ", filepath.Base(snapshotBaseFolder))
-
-	//Step 2: Restore all the files changed during that snapshot period
-	fastWalk(snapshotBaseFolder, func(filename string) error {
-		//Skip the datalink file
-		if filepath.Base(filename) == "snapshot.datalink" {
-			return nil
-		}
-		//Calculate the relative path of this file
-		relPath, err := filepath.Rel(snapshotBaseFolder, filepath.ToSlash(filename))
-		if err != nil {
-			//Just skip this cycle
-			return nil
-		}
-
-		assumedRestoreLocation := filepath.ToSlash(filepath.Join(snapshotRestoreDirectory, relPath))
-		allowRestore := false
-		if username == nil {
-			//Restore all files
-			allowRestore = true
-		} else {
-			//Restore only files owned by this user
-
-			isOwnedByThisUser := snapshotFileBelongsToUser("/"+filepath.ToSlash(relPath), *username)
-			if isOwnedByThisUser {
-				allowRestore = true
-			}
-
-		}
-
-		if allowRestore {
-			//Check if the restore file parent folder exists.
-			if !fileExists(filepath.Dir(assumedRestoreLocation)) {
-				os.MkdirAll(filepath.Dir(assumedRestoreLocation), 0775)
-			}
-			//Copy this file from backup to source, overwriting source if exists
-			err := BufferedLargeFileCopy(filepath.ToSlash(filename), filepath.ToSlash(assumedRestoreLocation), 0775)
-			if err != nil {
-				log.Println("[HybridBackup] Restore failed: " + err.Error())
-			}
-		}
-
-		return nil
-	})
-
-	//Step 3: Restore files from datalinking file
-	linkMap, err := readLinkFile(snapshotBaseFolder)
-	if err != nil {
-		return err
-	}
-
-	for relPath, restorePointer := range linkMap.UnchangedFile {
-		//Get the assume restore position and source location
-		sourceFileLocation := filepath.ToSlash(filepath.Join(backupTask.DiskPath, "/version/", "/"+restorePointer+"/", relPath))
-		assumedRestoreLocation := filepath.ToSlash(filepath.Join(snapshotRestoreDirectory, relPath))
-
-		//Check if the restore file parent folder exists.
-		if snapshotFileBelongsToUser(filepath.ToSlash(relPath), *username) {
-			if !fileExists(filepath.Dir(assumedRestoreLocation)) {
-				os.MkdirAll(filepath.Dir(assumedRestoreLocation), 0775)
-			}
-			//Copy this file from backup to source, overwriting source if exists
-			BufferedLargeFileCopy(filepath.ToSlash(sourceFileLocation), filepath.ToSlash(assumedRestoreLocation), 0775)
-			log.Println("[HybridBackup] Restored " + assumedRestoreLocation + " for user " + *username)
-		}
-	}
-
-	return nil
-}

+ 0 - 452
src/mod/network/reverseproxy/reverse_test.go

@@ -1,452 +0,0 @@
-package reverseproxy
-
-import (
-	"bytes"
-	"io/ioutil"
-	"log"
-	"net/http"
-	"net/http/httptest"
-	"net/url"
-	"reflect"
-	"strings"
-	"testing"
-	"time"
-)
-
-const fakeHopHeader = "X-Fake-Hop-Header-For-Test"
-
-func init() {
-	hopHeaders = append(hopHeaders, fakeHopHeader)
-}
-
-func TestReverseProxy(t *testing.T) {
-	backendResponse := "I am the backend"
-	backendStatus := 404
-	backend := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-		if len(req.TransferEncoding) > 0 {
-			t.Errorf("backend got unexpected TransferEncoding: %v", req.TransferEncoding)
-		}
-
-		if req.Header.Get("X-Forwarded-For") == "" {
-			t.Errorf("didn't get X-Forwarded-For header")
-		}
-
-		if c := req.Header.Get("Connection"); c != "" {
-			t.Errorf("handler got Connection header value %q", c)
-		}
-
-		if c := req.Header.Get("Upgrade"); c != "" {
-			t.Errorf("handler got Upgrade header value %q", c)
-		}
-
-		if c := req.Header.Get("Proxy-Connection"); c != "" {
-			t.Errorf("handler got Proxy-Connection header value %q", c)
-		}
-
-		if c := req.Host; c == "" {
-			t.Errorf("backend got Host header %q", c)
-		}
-
-		rw.Header().Set("X-Foo", "bar")
-		rw.Header().Set(fakeHopHeader, "foo")
-		rw.Header().Set("Trailers", "not a special header field name")
-		rw.Header().Set("Trailer", "X-Trailer")
-		rw.Header().Set("Upgrade", "foo")
-		rw.Header().Add("X-Multi-Value", "foo")
-		rw.Header().Add("X-Multi-Value", "bar")
-		http.SetCookie(rw, &http.Cookie{Name: "flavor", Value: "chocolateChip"})
-		rw.WriteHeader(backendStatus)
-		rw.Write([]byte(backendResponse))
-		rw.Header().Set("X-Trailer", "trailer_value")
-	}))
-
-	defer backend.Close()
-	backendURL, err := url.Parse(backend.URL)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	proxyHandler := NewReverseProxy(backendURL)
-	proxyHandler.ErrorLog = log.New(ioutil.Discard, "", 0)
-	frontend := httptest.NewServer(proxyHandler)
-	defer frontend.Close()
-
-	getReq, _ := http.NewRequest("GET", frontend.URL, nil)
-	getReq.Host = "some host"
-	getReq.Header.Set("Connection", "close")
-	getReq.Header.Set("Proxy-Connection", "should be deleted")
-	getReq.Header.Set("Upgrade", "foo")
-	getReq.Close = true
-	res, err := http.DefaultClient.Do(getReq)
-	if err != nil {
-		t.Fatalf("Get: %v", err)
-	}
-
-	if g, e := res.StatusCode, backendStatus; g != e {
-		t.Errorf("got res.StatusCode %d; expected %d", g, e)
-	}
-
-	if g, e := res.Header.Get("X-Foo"), "bar"; g != e {
-		t.Errorf("got X-Foo %q; expected %q", g, e)
-	}
-
-	if c := res.Header.Get(fakeHopHeader); c != "" {
-		t.Errorf("got %s header value %q", fakeHopHeader, c)
-	}
-
-	if g, e := res.Header.Get("Trailers"), "not a special header field name"; g != e {
-		t.Errorf("header Trailers = %q; want %q", g, e)
-	}
-
-	if g, e := len(res.Header["X-Multi-Value"]), 2; g != e {
-		t.Errorf("got %d X-Multi-Value header values; expected %d", g, e)
-	}
-
-	if g, e := len(res.Header["Set-Cookie"]), 1; g != e {
-		t.Fatalf("got %d SetCookies, want %d", g, e)
-	}
-
-	if g, e := res.Trailer, (http.Header{"X-Trailer": nil}); !reflect.DeepEqual(g, e) {
-		t.Errorf("before reading body, Trailer = %#v; want %#v", g, e)
-	}
-
-	if cookie := res.Cookies()[0]; cookie.Name != "flavor" {
-		t.Errorf("unexpected cookie %q", cookie.Name)
-	}
-
-	bodyBytes, _ := ioutil.ReadAll(res.Body)
-
-	if g, e := string(bodyBytes), backendResponse; g != e {
-		t.Errorf("got body %q; expected %q", g, e)
-	}
-
-	if g, e := res.Trailer.Get("X-Trailer"), "trailer_value"; g != e {
-		t.Errorf("Trailer(X-Trailer) = %q ; want %q", g, e)
-	}
-
-}
-
-func TestReverseProxyStripHeadersPresentInConnection(t *testing.T) {
-	const fakeConnectionToken = "X-Fake-Connection-Token"
-	const backendResponse = "I am the backend"
-
-	backend := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-		if c := req.Header.Get(fakeConnectionToken); c != "" {
-			t.Errorf("handler got header %q = %q; want empty", fakeConnectionToken, c)
-		}
-
-		if c := req.Header.Get("Upgrade"); c != "" {
-			t.Errorf("handler got header %q = %q; want empty", "Upgrade", c)
-		}
-
-		rw.Header().Set("Connection", "Upgrade, "+fakeConnectionToken)
-		rw.Header().Set("Upgrade", "should be deleted")
-		rw.Header().Set(fakeConnectionToken, "should be deleted")
-		rw.Write([]byte(backendResponse))
-	}))
-	defer backend.Close()
-
-	backendURL, err := url.Parse(backend.URL)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	proxyHandler := NewReverseProxy(backendURL)
-	frontend := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-		proxyHandler.ServeHTTP(rw, req)
-		if c := req.Header.Get("Upgrade"); c != "original value" {
-			t.Errorf("handler modified header %q = %q; want %q", "Upgrade", c, "original value")
-		}
-	}))
-	defer frontend.Close()
-
-	getReq, _ := http.NewRequest("GET", frontend.URL, nil)
-	getReq.Header.Set("Connection", "Upgrade, "+fakeConnectionToken)
-	getReq.Header.Set("Upgrade", "original value")
-	getReq.Header.Set(fakeConnectionToken, "should be deleted")
-	res, err := http.DefaultClient.Do(getReq)
-	if err != nil {
-		t.Fatalf("Get: %v", err)
-	}
-	defer res.Body.Close()
-	bodyBytes, err := ioutil.ReadAll(res.Body)
-	if err != nil {
-		t.Fatalf("reading body: %v", err)
-	}
-
-	if g, e := string(bodyBytes), backendResponse; g != e {
-		t.Errorf("got body %q; want %q", g, e)
-	}
-
-	if c := res.Header.Get("Upgrade"); c != "" {
-		t.Errorf("handler got header %q = %q; want empty", "Upgrade", c)
-	}
-
-	if c := res.Header.Get(fakeConnectionToken); c != "" {
-		t.Errorf("handler got header %q = %q; want empty", fakeConnectionToken, c)
-	}
-}
-
-func TestXForwardedFor(t *testing.T) {
-	const prevForwardedFor = "client ip"
-	const backendResponse = "I am the backend"
-	const backendStatus = 404
-	backend := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-		if req.Header.Get("X-Forwarded-For") == "" {
-			t.Errorf("didn't get X-Forwarded-For header")
-		}
-
-		if !strings.Contains(req.Header.Get("X-Forwarded-For"), prevForwardedFor) {
-			t.Errorf("X-Forwarded-For didn't contain prior data")
-		}
-
-		rw.WriteHeader(backendStatus)
-		rw.Write([]byte(backendResponse))
-	}))
-
-	defer backend.Close()
-	backendURL, err := url.Parse(backend.URL)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	proxyHandler := NewReverseProxy(backendURL)
-	frontend := httptest.NewServer(proxyHandler)
-	defer frontend.Close()
-
-	getReq, _ := http.NewRequest("GET", frontend.URL, nil)
-	getReq.Header.Set("X-Forwarded-For", prevForwardedFor)
-	getReq.Close = true
-	res, err := http.DefaultClient.Do(getReq)
-	if err != nil {
-		t.Fatalf("Get: %v", err)
-	}
-	defer res.Body.Close()
-
-	if g, e := res.StatusCode, backendStatus; g != e {
-		t.Errorf("got res.StatusCode %d; expected %d", g, e)
-	}
-	bodyBytes, _ := ioutil.ReadAll(res.Body)
-	if g, e := string(bodyBytes), backendResponse; g != e {
-		t.Errorf("got body %q; expected %q", g, e)
-	}
-}
-
-var proxyQueryTests = []struct {
-	baseSuffix string // suffix to add to backend URL
-	reqSuffix  string // suffix to add to frontend's request URL
-	want       string // what backend should see for final request URL (without ?)
-}{
-	{"", "", ""},
-	{"?sta=tic", "?us=er", "sta=tic&us=er"},
-	{"", "?us=er", "us=er"},
-	{"?sta=tic", "", "sta=tic"},
-}
-
-func TestReverseProxyQuery(t *testing.T) {
-	backend := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-		rw.Header().Set("X-Got-Query", req.URL.RawQuery)
-		rw.Write([]byte("hi"))
-	}))
-	defer backend.Close()
-
-	for i, tt := range proxyQueryTests {
-		backendURL, err := url.Parse(backend.URL + tt.baseSuffix)
-		if err != nil {
-			t.Fatal(err)
-		}
-		frontend := httptest.NewServer(NewReverseProxy(backendURL))
-		req, _ := http.NewRequest("GET", frontend.URL+tt.reqSuffix, nil)
-		req.Close = true
-		res, err := http.DefaultClient.Do(req)
-		if err != nil {
-			t.Fatalf("%d. Get: %v", i, err)
-		}
-		if g, e := res.Header.Get("X-Got-Query"), tt.want; g != e {
-			t.Errorf("%d. got query %q; expected %q", i, g, e)
-		}
-		res.Body.Close()
-		frontend.Close()
-	}
-}
-
-func TestReverseProxyFlushInterval(t *testing.T) {
-	const expected = "hi"
-	backend := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-		rw.Write([]byte(expected))
-	}))
-	defer backend.Close()
-
-	backendURL, err := url.Parse(backend.URL)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	proxyHandler := NewReverseProxy(backendURL)
-	proxyHandler.FlushInterval = time.Microsecond
-
-	done := make(chan bool)
-	onExitFlushLoop = func() { done <- true }
-
-	frontend := httptest.NewServer(proxyHandler)
-	defer frontend.Close()
-
-	getReq, _ := http.NewRequest("GET", frontend.URL, nil)
-	getReq.Close = true
-	res, err := http.DefaultClient.Do(getReq)
-	if err != nil {
-		t.Fatalf("Get: %v", err)
-	}
-	defer res.Body.Close()
-
-	bodyBytes, _ := ioutil.ReadAll(res.Body)
-	if g, e := string(bodyBytes), expected; g != e {
-		t.Errorf("got body %q; expected %q", g, e)
-	}
-
-	select {
-	case <-done:
-		// do nothing
-	case <-time.After(3 * time.Second):
-		t.Errorf("maxLatencyWriter flushLoop() never exited")
-	}
-}
-
-func TestReverseProxyCancelation(t *testing.T) {
-	const backendResponse = "I am the backend"
-
-	reqInFlight := make(chan bool)
-	backend := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-		close(reqInFlight)
-
-		select {
-		case <-time.After(time.Second * 3):
-			t.Errorf("Handler never saw CloseNotify")
-		case <-rw.(http.CloseNotifier).CloseNotify():
-			// do nothing
-		}
-
-		rw.WriteHeader(http.StatusOK)
-		rw.Write([]byte(backendResponse))
-	}))
-	defer backend.Close()
-
-	backendURL, err := url.Parse(backend.URL)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	proxyHandler := NewReverseProxy(backendURL)
-	proxyHandler.ErrorLog = log.New(ioutil.Discard, "", 0)
-	frontend := httptest.NewServer(proxyHandler)
-	defer frontend.Close()
-
-	getReq, err := http.NewRequest("GET", frontend.URL, nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	go func() {
-		<-reqInFlight
-		http.DefaultTransport.(*http.Transport).CancelRequest(getReq)
-	}()
-
-	res, err := http.DefaultClient.Do(getReq)
-
-	if res != nil {
-		t.Errorf("got response %v; want nil", res.Status)
-	}
-
-	if err == nil {
-		t.Error("DefaultClient.Do() returned nil error; want non-nil error")
-	}
-}
-
-func TestReverProxyPost(t *testing.T) {
-	const backendResponse = "I am the backend"
-	const backendStatus = 200
-	var requestBody = bytes.Repeat([]byte("a"), 1<<20)
-
-	backend := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-		requestData, err := ioutil.ReadAll(req.Body)
-		if err != nil {
-			t.Errorf("Backend body read = %v", err)
-		}
-
-		if len(requestData) != len(requestBody) {
-			t.Errorf("Backend read %d request body bytes; want %d", len(requestData), len(requestBody))
-		}
-
-		if !bytes.Equal(requestData, requestBody) {
-			t.Error("Backend read wrong request body.")
-		}
-
-		rw.Write([]byte(backendResponse))
-	}))
-	defer backend.Close()
-
-	backendURL, err := url.Parse(backend.URL)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	proxyHandler := NewReverseProxy(backendURL)
-	frontend := httptest.NewServer(proxyHandler)
-	defer frontend.Close()
-
-	res, err := http.Post(frontend.URL, "", bytes.NewReader(requestBody))
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer res.Body.Close()
-
-	bodyBytes, err := ioutil.ReadAll(res.Body)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if g, e := string(bodyBytes), backendResponse; g != e {
-		t.Errorf("got response %v, want %v", g, e)
-	}
-}
-
-func TestHTTPTunnel(t *testing.T) {
-	const backendResponse = "I am the backend"
-	backend := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-		rw.Write([]byte(backendResponse))
-	}))
-	defer backend.Close()
-
-	backendURL, err := url.Parse(backend.URL)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	proxyHandler := NewReverseProxy(backendURL)
-	frontend := httptest.NewServer(proxyHandler)
-	defer frontend.Close()
-
-	frontendURL, err := url.Parse(frontend.URL)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	getReq := &http.Request{
-		Method: "CONNECT",
-		URL: &url.URL{
-			Host:   frontendURL.Host,
-			Scheme: frontendURL.Scheme,
-			Path:   "google.com:80",
-		},
-		Header: http.Header{},
-	}
-
-	res, err := http.DefaultTransport.(*http.Transport).RoundTrip(getReq)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer res.Body.Close()
-	if res.Status != "200 OK" {
-		t.Errorf("got response status %v, want %v", res.Status, "200 OK")
-	}
-}

+ 0 - 906
src/mod/network/webdav/xml_test.go

@@ -1,906 +0,0 @@
-// Copyright 2014 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package webdav
-
-import (
-	"bytes"
-	"encoding/xml"
-	"fmt"
-	"io"
-	"net/http"
-	"net/http/httptest"
-	"reflect"
-	"sort"
-	"strings"
-	"testing"
-
-	ixml "golang.org/x/net/webdav/internal/xml"
-)
-
-func TestReadLockInfo(t *testing.T) {
-	// The "section x.y.z" test cases come from section x.y.z of the spec at
-	// http://www.webdav.org/specs/rfc4918.html
-	testCases := []struct {
-		desc       string
-		input      string
-		wantLI     lockInfo
-		wantStatus int
-	}{{
-		"bad: junk",
-		"xxx",
-		lockInfo{},
-		http.StatusBadRequest,
-	}, {
-		"bad: invalid owner XML",
-		"" +
-			"<D:lockinfo xmlns:D='DAV:'>\n" +
-			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
-			"  <D:locktype><D:write/></D:locktype>\n" +
-			"  <D:owner>\n" +
-			"    <D:href>   no end tag   \n" +
-			"  </D:owner>\n" +
-			"</D:lockinfo>",
-		lockInfo{},
-		http.StatusBadRequest,
-	}, {
-		"bad: invalid UTF-8",
-		"" +
-			"<D:lockinfo xmlns:D='DAV:'>\n" +
-			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
-			"  <D:locktype><D:write/></D:locktype>\n" +
-			"  <D:owner>\n" +
-			"    <D:href>   \xff   </D:href>\n" +
-			"  </D:owner>\n" +
-			"</D:lockinfo>",
-		lockInfo{},
-		http.StatusBadRequest,
-	}, {
-		"bad: unfinished XML #1",
-		"" +
-			"<D:lockinfo xmlns:D='DAV:'>\n" +
-			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
-			"  <D:locktype><D:write/></D:locktype>\n",
-		lockInfo{},
-		http.StatusBadRequest,
-	}, {
-		"bad: unfinished XML #2",
-		"" +
-			"<D:lockinfo xmlns:D='DAV:'>\n" +
-			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
-			"  <D:locktype><D:write/></D:locktype>\n" +
-			"  <D:owner>\n",
-		lockInfo{},
-		http.StatusBadRequest,
-	}, {
-		"good: empty",
-		"",
-		lockInfo{},
-		0,
-	}, {
-		"good: plain-text owner",
-		"" +
-			"<D:lockinfo xmlns:D='DAV:'>\n" +
-			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
-			"  <D:locktype><D:write/></D:locktype>\n" +
-			"  <D:owner>gopher</D:owner>\n" +
-			"</D:lockinfo>",
-		lockInfo{
-			XMLName:   ixml.Name{Space: "DAV:", Local: "lockinfo"},
-			Exclusive: new(struct{}),
-			Write:     new(struct{}),
-			Owner: owner{
-				InnerXML: "gopher",
-			},
-		},
-		0,
-	}, {
-		"section 9.10.7",
-		"" +
-			"<D:lockinfo xmlns:D='DAV:'>\n" +
-			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
-			"  <D:locktype><D:write/></D:locktype>\n" +
-			"  <D:owner>\n" +
-			"    <D:href>http://example.org/~ejw/contact.html</D:href>\n" +
-			"  </D:owner>\n" +
-			"</D:lockinfo>",
-		lockInfo{
-			XMLName:   ixml.Name{Space: "DAV:", Local: "lockinfo"},
-			Exclusive: new(struct{}),
-			Write:     new(struct{}),
-			Owner: owner{
-				InnerXML: "\n    <D:href>http://example.org/~ejw/contact.html</D:href>\n  ",
-			},
-		},
-		0,
-	}}
-
-	for _, tc := range testCases {
-		li, status, err := readLockInfo(strings.NewReader(tc.input))
-		if tc.wantStatus != 0 {
-			if err == nil {
-				t.Errorf("%s: got nil error, want non-nil", tc.desc)
-				continue
-			}
-		} else if err != nil {
-			t.Errorf("%s: %v", tc.desc, err)
-			continue
-		}
-		if !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus {
-			t.Errorf("%s:\ngot  lockInfo=%v, status=%v\nwant lockInfo=%v, status=%v",
-				tc.desc, li, status, tc.wantLI, tc.wantStatus)
-			continue
-		}
-	}
-}
-
-func TestReadPropfind(t *testing.T) {
-	testCases := []struct {
-		desc       string
-		input      string
-		wantPF     propfind
-		wantStatus int
-	}{{
-		desc: "propfind: propname",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:propname/>\n" +
-			"</A:propfind>",
-		wantPF: propfind{
-			XMLName:  ixml.Name{Space: "DAV:", Local: "propfind"},
-			Propname: new(struct{}),
-		},
-	}, {
-		desc:  "propfind: empty body means allprop",
-		input: "",
-		wantPF: propfind{
-			Allprop: new(struct{}),
-		},
-	}, {
-		desc: "propfind: allprop",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"   <A:allprop/>\n" +
-			"</A:propfind>",
-		wantPF: propfind{
-			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
-			Allprop: new(struct{}),
-		},
-	}, {
-		desc: "propfind: allprop followed by include",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:allprop/>\n" +
-			"  <A:include><A:displayname/></A:include>\n" +
-			"</A:propfind>",
-		wantPF: propfind{
-			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
-			Allprop: new(struct{}),
-			Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
-		},
-	}, {
-		desc: "propfind: include followed by allprop",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:include><A:displayname/></A:include>\n" +
-			"  <A:allprop/>\n" +
-			"</A:propfind>",
-		wantPF: propfind{
-			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
-			Allprop: new(struct{}),
-			Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
-		},
-	}, {
-		desc: "propfind: propfind",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop><A:displayname/></A:prop>\n" +
-			"</A:propfind>",
-		wantPF: propfind{
-			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
-			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
-		},
-	}, {
-		desc: "propfind: prop with ignored comments",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop>\n" +
-			"    <!-- ignore -->\n" +
-			"    <A:displayname><!-- ignore --></A:displayname>\n" +
-			"  </A:prop>\n" +
-			"</A:propfind>",
-		wantPF: propfind{
-			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
-			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
-		},
-	}, {
-		desc: "propfind: propfind with ignored whitespace",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop>   <A:displayname/></A:prop>\n" +
-			"</A:propfind>",
-		wantPF: propfind{
-			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
-			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
-		},
-	}, {
-		desc: "propfind: propfind with ignored mixed-content",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop>foo<A:displayname/>bar</A:prop>\n" +
-			"</A:propfind>",
-		wantPF: propfind{
-			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
-			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
-		},
-	}, {
-		desc: "propfind: propname with ignored element (section A.4)",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:propname/>\n" +
-			"  <E:leave-out xmlns:E='E:'>*boss*</E:leave-out>\n" +
-			"</A:propfind>",
-		wantPF: propfind{
-			XMLName:  ixml.Name{Space: "DAV:", Local: "propfind"},
-			Propname: new(struct{}),
-		},
-	}, {
-		desc:       "propfind: bad: junk",
-		input:      "xxx",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "propfind: bad: propname and allprop (section A.3)",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:propname/>" +
-			"  <A:allprop/>" +
-			"</A:propfind>",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "propfind: bad: propname and prop",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop><A:displayname/></A:prop>\n" +
-			"  <A:propname/>\n" +
-			"</A:propfind>",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "propfind: bad: allprop and prop",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:allprop/>\n" +
-			"  <A:prop><A:foo/><A:/prop>\n" +
-			"</A:propfind>",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "propfind: bad: empty propfind with ignored element (section A.4)",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <E:expired-props/>\n" +
-			"</A:propfind>",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "propfind: bad: empty prop",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop/>\n" +
-			"</A:propfind>",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "propfind: bad: prop with just chardata",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop>foo</A:prop>\n" +
-			"</A:propfind>",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "bad: interrupted prop",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop><A:foo></A:prop>\n",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "bad: malformed end element prop",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop><A:foo/></A:bar></A:prop>\n",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "propfind: bad: property with chardata value",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop><A:foo>bar</A:foo></A:prop>\n" +
-			"</A:propfind>",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "propfind: bad: property with whitespace value",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:prop><A:foo> </A:foo></A:prop>\n" +
-			"</A:propfind>",
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "propfind: bad: include without allprop",
-		input: "" +
-			"<A:propfind xmlns:A='DAV:'>\n" +
-			"  <A:include><A:foo/></A:include>\n" +
-			"</A:propfind>",
-		wantStatus: http.StatusBadRequest,
-	}}
-
-	for _, tc := range testCases {
-		pf, status, err := readPropfind(strings.NewReader(tc.input))
-		if tc.wantStatus != 0 {
-			if err == nil {
-				t.Errorf("%s: got nil error, want non-nil", tc.desc)
-				continue
-			}
-		} else if err != nil {
-			t.Errorf("%s: %v", tc.desc, err)
-			continue
-		}
-		if !reflect.DeepEqual(pf, tc.wantPF) || status != tc.wantStatus {
-			t.Errorf("%s:\ngot  propfind=%v, status=%v\nwant propfind=%v, status=%v",
-				tc.desc, pf, status, tc.wantPF, tc.wantStatus)
-			continue
-		}
-	}
-}
-
-func TestMultistatusWriter(t *testing.T) {
-	///The "section x.y.z" test cases come from section x.y.z of the spec at
-	// http://www.webdav.org/specs/rfc4918.html
-	testCases := []struct {
-		desc        string
-		responses   []response
-		respdesc    string
-		writeHeader bool
-		wantXML     string
-		wantCode    int
-		wantErr     error
-	}{{
-		desc: "section 9.2.2 (failed dependency)",
-		responses: []response{{
-			Href: []string{"http://example.com/foo"},
-			Propstat: []propstat{{
-				Prop: []Property{{
-					XMLName: xml.Name{
-						Space: "http://ns.example.com/",
-						Local: "Authors",
-					},
-				}},
-				Status: "HTTP/1.1 424 Failed Dependency",
-			}, {
-				Prop: []Property{{
-					XMLName: xml.Name{
-						Space: "http://ns.example.com/",
-						Local: "Copyright-Owner",
-					},
-				}},
-				Status: "HTTP/1.1 409 Conflict",
-			}},
-			ResponseDescription: "Copyright Owner cannot be deleted or altered.",
-		}},
-		wantXML: `` +
-			`<?xml version="1.0" encoding="UTF-8"?>` +
-			`<multistatus xmlns="DAV:">` +
-			`  <response>` +
-			`    <href>http://example.com/foo</href>` +
-			`    <propstat>` +
-			`      <prop>` +
-			`        <Authors xmlns="http://ns.example.com/"></Authors>` +
-			`      </prop>` +
-			`      <status>HTTP/1.1 424 Failed Dependency</status>` +
-			`    </propstat>` +
-			`    <propstat xmlns="DAV:">` +
-			`      <prop>` +
-			`        <Copyright-Owner xmlns="http://ns.example.com/"></Copyright-Owner>` +
-			`      </prop>` +
-			`      <status>HTTP/1.1 409 Conflict</status>` +
-			`    </propstat>` +
-			`  <responsedescription>Copyright Owner cannot be deleted or altered.</responsedescription>` +
-			`</response>` +
-			`</multistatus>`,
-		wantCode: StatusMulti,
-	}, {
-		desc: "section 9.6.2 (lock-token-submitted)",
-		responses: []response{{
-			Href:   []string{"http://example.com/foo"},
-			Status: "HTTP/1.1 423 Locked",
-			Error: &xmlError{
-				InnerXML: []byte(`<lock-token-submitted xmlns="DAV:"/>`),
-			},
-		}},
-		wantXML: `` +
-			`<?xml version="1.0" encoding="UTF-8"?>` +
-			`<multistatus xmlns="DAV:">` +
-			`  <response>` +
-			`    <href>http://example.com/foo</href>` +
-			`    <status>HTTP/1.1 423 Locked</status>` +
-			`    <error><lock-token-submitted xmlns="DAV:"/></error>` +
-			`  </response>` +
-			`</multistatus>`,
-		wantCode: StatusMulti,
-	}, {
-		desc: "section 9.1.3",
-		responses: []response{{
-			Href: []string{"http://example.com/foo"},
-			Propstat: []propstat{{
-				Prop: []Property{{
-					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "bigbox"},
-					InnerXML: []byte(`` +
-						`<BoxType xmlns="http://ns.example.com/boxschema/">` +
-						`Box type A` +
-						`</BoxType>`),
-				}, {
-					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "author"},
-					InnerXML: []byte(`` +
-						`<Name xmlns="http://ns.example.com/boxschema/">` +
-						`J.J. Johnson` +
-						`</Name>`),
-				}},
-				Status: "HTTP/1.1 200 OK",
-			}, {
-				Prop: []Property{{
-					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "DingALing"},
-				}, {
-					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "Random"},
-				}},
-				Status:              "HTTP/1.1 403 Forbidden",
-				ResponseDescription: "The user does not have access to the DingALing property.",
-			}},
-		}},
-		respdesc: "There has been an access violation error.",
-		wantXML: `` +
-			`<?xml version="1.0" encoding="UTF-8"?>` +
-			`<multistatus xmlns="DAV:" xmlns:B="http://ns.example.com/boxschema/">` +
-			`  <response>` +
-			`    <href>http://example.com/foo</href>` +
-			`    <propstat>` +
-			`      <prop>` +
-			`        <B:bigbox><B:BoxType>Box type A</B:BoxType></B:bigbox>` +
-			`        <B:author><B:Name>J.J. Johnson</B:Name></B:author>` +
-			`      </prop>` +
-			`      <status>HTTP/1.1 200 OK</status>` +
-			`    </propstat>` +
-			`    <propstat>` +
-			`      <prop>` +
-			`        <B:DingALing/>` +
-			`        <B:Random/>` +
-			`      </prop>` +
-			`      <status>HTTP/1.1 403 Forbidden</status>` +
-			`      <responsedescription>The user does not have access to the DingALing property.</responsedescription>` +
-			`    </propstat>` +
-			`  </response>` +
-			`  <responsedescription>There has been an access violation error.</responsedescription>` +
-			`</multistatus>`,
-		wantCode: StatusMulti,
-	}, {
-		desc: "no response written",
-		// default of http.responseWriter
-		wantCode: http.StatusOK,
-	}, {
-		desc:     "no response written (with description)",
-		respdesc: "too bad",
-		// default of http.responseWriter
-		wantCode: http.StatusOK,
-	}, {
-		desc:        "empty multistatus with header",
-		writeHeader: true,
-		wantXML:     `<multistatus xmlns="DAV:"></multistatus>`,
-		wantCode:    StatusMulti,
-	}, {
-		desc: "bad: no href",
-		responses: []response{{
-			Propstat: []propstat{{
-				Prop: []Property{{
-					XMLName: xml.Name{
-						Space: "http://example.com/",
-						Local: "foo",
-					},
-				}},
-				Status: "HTTP/1.1 200 OK",
-			}},
-		}},
-		wantErr: errInvalidResponse,
-		// default of http.responseWriter
-		wantCode: http.StatusOK,
-	}, {
-		desc: "bad: multiple hrefs and no status",
-		responses: []response{{
-			Href: []string{"http://example.com/foo", "http://example.com/bar"},
-		}},
-		wantErr: errInvalidResponse,
-		// default of http.responseWriter
-		wantCode: http.StatusOK,
-	}, {
-		desc: "bad: one href and no propstat",
-		responses: []response{{
-			Href: []string{"http://example.com/foo"},
-		}},
-		wantErr: errInvalidResponse,
-		// default of http.responseWriter
-		wantCode: http.StatusOK,
-	}, {
-		desc: "bad: status with one href and propstat",
-		responses: []response{{
-			Href: []string{"http://example.com/foo"},
-			Propstat: []propstat{{
-				Prop: []Property{{
-					XMLName: xml.Name{
-						Space: "http://example.com/",
-						Local: "foo",
-					},
-				}},
-				Status: "HTTP/1.1 200 OK",
-			}},
-			Status: "HTTP/1.1 200 OK",
-		}},
-		wantErr: errInvalidResponse,
-		// default of http.responseWriter
-		wantCode: http.StatusOK,
-	}, {
-		desc: "bad: multiple hrefs and propstat",
-		responses: []response{{
-			Href: []string{
-				"http://example.com/foo",
-				"http://example.com/bar",
-			},
-			Propstat: []propstat{{
-				Prop: []Property{{
-					XMLName: xml.Name{
-						Space: "http://example.com/",
-						Local: "foo",
-					},
-				}},
-				Status: "HTTP/1.1 200 OK",
-			}},
-		}},
-		wantErr: errInvalidResponse,
-		// default of http.responseWriter
-		wantCode: http.StatusOK,
-	}}
-
-	n := xmlNormalizer{omitWhitespace: true}
-loop:
-	for _, tc := range testCases {
-		rec := httptest.NewRecorder()
-		w := multistatusWriter{w: rec, responseDescription: tc.respdesc}
-		if tc.writeHeader {
-			if err := w.writeHeader(); err != nil {
-				t.Errorf("%s: got writeHeader error %v, want nil", tc.desc, err)
-				continue
-			}
-		}
-		for _, r := range tc.responses {
-			if err := w.write(&r); err != nil {
-				if err != tc.wantErr {
-					t.Errorf("%s: got write error %v, want %v",
-						tc.desc, err, tc.wantErr)
-				}
-				continue loop
-			}
-		}
-		if err := w.close(); err != tc.wantErr {
-			t.Errorf("%s: got close error %v, want %v",
-				tc.desc, err, tc.wantErr)
-			continue
-		}
-		if rec.Code != tc.wantCode {
-			t.Errorf("%s: got HTTP status code %d, want %d\n",
-				tc.desc, rec.Code, tc.wantCode)
-			continue
-		}
-		gotXML := rec.Body.String()
-		eq, err := n.equalXML(strings.NewReader(gotXML), strings.NewReader(tc.wantXML))
-		if err != nil {
-			t.Errorf("%s: equalXML: %v", tc.desc, err)
-			continue
-		}
-		if !eq {
-			t.Errorf("%s: XML body\ngot  %s\nwant %s", tc.desc, gotXML, tc.wantXML)
-		}
-	}
-}
-
-func TestReadProppatch(t *testing.T) {
-	ppStr := func(pps []Proppatch) string {
-		var outer []string
-		for _, pp := range pps {
-			var inner []string
-			for _, p := range pp.Props {
-				inner = append(inner, fmt.Sprintf("{XMLName: %q, Lang: %q, InnerXML: %q}",
-					p.XMLName, p.Lang, p.InnerXML))
-			}
-			outer = append(outer, fmt.Sprintf("{Remove: %t, Props: [%s]}",
-				pp.Remove, strings.Join(inner, ", ")))
-		}
-		return "[" + strings.Join(outer, ", ") + "]"
-	}
-
-	testCases := []struct {
-		desc       string
-		input      string
-		wantPP     []Proppatch
-		wantStatus int
-	}{{
-		desc: "proppatch: section 9.2 (with simple property value)",
-		input: `` +
-			`<?xml version="1.0" encoding="utf-8" ?>` +
-			`<D:propertyupdate xmlns:D="DAV:"` +
-			`                  xmlns:Z="http://ns.example.com/z/">` +
-			`    <D:set>` +
-			`         <D:prop><Z:Authors>somevalue</Z:Authors></D:prop>` +
-			`    </D:set>` +
-			`    <D:remove>` +
-			`         <D:prop><Z:Copyright-Owner/></D:prop>` +
-			`    </D:remove>` +
-			`</D:propertyupdate>`,
-		wantPP: []Proppatch{{
-			Props: []Property{{
-				xml.Name{Space: "http://ns.example.com/z/", Local: "Authors"},
-				"",
-				[]byte(`somevalue`),
-			}},
-		}, {
-			Remove: true,
-			Props: []Property{{
-				xml.Name{Space: "http://ns.example.com/z/", Local: "Copyright-Owner"},
-				"",
-				nil,
-			}},
-		}},
-	}, {
-		desc: "proppatch: lang attribute on prop",
-		input: `` +
-			`<?xml version="1.0" encoding="utf-8" ?>` +
-			`<D:propertyupdate xmlns:D="DAV:">` +
-			`    <D:set>` +
-			`         <D:prop xml:lang="en">` +
-			`              <foo xmlns="http://example.com/ns"/>` +
-			`         </D:prop>` +
-			`    </D:set>` +
-			`</D:propertyupdate>`,
-		wantPP: []Proppatch{{
-			Props: []Property{{
-				xml.Name{Space: "http://example.com/ns", Local: "foo"},
-				"en",
-				nil,
-			}},
-		}},
-	}, {
-		desc: "bad: remove with value",
-		input: `` +
-			`<?xml version="1.0" encoding="utf-8" ?>` +
-			`<D:propertyupdate xmlns:D="DAV:"` +
-			`                  xmlns:Z="http://ns.example.com/z/">` +
-			`    <D:remove>` +
-			`         <D:prop>` +
-			`              <Z:Authors>` +
-			`              <Z:Author>Jim Whitehead</Z:Author>` +
-			`              </Z:Authors>` +
-			`         </D:prop>` +
-			`    </D:remove>` +
-			`</D:propertyupdate>`,
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "bad: empty propertyupdate",
-		input: `` +
-			`<?xml version="1.0" encoding="utf-8" ?>` +
-			`<D:propertyupdate xmlns:D="DAV:"` +
-			`</D:propertyupdate>`,
-		wantStatus: http.StatusBadRequest,
-	}, {
-		desc: "bad: empty prop",
-		input: `` +
-			`<?xml version="1.0" encoding="utf-8" ?>` +
-			`<D:propertyupdate xmlns:D="DAV:"` +
-			`                  xmlns:Z="http://ns.example.com/z/">` +
-			`    <D:remove>` +
-			`        <D:prop/>` +
-			`    </D:remove>` +
-			`</D:propertyupdate>`,
-		wantStatus: http.StatusBadRequest,
-	}}
-
-	for _, tc := range testCases {
-		pp, status, err := readProppatch(strings.NewReader(tc.input))
-		if tc.wantStatus != 0 {
-			if err == nil {
-				t.Errorf("%s: got nil error, want non-nil", tc.desc)
-				continue
-			}
-		} else if err != nil {
-			t.Errorf("%s: %v", tc.desc, err)
-			continue
-		}
-		if status != tc.wantStatus {
-			t.Errorf("%s: got status %d, want %d", tc.desc, status, tc.wantStatus)
-			continue
-		}
-		if !reflect.DeepEqual(pp, tc.wantPP) || status != tc.wantStatus {
-			t.Errorf("%s: proppatch\ngot  %v\nwant %v", tc.desc, ppStr(pp), ppStr(tc.wantPP))
-		}
-	}
-}
-
-func TestUnmarshalXMLValue(t *testing.T) {
-	testCases := []struct {
-		desc    string
-		input   string
-		wantVal string
-	}{{
-		desc:    "simple char data",
-		input:   "<root>foo</root>",
-		wantVal: "foo",
-	}, {
-		desc:    "empty element",
-		input:   "<root><foo/></root>",
-		wantVal: "<foo/>",
-	}, {
-		desc:    "preserve namespace",
-		input:   `<root><foo xmlns="bar"/></root>`,
-		wantVal: `<foo xmlns="bar"/>`,
-	}, {
-		desc:    "preserve root element namespace",
-		input:   `<root xmlns:bar="bar"><bar:foo/></root>`,
-		wantVal: `<foo xmlns="bar"/>`,
-	}, {
-		desc:    "preserve whitespace",
-		input:   "<root>  \t </root>",
-		wantVal: "  \t ",
-	}, {
-		desc:    "preserve mixed content",
-		input:   `<root xmlns="bar">  <foo>a<bam xmlns="baz"/> </foo> </root>`,
-		wantVal: `  <foo xmlns="bar">a<bam xmlns="baz"/> </foo> `,
-	}, {
-		desc: "section 9.2",
-		input: `` +
-			`<Z:Authors xmlns:Z="http://ns.example.com/z/">` +
-			`  <Z:Author>Jim Whitehead</Z:Author>` +
-			`  <Z:Author>Roy Fielding</Z:Author>` +
-			`</Z:Authors>`,
-		wantVal: `` +
-			`  <Author xmlns="http://ns.example.com/z/">Jim Whitehead</Author>` +
-			`  <Author xmlns="http://ns.example.com/z/">Roy Fielding</Author>`,
-	}, {
-		desc: "section 4.3.1 (mixed content)",
-		input: `` +
-			`<x:author ` +
-			`    xmlns:x='http://example.com/ns' ` +
-			`    xmlns:D="DAV:">` +
-			`  <x:name>Jane Doe</x:name>` +
-			`  <!-- Jane's contact info -->` +
-			`  <x:uri type='email'` +
-			`         added='2005-11-26'>mailto:jane.doe@example.com</x:uri>` +
-			`  <x:uri type='web'` +
-			`         added='2005-11-27'>http://www.example.com</x:uri>` +
-			`  <x:notes xmlns:h='http://www.w3.org/1999/xhtml'>` +
-			`    Jane has been working way <h:em>too</h:em> long on the` +
-			`    long-awaited revision of <![CDATA[<RFC2518>]]>.` +
-			`  </x:notes>` +
-			`</x:author>`,
-		wantVal: `` +
-			`  <name xmlns="http://example.com/ns">Jane Doe</name>` +
-			`  ` +
-			`  <uri type='email'` +
-			`       xmlns="http://example.com/ns" ` +
-			`       added='2005-11-26'>mailto:jane.doe@example.com</uri>` +
-			`  <uri added='2005-11-27'` +
-			`       type='web'` +
-			`       xmlns="http://example.com/ns">http://www.example.com</uri>` +
-			`  <notes xmlns="http://example.com/ns" ` +
-			`         xmlns:h="http://www.w3.org/1999/xhtml">` +
-			`    Jane has been working way <h:em>too</h:em> long on the` +
-			`    long-awaited revision of &lt;RFC2518&gt;.` +
-			`  </notes>`,
-	}}
-
-	var n xmlNormalizer
-	for _, tc := range testCases {
-		d := ixml.NewDecoder(strings.NewReader(tc.input))
-		var v xmlValue
-		if err := d.Decode(&v); err != nil {
-			t.Errorf("%s: got error %v, want nil", tc.desc, err)
-			continue
-		}
-		eq, err := n.equalXML(bytes.NewReader(v), strings.NewReader(tc.wantVal))
-		if err != nil {
-			t.Errorf("%s: equalXML: %v", tc.desc, err)
-			continue
-		}
-		if !eq {
-			t.Errorf("%s:\ngot  %s\nwant %s", tc.desc, string(v), tc.wantVal)
-		}
-	}
-}
-
-// xmlNormalizer normalizes XML.
-type xmlNormalizer struct {
-	// omitWhitespace instructs to ignore whitespace between element tags.
-	omitWhitespace bool
-	// omitComments instructs to ignore XML comments.
-	omitComments bool
-}
-
-// normalize writes the normalized XML content of r to w. It applies the
-// following rules
-//
-//     * Rename namespace prefixes according to an internal heuristic.
-//     * Remove unnecessary namespace declarations.
-//     * Sort attributes in XML start elements in lexical order of their
-//       fully qualified name.
-//     * Remove XML directives and processing instructions.
-//     * Remove CDATA between XML tags that only contains whitespace, if
-//       instructed to do so.
-//     * Remove comments, if instructed to do so.
-//
-func (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error {
-	d := ixml.NewDecoder(r)
-	e := ixml.NewEncoder(w)
-	for {
-		t, err := d.Token()
-		if err != nil {
-			if t == nil && err == io.EOF {
-				break
-			}
-			return err
-		}
-		switch val := t.(type) {
-		case ixml.Directive, ixml.ProcInst:
-			continue
-		case ixml.Comment:
-			if n.omitComments {
-				continue
-			}
-		case ixml.CharData:
-			if n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 {
-				continue
-			}
-		case ixml.StartElement:
-			start, _ := ixml.CopyToken(val).(ixml.StartElement)
-			attr := start.Attr[:0]
-			for _, a := range start.Attr {
-				if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" {
-					continue
-				}
-				attr = append(attr, a)
-			}
-			sort.Sort(byName(attr))
-			start.Attr = attr
-			t = start
-		}
-		err = e.EncodeToken(t)
-		if err != nil {
-			return err
-		}
-	}
-	return e.Flush()
-}
-
-// equalXML tests for equality of the normalized XML contents of a and b.
-func (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) {
-	var buf bytes.Buffer
-	if err := n.normalize(&buf, a); err != nil {
-		return false, err
-	}
-	normA := buf.String()
-	buf.Reset()
-	if err := n.normalize(&buf, b); err != nil {
-		return false, err
-	}
-	normB := buf.String()
-	return normA == normB, nil
-}
-
-type byName []ixml.Attr
-
-func (a byName) Len() int      { return len(a) }
-func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-func (a byName) Less(i, j int) bool {
-	if a[i].Name.Space != a[j].Name.Space {
-		return a[i].Name.Space < a[j].Name.Space
-	}
-	return a[i].Name.Local < a[j].Name.Local
-}