commit a7e7d1d5189113e16d1cd8eb7a6d044d3eca0aca from: Oliver Lowe date: Wed Apr 13 07:55:18 2022 UTC wip commit - 5b8260a55717a51bd99ac6e148cd9bf179561126 commit + a7e7d1d5189113e16d1cd8eb7a6d044d3eca0aca blob - d12434921090a07f23a8ab1eec3054d934401d8a blob + 98a9f070192863dc80ac6d29db43f0aa03ec5064 --- client.go +++ client.go @@ -4,7 +4,10 @@ import ( "bytes" "encoding/json" "fmt" + "io" "net/http" + + "mailmux.net/aliases" ) const apiurl = "https://mailmux.net/v1/aliases" @@ -49,33 +52,33 @@ func (c *Client) Register(username, password string) e return fmt.Errorf("register %s: %s", username, rerror.Error) } -func (c *Client) NewAlias() (Alias, error) { - mcall := &Mcall{ +func (c *Client) NewAlias() (aliases.Aliases, error) { + tmsg := &Mcall{ Type: Tnew, Username: c.user, Password: c.token, } buf := &bytes.Buffer{} - if err := json.NewEncoder(buf).Encode(mcall); err != nil { - return Alias{}, fmt.Errorf("new alias: %w", err) + if err := json.NewEncoder(buf).Encode(tmsg); err != nil { + return nil, fmt.Errorf("new alias: %w", err) } + resp, err := http.Post(c.addr+"/aliases", jsonContentType, buf) if err != nil { - return Alias{}, fmt.Errorf("new alias: %w", err) + return nil, fmt.Errorf("new alias: %w", err) } - defer resp.Body.Close() rmsg, err := ParseMcall(resp.Body) if err != nil { - return Alias{}, fmt.Errorf("new alias: parse response: %w", err) + return nil, fmt.Errorf("new alias: parse response: %w", err) } if rmsg.Type == Rerror { - return Alias{}, fmt.Errorf("new alias: %v", rmsg.Error) + return nil, fmt.Errorf("new alias: %v", rmsg.Error) } - return rmsg.Aliases[0], nil + return rmsg.Aliases, nil } -func (c *Client) Aliases() ([]Alias, error) { +func (c *Client) Aliases() (aliases.Aliases, error) { tmsg := &Mcall{ Type: Tlist, Username: c.user, @@ -91,7 +94,10 @@ func (c *Client) Aliases() ([]Alias, error) { } defer resp.Body.Close() - rmsg, err := ParseMcall(resp.Body) + buf.Reset() + io.Copy(buf, resp.Body) + fmt.Println(string(buf.Bytes())) + rmsg, err := ParseMcall(buf) if err != nil { return nil, fmt.Errorf("list aliases: parse response: %w", err) } blob - 95eb5dc5f0570ad7be1eab427a0896ff34a8f85c blob + 51e0bf7797f62622a549f18aa0f43c9b2b4b2d2e --- cmd/mailmux/mailmux.go +++ cmd/mailmux/mailmux.go @@ -15,77 +15,82 @@ import ( ) type server struct { - seedfile string + seedfile string aliaspath string - aliases aliases.Aliases - users mailmux.UserStore + aliases aliases.Aliases + users mailmux.UserStore } func (srv *server) aliasHandler(w http.ResponseWriter, req *http.Request) { - if err := req.ParseForm(); err != nil { - w.WriteHeader(http.StatusBadRequest) + tmsg, err := mailmux.ParseMcall(req.Body) + if err != nil { + rerror(w, err.Error(), http.StatusBadRequest) return } - user := req.Form.Get("username") - if user == "" { - http.Error(w, "empty user", http.StatusBadRequest) - return - } - token := req.Form.Get("token") - if token == "" { - http.Error(w, "empty token", http.StatusBadRequest) - return - } - _, err := srv.users.Lookup(user) + _, err = srv.users.Lookup(tmsg.Username) if err != nil { - w.WriteHeader(http.StatusUnauthorized) + rerror(w, "unauthorised", http.StatusUnauthorized) log.Println(err) return } - switch req.Method { - case http.MethodGet: - srv.listAliasHandler(w, req) + // TODO authenticate user + + switch tmsg.Type { + case mailmux.Tnew: + srv.newAlias(w, tmsg) return - case http.MethodPost: - srv.newAliasHandler(w, req) + case mailmux.Tlist: + srv.listAliasHandler(w, tmsg) return - case http.MethodDelete: - srv.deleteAlias(w, req) - return } - w.WriteHeader(http.StatusMethodNotAllowed) + rerror(w, "not implemented yet", http.StatusNotImplemented) } -func (srv *server) listAliasHandler(w http.ResponseWriter, req *http.Request) { - username := req.Form.Get("username") +func (srv *server) listAliasHandler(w http.ResponseWriter, tmsg *mailmux.Mcall) { filtered := make(aliases.Aliases) for rcpt, dest := range srv.aliases { - if dest == username { + if dest == tmsg.Username { filtered[rcpt] = dest } } - if err := json.NewEncoder(w).Encode(filtered); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + rmsg := &mailmux.Mcall{ + Type: mailmux.Rlist, + Username: tmsg.Username, + Aliases: filtered, + } + if err := json.NewEncoder(w).Encode(rmsg); err != nil { + rerror(w, err.Error(), http.StatusInternalServerError) return } } -func (srv *server) newAliasHandler(w http.ResponseWriter, req *http.Request) { - username := req.Form.Get("username") +func (srv *server) newAlias(w http.ResponseWriter, tmsg *mailmux.Mcall) { + if tmsg.Type != mailmux.Tnew { + rerror(w, "message type is not Tnew", http.StatusBadRequest) + return + } + rcpt, err := mailmux.RandomUsername(srv.seedfile) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + s := fmt.Sprintf("random username: %v", err) + rerror(w, s, http.StatusInternalServerError) return } - srv.aliases[rcpt] = username + srv.aliases[rcpt] = tmsg.Username if err := aliases.Put(srv.aliases, srv.aliaspath); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + s := fmt.Sprintf("put: %v", err) + rerror(w, s, http.StatusInternalServerError) return } - rcpt = rcpt + "@mailmux.net" - if err := json.NewEncoder(w).Encode(rcpt); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + + rnew := &mailmux.Mcall{ + Type: mailmux.Rnew, + Username: tmsg.Username, + Aliases: aliases.Aliases{rcpt: tmsg.Username}, + } + if err := json.NewEncoder(w).Encode(rnew); err != nil { + rerror(w, err.Error(), http.StatusInternalServerError) return } } @@ -113,13 +118,12 @@ func (srv *server) deleteAlias(w http.ResponseWriter, // with its Error field set to errormsg. // Just like http.Error, callers should ensure no further writes are done to w. func rerror(w http.ResponseWriter, errormsg string, status int) { - mcall := &mailmux.Mcall{Type: mailmux.Rerror, Error: errormsg} - if err := json.NewEncoder(w).Encode(mcall); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } w.WriteHeader(status) - return + rmsg := &mailmux.Mcall{Type: mailmux.Rerror, Error: errormsg} + if err := json.NewEncoder(w).Encode(rmsg); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } func main() { blob - 52f9c160dcf7b2ab08ae17f9b0923353ce2d9b0f blob + 26966c2e5dd6cb30b90898b3dedd30a52773a6a6 --- cmd/mailmux/mailmux_test.go +++ cmd/mailmux/mailmux_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "net" "net/http" "os" @@ -8,6 +9,7 @@ import ( "testing" mailmux "mailmux.net" + "mailmux.net/aliases" ) func newTestServerClient(t *testing.T) *mailmux.Client { @@ -21,6 +23,7 @@ func newTestServerClient(t *testing.T) *mailmux.Client } srv := &server{ users: db, + aliases: make(aliases.Aliases), aliaspath: path.Join(dir, "aliases"), seedfile: "/usr/share/dict/words", } @@ -30,22 +33,27 @@ func newTestServerClient(t *testing.T) *mailmux.Client t.Fatal(err) } - http.HandleFunc("/register", srv.registerHandler) - http.HandleFunc("/aliases", srv.aliasHandler) + mux := http.NewServeMux() + mux.HandleFunc("/register", srv.registerHandler) + mux.HandleFunc("/aliases", srv.aliasHandler) go func() { - t.Fatal(http.Serve(ln, nil)) + t.Fatal(http.Serve(ln, mux)) }() - return mailmux.Dial("http://" + ln.Addr().String(), "test", "test") + client := mailmux.Dial("http://"+ln.Addr().String(), "test@example.com", "test") + if err := client.Register("test@example.com", "test"); err != nil { + t.Fatal(err) + } + return client } func TestBadRegistration(t *testing.T) { client := newTestServerClient(t) registrations := map[string]string{ - "djfkjskdjf": "dfjkdkfjsd", - "": "asdfgjkl", - "fjdklskjdsf": "", - "@@@@": "dfjksjkdf", + "djfkjskdjf": "dfjkdkfjsd", + "": "asdfgjkl", + "fjdklskjdsf": "", + "@@@@": "dfjksjkdf", "foo@example.com": "", } for username, password := range registrations { @@ -55,3 +63,19 @@ func TestBadRegistration(t *testing.T) { } } } + +func TestAliases(t *testing.T) { + client := newTestServerClient(t) + for i := 0; i <= 100; i++ { + a, err := client.NewAlias() + if err != nil { + t.Fatal(err) + } + fmt.Println(a) + } + a, err := client.Aliases() + if err != nil { + t.Fatal(err) + } + fmt.Println(a) +} blob - 46f5ee368b9a28a1cf6f475ad9dfa4a501d8d6e9 blob + 9428a3263c8c7a06be0274eedf32b489c0ca49f3 --- cmd/mailmux/register.go +++ cmd/mailmux/register.go @@ -11,12 +11,12 @@ import ( func (srv *server) registerUser(name string, pw mailmux.Password) error { _, err := srv.users.Lookup(name) - if err == nil { - return mailmux.ErrUserExist - } if errors.Is(err, mailmux.ErrUnknownUser) { return srv.users.Change(name, pw) } + if err == nil { + return mailmux.ErrUserExist + } return err } @@ -26,28 +26,29 @@ func (srv *server) registerHandler(w http.ResponseWrit return } - var mcall mailmux.Mcall - if err := json.NewDecoder(req.Body).Decode(&mcall); err != nil { + var tmsg mailmux.Mcall + if err := json.NewDecoder(req.Body).Decode(&tmsg); err != nil { rerror(w, err.Error(), http.StatusBadRequest) return } - if mcall.Type != mailmux.Tregister { - s := fmt.Sprintf("mcall type %d is not Tregister", mcall.Type) + if tmsg.Type != mailmux.Tregister { + s := fmt.Sprintf("mcall type %d is not Tregister", tmsg.Type) rerror(w, s, http.StatusBadRequest) return } - if mcall.Username == "" { + if tmsg.Username == "" { rerror(w, "empty username", http.StatusBadRequest) return } - if mcall.Password == "" { + if tmsg.Password == "" { rerror(w, "empty password", http.StatusBadRequest) return } - err := srv.registerUser(mcall.Username, mailmux.Password(mcall.Password)) + err := srv.registerUser(tmsg.Username, mailmux.Password(tmsg.Password)) if err != nil { rerror(w, err.Error(), http.StatusInternalServerError) + return } } blob - b3008bfc6eabb6e3a0750bd60e55419257c9291e blob + f147c4457cb40a47a4144343f9916eac478b7d86 --- mcall.go +++ mcall.go @@ -5,6 +5,8 @@ import ( "errors" "io" "time" + + "mailmux.net/aliases" ) const ( @@ -25,11 +27,11 @@ const ( // This design is loosely based on the Plan 9 network file protocol 9P. type Mcall struct { Type uint - Username string // Tregister, Rregister - Password string // Tregister - Error string // Rerror - Aliases []Alias // Rnew, Rlist, Tremove - Expiry time.Time // Tnew, Rnew + Username string // Tregister, Rregister + Password string // Tregister + Error string // Rerror + Aliases aliases.Aliases // Rnew, Rlist, Tremove + Expiry time.Time // Tnew, Rnew } // ParseMcall parses and validates one Mcall from r. @@ -38,17 +40,17 @@ func ParseMcall(r io.Reader) (*Mcall, error) { if err := json.NewDecoder(r).Decode(&mc); err != nil { return nil, err } - if mc.Username == "" { - return nil, errors.New("empty username") + switch mc.Type { + case Rerror: + if mc.Error == "" { + return nil, errors.New("empty error message") + } + case Tregister, Tnew, Tlist, Tremove: + if mc.Username == "" { + return nil, errors.New("empty username") + } else if mc.Password == "" { + return nil, errors.New("empty password") + } } - if mc.Password == "" { - return nil, errors.New("empty password") - } - if mc.Type != Rerror && mc.Error != "" { - return nil, errors.New("non-empty error field") - } - if mc.Type == Rerror && mc.Error == "" { - return nil, errors.New("empty error message") - } return &mc, nil }