server.go 4.18 KB
Newer Older
ale's avatar
ale committed
1
2
3
package server

import (
ale's avatar
ale committed
4
	"encoding/json"
5
	"fmt"
ale's avatar
ale committed
6
7
	"log"
	"net/http"
8
	"reflect"
ale's avatar
ale committed
9
10
11

	"git.autistici.org/ai3/go-common/serverutil"

ale's avatar
ale committed
12
	as "git.autistici.org/ai3/accountserver"
ale's avatar
ale committed
13
14
)

15
16
type actionRegistry struct {
	handlers map[string]reflect.Type
ale's avatar
ale committed
17
18
}

19
20
21
func newActionRegistry() *actionRegistry {
	return &actionRegistry{
		handlers: make(map[string]reflect.Type),
22
	}
ale's avatar
ale committed
23
24
}

25
26
func (r *actionRegistry) Register(path string, rtype as.Request) {
	r.handlers[path] = reflect.ValueOf(rtype).Elem().Type()
ale's avatar
ale committed
27
28
}

29
30
31
32
33
34
func (r *actionRegistry) newRequest(path string) (as.Request, bool) {
	h, ok := r.handlers[path]
	if !ok {
		return nil, false
	}
	return reflect.New(h).Interface().(as.Request), true
35
36
}

37
38
// APIServer is the HTTP API interface to AccountService.
type APIServer struct {
39
40
	*actionRegistry
	service *as.AccountService
ale's avatar
ale committed
41
42
}

43
44
45
// New creates a new APIServer.
func New(service *as.AccountService, backend as.Backend) *APIServer {
	s := &APIServer{
46
47
48
		actionRegistry: newActionRegistry(),
		service:        service,
	}
49

50
51
52
53
	s.Register("/api/user/get", &as.GetUserRequest{})
	s.Register("/api/user/create", &as.CreateUserRequest{})
	s.Register("/api/user/update", &as.UpdateUserRequest{})
	s.Register("/api/user/change_password", &as.ChangeUserPasswordRequest{})
54
	s.Register("/api/user/set_account_recovery_hint", &as.SetAccountRecoveryHintRequest{})
55
56
57
58
59
60
61
62
63
64
65
	s.Register("/api/user/enable_otp", &as.EnableOTPRequest{})
	s.Register("/api/user/disable_otp", &as.DisableOTPRequest{})
	s.Register("/api/user/create_app_specific_password", &as.CreateApplicationSpecificPasswordRequest{})
	s.Register("/api/user/delete_app_specific_password", &as.DeleteApplicationSpecificPasswordRequest{})
	s.Register("/api/resource/enable", &as.EnableResourceRequest{})
	s.Register("/api/resource/disable", &as.DisableResourceRequest{})
	s.Register("/api/resource/create", &as.CreateResourcesRequest{})
	s.Register("/api/resource/move", &as.MoveResourceRequest{})
	s.Register("/api/resource/reset_password", &as.ResetPasswordRequest{})
	s.Register("/api/resource/email/add_alias", &as.AddEmailAliasRequest{})
	s.Register("/api/resource/email/delete_alias", &as.DeleteEmailAliasRequest{})
66
	s.Register("/api/recover_account", &as.AccountRecoveryRequest{})
67
68

	return s
69
70
}

71
var emptyResponse struct{}
72

73
74
75
76
type jsonError interface {
	JSON() []byte
}

77
func (s *APIServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
78
79
80
81
82
83
84
85
86
87
88
	// Create a new empty request object based on the request
	// path, then decode the HTTP request JSON body onto it.
	r, ok := s.newRequest(req.URL.Path)
	if !ok {
		http.NotFound(w, req)
		return
	}
	if !serverutil.DecodeJSONRequest(w, req, r) {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}
89

90
91
	resp, err := s.service.Handle(req.Context(), r)
	if err != nil {
92
93
94
95
96
97
98
99
100
		// Handle structured errors, serve a JSON response.
		status := errToStatus(err)
		if jerr, ok := err.(jsonError); ok {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(status)
			w.Write(jerr.JSON()) // nolint
		} else {
			http.Error(w, err.Error(), status)
		}
101
102
103
104
	} else {
		// Don't send nulls, send empty dicts instead.
		if resp == nil {
			resp = emptyResponse
105
		}
ale's avatar
ale committed
106
		serverutil.EncodeJSONResponse(w, resp)
107
	}
ale's avatar
ale committed
108

109
110
111
112
113
114
115
116
117
	// Now that all is done, we can log the request/response
	// (sanitization might modify the objects in place).
	reqData := marshalJSONSanitized(r)
	if err != nil {
		log.Printf("request: %s -> %v", reqData, err)
	} else {
		respData := marshalJSONSanitized(resp)
		log.Printf("request: %s -> %s", reqData, respData)
	}
ale's avatar
ale committed
118
}
ale's avatar
ale committed
119
120
121
122
123
124

func errToStatus(err error) int {
	switch {
	case err == as.ErrUserNotFound, err == as.ErrResourceNotFound:
		return http.StatusNotFound
	case as.IsAuthError(err):
ale's avatar
ale committed
125
		return http.StatusForbidden
ale's avatar
ale committed
126
127
128
129
130
131
132
	case as.IsRequestError(err):
		return http.StatusBadRequest
	default:
		return http.StatusInternalServerError
	}
}

133
134
135
136
137
138
139
140
141
142
// Some requests contain private information that should not be
// logged: these objects should implement a Sanitize() method that
// modifies the object in-place by editing out the private fields.
type hasSanitize interface {
	Sanitize()
}

func marshalJSONSanitized(obj interface{}) string {
	if s, ok := obj.(hasSanitize); ok {
		s.Sanitize()
ale's avatar
ale committed
143
	}
144
	data, err := json.Marshal(obj)
ale's avatar
ale committed
145
	if err != nil {
146
		return fmt.Sprintf("SERIALIZATION ERROR: %v", err)
ale's avatar
ale committed
147
148
149
	}
	return string(data)
}