hlfw.ca

webbing

Download patch

ref: 05d82d371ca4a2c56aaccdf65899e02b356a7950
author: halfwit <michaelmisch1985@gmail.com>
date: Sat Mar 28 07:39:46 PDT 2020

move router

--- /dev/null
+++ b/footer.go
@@ -1,0 +1,33 @@
+package router
+
+import (
+	"golang.org/x/text/message"
+)
+
+func footer(p *message.Printer) map[string]string {
+	return map[string]string{
+		"faq":      p.Sprintf("FAQ"),
+		"help":     p.Sprintf("Help"),
+		"banner":   p.Sprintf("Quality Healthcare"),
+		"pay":      p.Sprintf("Payment Methods"),
+		"fees":     p.Sprintf("Prices and Fees"),
+		"verify":   p.Sprintf("Verification"),
+		"appt":     p.Sprintf("Appointments"),
+		"legal":    p.Sprintf("Legal"),
+		"privacy":  p.Sprintf("Privacy Policy"),
+		"howworks": p.Sprint("How It Works"),
+		"contact":  p.Sprint("Contact Us"),
+		"pricing":  p.Sprint("Pricing"),
+		"catalog":  p.Sprint("Catalog"),
+		"appts":    p.Sprint("Appointments"),
+		"proc":     p.Sprint("Payment Procedures"),
+		"payments": p.Sprint("Payment Methods"),
+		"phone":    p.Sprint("Call toll free"),
+		"number":   p.Sprint("1(555)555-1234"),
+		"email":    p.Sprint("Email"),
+		"partHead": p.Sprintf("Work with us!"),
+		"partner":  p.Sprintf("Become A Partner"),
+		"provider": p.Sprint("Become A Provider"),
+		"copy":     p.Sprintf("Copyright 2017, 2018, 2019"),
+	}
+}
--- /dev/null
+++ b/forms.go
@@ -1,0 +1,66 @@
+package router
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/olmaxmedical/olmax_go/session"
+	"golang.org/x/text/message"
+)
+
+var formlist map[string]*Form
+
+// Form - POST requests
+type Form struct {
+	Access    Access
+	After     PluginMask
+	Path      string
+	Redirect  string
+	Validator func(r *http.Request, p *message.Printer) []string
+}
+
+func init() {
+	formlist = make(map[string]*Form)
+}
+
+// AddPost - Register a POST form from forms/
+func AddPost(f *Form) {
+	formlist[f.Path+".html"] = f
+}
+
+func parseForm(p *Request, w http.ResponseWriter, r *http.Request) (*Form, []string) {
+	var errors []string
+	form, ok := formlist[p.path]
+	if !ok {
+		errors = append(errors, "No such page")
+		return nil, errors
+	}
+	if errs := form.Validator(r, p.printer); len(errs) > 0 {
+		return nil, errs
+	}
+	for _, key := range pluginKey {
+		if (form.After&key) != 0 && pluginCache[key].Validate != nil {
+			if e := pluginCache[key].Validate(p); e != nil {
+				errors = append(errors, fmt.Sprint(e))
+				return nil, errors
+			}
+		}
+	}
+	return form, errors
+}
+
+func postform(p *Request, us session.Session, w http.ResponseWriter, r *http.Request) {
+	form, errors := parseForm(p, w, r)
+	if len(errors) > 0 && errors[0] != "nil" {
+		// NOTE(halfwit) this stashes previous entries, but does not work
+		// on multipart forms (with file uploads)
+		us.Set("errors", errors)
+		// Maybe store form args instead here in session
+		url := fmt.Sprintf("%s?%s", r.URL.String(), r.Form.Encode())
+		http.Redirect(w, r, url, 302)
+	}
+	if form != nil {
+		us.Set("errors", []string{})
+		http.Redirect(w, r, form.Redirect, 302)
+	}
+}
--- /dev/null
+++ b/handler.go
@@ -1,0 +1,145 @@
+package router
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/olmaxmedical/olmax_go/db"
+	"github.com/olmaxmedical/olmax_go/email"
+	"github.com/olmaxmedical/olmax_go/session"
+	"golang.org/x/text/message"
+)
+
+// Handle specific endpoints
+type handler struct {
+	manager *session.Manager
+}
+
+func (d *handler) logout(w http.ResponseWriter, r *http.Request) {
+	d.manager.Destroy(w, r)
+	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+	http.Redirect(w, r, "/index.html", 302)
+}
+
+func (d *handler) normal(w http.ResponseWriter, r *http.Request) {
+	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+	if r.URL.Path == "/" {
+		http.Redirect(w, r, "/index.html", 302)
+		return
+	}
+	user, status, us, role := d.getUser(w, r)
+	p := &Request{
+		printer: userLang(r),
+		status:  status,
+		request: r,
+		user:    user,
+		role:    role,
+		session: us,
+		path:    r.URL.Path[1:],
+	}
+
+	switch r.Method {
+	case "GET":
+		getpage(p, w)
+	case "POST":
+		postform(p, us, w, r)
+	}
+}
+
+// TODO: This will require actual client data from the database to populate the page
+func (d *handler) profile(w http.ResponseWriter, r *http.Request) {
+	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+	user, status, us, role := d.getUser(w, r)
+	if status == "false" {
+		http.Redirect(w, r, "/login.html", 302)
+		return
+	}
+	if rd, ok := us.Get("redirect").(string); ok {
+		us.Delete("redirect")
+		http.Redirect(w, r, "/"+rd, 302)
+		return
+	}
+	p := &Request{
+		printer: userLang(r),
+		status:  status,
+		session: us,
+		user:    user,
+		role:    role,
+	}
+	var data []byte
+	var err error
+	switch db.UserRole(user) {
+	case db.DoctorAuth:
+		if role != db.DoctorAuth {
+			http.Error(w, "Unauthorized", 401)
+			return
+		}
+		p.path = "doctor/profile.html"
+		data, err = getData(p, "doctor")
+	case db.PatientAuth:
+		if role != db.PatientAuth {
+			http.Error(w, "Unauthorized", 401)
+			return
+		}
+		p.path = "patient/profile.html"
+		data, err = getData(p, "patient")
+	default:
+		http.Error(w, "Forbidden", 403)
+		return
+	}
+	if err != nil {
+		http.Error(w, "Service Unavailable", 503)
+		return
+	}
+	fmt.Fprintf(w, "%s", data)
+}
+
+func (d *handler) activate(w http.ResponseWriter, r *http.Request) {
+	w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+	if len(r.URL.Path) != 46 && r.URL.Path[:9] != "/activate" {
+		http.Error(w, "Bad Request", 400)
+		return
+	}
+	email.ValidateSignupToken(w, r, r.URL.Path[10:])
+}
+
+func (d *handler) reset(w http.ResponseWriter, r *http.Request) {
+	if len(r.URL.Path) != 43 && r.URL.Path[:6] != "/reset" {
+		http.Error(w, "Bad Request", 400)
+		return
+	}
+	p := userLang(r)
+	user, _, us, _ := d.getUser(w, r)
+	token := email.NextResetToken(r.URL.Path[7:], user)
+	if token == "" {
+		us.Set("errors", [1]string{p.Sprint("Token expired")})
+		return
+	}
+	us.Set("token", token)
+	r.URL.Path = "/newpassword.html"
+	d.normal(w, r)
+}
+
+func (d *handler) getUser(w http.ResponseWriter, r *http.Request) (string, string, session.Session, db.Access) {
+	us := d.manager.Start(w, r)
+	user, ok1 := us.Get("username").(string)
+	status, ok2 := us.Get("login").(string)
+	role, ok3 := us.Get("role").(db.Access)
+	if !ok1 || !ok2 || status != "true" {
+		status = "false"
+	}
+	if !ok3 {
+		role = db.GuestAuth
+	}
+	if status == "true" {
+		us.Set("token", db.NewToken())
+	}
+	return user, status, us, role
+}
+
+func userLang(r *http.Request) *message.Printer {
+	accept := r.Header.Get("Accept-Language")
+	lang := r.FormValue("lang")
+	tag := message.MatchLanguage(lang, accept)
+	return message.NewPrinter(tag)
+}
--- /dev/null
+++ b/header.go
@@ -1,0 +1,16 @@
+package router
+
+import (
+	"golang.org/x/text/message"
+)
+
+func header(p *message.Printer, status string) map[string]string {
+	return map[string]string{
+		"home":    p.Sprint("Home"),
+		"login":   p.Sprint("Login"),
+		"logout":  p.Sprint("Logout"),
+		"signup":  p.Sprint("Sign Up"),
+		"profile": p.Sprint("Profile"),
+		"status":  status,
+	}
+}
--- /dev/null
+++ b/pages.go
@@ -1,0 +1,155 @@
+package router
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"html/template"
+	"net/http"
+	"os"
+	"path"
+	"strings"
+
+	"github.com/olmaxmedical/olmax_go/db"
+	"golang.org/x/text/message"
+)
+
+var pagecache map[string]*Page
+
+func init() {
+	pagecache = make(map[string]*Page)
+}
+
+// Access defines the access rights for a specific page
+type Access uint8
+
+const (
+	// GuestAuth - non registered user
+	GuestAuth Access = 1 << iota
+	// PatientAuth - normal user, added by registration process
+	PatientAuth
+	// DoctorAuth - manually added to the database
+	DoctorAuth
+)
+
+// Page defines what a client receives from a GET request
+type Page struct {
+	Access Access
+	Extra  PluginMask
+	CSS    string
+	Path   string
+	Data   func(p *message.Printer) map[string]interface{}
+	tmpl   *template.Template
+}
+
+// AddPage - register a *Page to the cache
+func AddPage(p *Page) {
+	pagecache[p.Path+".html"] = p
+}
+
+// ValidatePages - Walk all our templates, test them, and finally return applicable errors as an array
+func ValidatePages() []error {
+	var errs []error
+	hd := path.Join("templates", "header.tpl")
+	fd := path.Join("templates", "footer.tpl")
+	ld := path.Join("templates", "layout.tpl")
+	extra, err := os.Open(path.Join("templates", "plugins"))
+	if err != nil {
+		errs = append(errs, errors.New("Unable to locate templates/plugins"))
+		return errs
+	}
+	dirs, err := extra.Readdirnames(0)
+	for n, dir := range dirs {
+		dirs[n] = path.Join("templates", "plugins", dir)
+	}
+	// TODO(halfwit) Validate our plugin templates here as well
+	dirs = append(dirs, hd, fd, ld)
+	printer := message.NewPrinter(message.MatchLanguage("en"))
+	for _, item := range pagecache {
+		var err error
+		tp := path.Join("templates", item.Path) + ".tpl"
+		t := template.New(path.Base(tp))
+		// TODO(halfwit) Contemplate only adding templates for plugins each page uses
+		item.tmpl, _ = t.ParseFiles(dirs...)
+		item.tmpl, err = t.ParseFiles(tp)
+		if err != nil {
+			errs = append(errs, fmt.Errorf("parsing in %s - %v", path.Dir(item.Path), err))
+			continue
+		}
+		p := &Request{
+			printer: printer,
+			path:    item.Path + ".html",
+			role:    db.PatientAuth | db.DoctorAuth | db.GuestAuth,
+		}
+		_, err = getData(p, "")
+		if err != nil {
+			errs = append(errs, err)
+		}
+	}
+	return errs
+}
+
+func getpage(p *Request, w http.ResponseWriter) {
+	var data []byte
+	var err error
+	switch db.UserRole(p.user) {
+	case db.DoctorAuth:
+		data, err = getData(p, "doctor")
+	case db.PatientAuth:
+		data, err = getData(p, "patient")
+	default:
+		data, err = getData(p, "guest")
+	}
+	if err != nil && err.Error() == "Unauthorized" {
+		p.Session().Set("redirect", p.path)
+		http.Redirect(w, p.Request(), "/login.html", 302)
+		return
+	}
+	if err != nil {
+		http.Error(w, "Service Unavailable", 503)
+		return
+	}
+	fmt.Fprintf(w, "%s", data)
+}
+
+func getData(p *Request, in string) ([]byte, error) {
+	cache, ok := pagecache[p.path]
+	if !ok {
+		return nil, fmt.Errorf("No such page: %s", p.path)
+	}
+	if uint8(p.role)&uint8(cache.Access) == 0 {
+		return nil, errors.New("Unauthorized")
+	}
+	r := cache.Data(p.printer)
+	r["css"] = cache.CSS
+	r["header"] = header(p.printer, p.status)
+	r["footer"] = footer(p.printer)
+	r["basedir"] = getBaseDir(cache.Path)
+	// TODO(halfwit) Test chunking in to go routines if n gets too large
+	for _, key := range pluginKey {
+		if (cache.Extra&key) != 0 && pluginCache[key].Run != nil {
+			r[pluginCache[key].Name] = pluginCache[key].Run(p)
+		}
+	}
+	if p.session != nil {
+		r["username"] = p.session.Get("username")
+		if _, ok := p.session.Get("redirect").(string); ok {
+			r["redirect"] = true
+		}
+	}
+	return cache.render(r)
+}
+
+func (page *Page) render(i map[string]interface{}) ([]byte, error) {
+	var buf bytes.Buffer
+	data := bufio.NewWriter(&buf)
+	err := page.tmpl.ExecuteTemplate(data, "layout", i)
+	data.Flush()
+	return buf.Bytes(), err
+
+}
+
+func getBaseDir(fp string) string {
+	return strings.Repeat("../", strings.Count(fp, "/"))
+}
--- /dev/null
+++ b/plugins.go
@@ -1,0 +1,49 @@
+package router
+
+import (
+	"fmt"
+)
+
+// PluginMask - (Must be unique) ID for a plugin
+type PluginMask uint32
+
+// DEAD is a magic string to indicate a non-unique plugin key
+const DEAD PluginMask = 1
+
+var pluginCache map[PluginMask]*Plugin
+var pluginKey []PluginMask
+
+// Plugin - Provide extra data or functionality from GET/POST pages
+type Plugin struct {
+	Name     string
+	Run      func(p *Request) map[string]interface{}
+	Validate func(p *Request) error
+}
+
+func init() {
+	pluginCache = make(map[PluginMask]*Plugin)
+}
+
+// ValidatePlugins - Make sure that its mapping isn't redundant with any other
+// Plugins have external testing to validate they are correct
+func ValidatePlugins() []error {
+	errs := []error{}
+	for key, item := range pluginCache {
+		if item.Validate == nil {
+			continue
+		}
+		if (key & DEAD) != 0 {
+			errs = append(errs, fmt.Errorf("Error registering %s: Key requested already in use", item.Name))
+		}
+	}
+	return errs
+}
+
+// AddPlugin - Add Plugin to map by key
+func AddPlugin(p *Plugin, key PluginMask) {
+	if pluginCache[key] != nil {
+		key |= DEAD
+	}
+	pluginKey = append(pluginKey, key)
+	pluginCache[key] = p
+}
--- /dev/null
+++ b/request.go
@@ -1,0 +1,35 @@
+package router
+
+import (
+	"net/http"
+
+	"github.com/olmaxmedical/olmax_go/db"
+	"github.com/olmaxmedical/olmax_go/session"
+	"golang.org/x/text/message"
+)
+
+// Request represents an incoming GET/POST
+type Request struct {
+	printer *message.Printer
+	session session.Session
+	request *http.Request
+	user    string
+	status  string
+	path    string
+	role    db.Access
+}
+
+// Printer - returns the client's localized printer handler
+func (r *Request) Printer() *message.Printer {
+	return r.printer
+}
+
+// Session - returns the client's session
+func (r *Request) Session() session.Session {
+	return r.session
+}
+
+// Request - underlying http.Request for forms and such
+func (r *Request) Request() *http.Request {
+	return r.request
+}
--- /dev/null
+++ b/run.go
@@ -1,0 +1,35 @@
+package router
+
+import (
+	"crypto/tls"
+	"net/http"
+
+	"github.com/olmaxmedical/olmax_go/session"
+)
+
+// Route - All requests pass through here first
+func Route(manager *session.Manager) error {
+	d := &handler{
+		manager: manager,
+	}
+	css := http.FileServer(http.Dir("resources/css/"))
+	jss := http.FileServer(http.Dir("resources/scripts"))
+	img := http.FileServer(http.Dir("resources/images/"))
+	mux := http.NewServeMux()
+	mux.Handle("/css/", http.StripPrefix("/css/", css))
+	mux.Handle("/scripts/", http.StripPrefix("/scripts/", jss))
+	mux.Handle("/images/", http.StripPrefix("/images/", img))
+	mux.HandleFunc("/activate/", d.activate)
+	mux.HandleFunc("/reset/", d.reset)
+	mux.HandleFunc("/logout.html", d.logout)
+	mux.HandleFunc("/profile.html", d.profile)
+	mux.HandleFunc("/", d.normal)
+	//from https://github.com/denji/golang-tls (creative commons)
+	srv := &http.Server{
+		Addr:         ":8443",
+		Handler:      mux,
+		TLSConfig:    getTlsConfig(),
+		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
+	}
+	return srv.ListenAndServeTLS("cert.pem", "key.pem")
+}
--- /dev/null
+++ b/tls.go
@@ -1,0 +1,21 @@
+package router
+
+import (
+	"crypto/tls"
+)
+
+func getTlsConfig() *tls.Config {
+	tlsConfig := &tls.Config{
+		MinVersion:               tls.VersionTLS12,
+		CurvePreferences:         []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
+		PreferServerCipherSuites: true,
+		CipherSuites: []uint16{
+			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+			tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+			tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+			tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+		},
+	}
+	tlsConfig.BuildNameToCertificate()
+	return tlsConfig
+}