A simple, fault-tolerant, and easy-to-audit web-based password manager. https://pwm.sequentialread.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

263 lines
6.7 KiB

package main
import (
"bytes"
"crypto/sha256"
"crypto/tls"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"text/template"
"time"
)
var appPort = "8073"
var dataPath string
var indexTemplate *template.Template
var application Application
type Application struct {
Version string
BackblazeBucketName string
BackblazeBucketRegion string
BackblazeAccessKeyId string
BackblazeSecretAccessKey string
}
func storage(response http.ResponseWriter, request *http.Request) {
var filename string
var pathElements = strings.Split(request.RequestURI, "/")
filename = pathElements[len(pathElements)-1]
matched, err := regexp.MatchString("[a-f0-9]+", filename)
if err != nil {
response.WriteHeader(500)
fmt.Fprintf(response, "500 %s", err)
return
}
if !matched {
response.WriteHeader(400)
fmt.Fprint(response, "400 filename must be a hexadecimal string")
return
}
fullPath := filepath.Join(dataPath, filename)
if request.Method == "GET" {
_, err := os.Stat(fullPath)
if err != nil {
response.WriteHeader(404)
fmt.Print("404 file not found: " + fullPath + "\n\n")
fmt.Fprint(response, "404 file not found")
return
}
file, err := os.Open(fullPath)
if err != nil {
response.WriteHeader(404)
fmt.Print("404 file not found: " + fullPath + "\n\n")
fmt.Fprint(response, "404 file not found")
return
}
defer file.Close()
response.Header().Add("Content-Type", "application/octet-stream")
io.Copy(response, file)
} else if request.Method == "PUT" {
bytes, err := ioutil.ReadAll(request.Body)
if err != nil {
response.WriteHeader(500)
fmt.Fprintf(response, "500 %s", err)
return
}
err = ioutil.WriteFile(fullPath, bytes, 0644)
if err != nil {
response.WriteHeader(500)
fmt.Fprintf(response, "500 %s", err)
return
}
} else if request.Method == "DELETE" {
response.WriteHeader(500)
fmt.Fprint(response, "500 not implemented yet")
} else {
response.Header().Add("Allow", "GET, PUT, DELETE")
response.WriteHeader(405)
fmt.Fprint(response, "405 Method Not Supported")
}
}
func indexHtml(response http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/" {
response.WriteHeader(404)
fmt.Fprintf(response, "404 not found: %s", request.URL.Path)
return
}
var buffer bytes.Buffer
err := indexTemplate.Execute(&buffer, application)
if err != nil {
response.WriteHeader(500)
fmt.Fprintf(response, "500 %s", err)
return
}
response.Header().Set("Etag", application.Version)
io.Copy(response, &buffer)
}
func loadTemplate(filename string) *template.Template {
newTemplateString, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
newTemplate, err := template.New(filename).Parse(string(newTemplateString))
if err != nil {
panic(err)
}
return newTemplate
}
func hashFiles(seeds []string, filenames []string) string {
hash := sha256.New()
for _, seed := range seeds {
hash.Write([]byte(seed))
}
for _, filename := range filenames {
fileContents, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
hash.Write([]byte(fileContents))
}
return fmt.Sprintf("%x", hash.Sum(nil))
}
func reloadStaticFiles() {
application.BackblazeAccessKeyId = os.Getenv("SEQUENTIALREAD_PWM_BACKBLAZE_ACCESS_KEY_ID")
application.BackblazeSecretAccessKey = os.Getenv("SEQUENTIALREAD_PWM_BACKBLAZE_SECRET_ACCESS_KEY")
application.BackblazeBucketName = os.Getenv("SEQUENTIALREAD_PWM_BACKBLAZE_BUCKET_NAME")
application.BackblazeBucketRegion = os.Getenv("SEQUENTIALREAD_PWM_BACKBLAZE_BUCKET_REGION")
application.Version = hashFiles(
[]string{
application.BackblazeAccessKeyId,
application.BackblazeSecretAccessKey,
application.BackblazeBucketName,
application.BackblazeBucketRegion,
},
[]string{
"index.html.gotemplate",
"static/application.css",
"static/application.js",
"static/serviceworker.js",
"static/s3Client.js",
"static/scryptWebWorker.js",
"static/vendor/scrypt_wasm_bg.wasm",
"static/vendor/sjcl.js",
"static/vendor/cryptoWordList.js",
},
)[:6]
indexTemplate = loadTemplate("index.html.gotemplate")
}
func main() {
GoDotEnvLoad()
dataPath = filepath.Join(".", "data")
os.MkdirAll(dataPath, os.ModePerm)
reloadStaticFiles()
http.HandleFunc("/", indexHtml)
http.HandleFunc("/serviceworker.js", func(response http.ResponseWriter, request *http.Request) {
file, _ := os.OpenFile("static/serviceworker.js", os.O_RDONLY, 0755)
defer file.Close()
response.Header().Set("Etag", application.Version)
response.Header().Set("Content-Type", "application/javascript")
io.Copy(response, file)
})
http.HandleFunc("/version", func(response http.ResponseWriter, request *http.Request) {
response.WriteHeader(200)
fmt.Fprint(response, application.Version)
})
http.HandleFunc("/storage/", storage)
http.HandleFunc("/error", func(response http.ResponseWriter, request *http.Request) {
response.WriteHeader(200)
fmt.Fprint(response, "serviceworker request failed")
})
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
var headless = flag.Bool("headless", false, "headless server mode")
var tlsFlag = flag.String("tls", "", "path to tlsFlag cert/key (for example, entering \"test\" will resolve \"./test.key\" and \"./test.pem\"")
flag.Parse()
if headless == nil || *headless == false {
go func() {
for true {
reloadStaticFiles()
time.Sleep(time.Second * 2)
}
}()
go func() {
var appUrl = "http://localhost:" + appPort
if tlsFlag != nil && *tlsFlag != "" {
appUrl = "https://localhost:" + appPort
}
var serverIsRunning = false
var attempts = 0
for !serverIsRunning && attempts < 15 {
attempts += 1
time.Sleep(time.Millisecond * 500)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
response, err := client.Get(appUrl)
if err == nil && response.StatusCode == 200 {
serverIsRunning = true
}
}
openUrl(appUrl)
}()
}
if tlsFlag != nil && *tlsFlag != "" {
err := http.ListenAndServeTLS(":"+appPort, fmt.Sprintf("%s.pem", *tlsFlag), fmt.Sprintf("%s.key", *tlsFlag), nil)
panic(err)
} else {
err := http.ListenAndServe(":"+appPort, nil)
panic(err)
}
}
func openUrl(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
fmt.Printf("can't open app in web browser because '%s'\n", err)
}
}