...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package kivikd
16
17 import (
18 "context"
19 "encoding/json"
20 errs "errors"
21 "fmt"
22 "net/http"
23 "os"
24 "strconv"
25 "strings"
26 "sync"
27
28 "github.com/go-kivik/kivik/v4"
29 internal "github.com/go-kivik/kivik/v4/int/errors"
30 "github.com/go-kivik/kivik/v4/x/kivikd/auth"
31 "github.com/go-kivik/kivik/v4/x/kivikd/authdb"
32 "github.com/go-kivik/kivik/v4/x/kivikd/conf"
33 "github.com/go-kivik/kivik/v4/x/kivikd/logger"
34 )
35
36
37
38 type Service struct {
39
40 Client *kivik.Client
41
42
43
44 UserStore authdb.UserStore
45
46
47
48 AuthHandlers []auth.Handler
49
50
51 CompatVersion string
52
53
54 VendorVersion string
55
56
57 VendorName string
58
59
60 Favicon string
61
62 RequestLogger logger.RequestLogger
63
64
65 ConfigFile string
66
67
68
69 Config *conf.Conf
70
71 conf *conf.Conf
72 confMU sync.RWMutex
73
74
75
76 authHandlers map[string]auth.Handler
77 authHandlerNames []string
78 }
79
80
81
82
83 func (s *Service) Init() (http.Handler, error) {
84 s.authHandlersSetup()
85 if err := s.loadConf(); err != nil {
86 return nil, err
87 }
88 if !s.Conf().IsSet("couch_httpd_auth.secret") {
89 fmt.Fprintf(os.Stderr, "couch_httpd_auth.secret is not set. This is insecure!\n")
90 }
91 return s.setupRoutes()
92 }
93
94 func (s *Service) loadConf() error {
95 s.confMU.Lock()
96 defer s.confMU.Unlock()
97 if s.Config != nil {
98 s.conf = s.Config
99 return nil
100 }
101 c, err := conf.Load(s.ConfigFile)
102 if err != nil {
103 return err
104 }
105 s.conf = c
106 return nil
107 }
108
109
110 func (s *Service) Conf() *conf.Conf {
111 s.confMU.RLock()
112 defer s.confMU.RUnlock()
113 if s.Config != nil {
114 s.confMU.RUnlock()
115 if err := s.loadConf(); err != nil {
116 panic(err)
117 }
118 s.confMU.RLock()
119 }
120 return s.conf
121 }
122
123
124 func (s *Service) Start() error {
125 server, err := s.Init()
126 if err != nil {
127 return err
128 }
129 addr := fmt.Sprintf("%s:%d",
130 s.Conf().GetString("httpd.bind_address"),
131 s.Conf().GetInt("httpd.port"),
132 )
133 fmt.Fprintf(os.Stderr, "Listening on %s\n", addr)
134 return http.ListenAndServe(addr, server)
135 }
136
137 func (s *Service) authHandlersSetup() {
138 if s.AuthHandlers == nil || len(s.AuthHandlers) == 0 {
139 fmt.Fprintf(os.Stderr, "No AuthHandler specified! Welcome to the PERPETUAL ADMIN PARTY!\n")
140 }
141 s.authHandlers = make(map[string]auth.Handler)
142 s.authHandlerNames = make([]string, 0, len(s.AuthHandlers))
143 for _, handler := range s.AuthHandlers {
144 name := handler.MethodName()
145 if _, ok := s.authHandlers[name]; ok {
146 panic(fmt.Sprintf("Multiple auth handlers for for `%s` registered", name))
147 }
148 s.authHandlers[name] = handler
149 s.authHandlerNames = append(s.authHandlerNames, name)
150 }
151 if s.UserStore == nil {
152 s.UserStore = &perpetualAdminParty{}
153 }
154 }
155
156 type perpetualAdminParty struct{}
157
158 var _ authdb.UserStore = &perpetualAdminParty{}
159
160 func (p *perpetualAdminParty) Validate(ctx context.Context, username, _ string) (*authdb.UserContext, error) {
161 return p.UserCtx(ctx, username)
162 }
163
164 func (p *perpetualAdminParty) UserCtx(_ context.Context, username string) (*authdb.UserContext, error) {
165 return &authdb.UserContext{
166 Name: username,
167 Roles: []string{"_admin"},
168 }, nil
169 }
170
171
172 func (s *Service) Bind(addr string) error {
173 port := addr[strings.LastIndex(addr, ":")+1:]
174 if _, err := strconv.Atoi(port); err != nil {
175 return fmt.Errorf("invalid port '%s': %w", port, err)
176 }
177 host := strings.TrimSuffix(addr, ":"+port)
178 s.Conf().Set("httpd.bind_address", host)
179 s.Conf().Set("httpd.port", port)
180 return nil
181 }
182
183 const (
184 typeJSON = "application/json"
185
186 typeForm = "application/x-www-form-urlencoded"
187
188 )
189
190 func reason(err error) string {
191 kerr := new(internal.Error)
192 if errs.As(err, &kerr) {
193 return kerr.Message
194 }
195 return err.Error()
196 }
197
198 func reportError(w http.ResponseWriter, err error) {
199 w.Header().Add("Content-Type", typeJSON)
200 status := kivik.HTTPStatus(err)
201 w.WriteHeader(status)
202 short := err.Error()
203 reason := reason(err)
204 if reason == "" {
205 reason = short
206 } else {
207 short = strings.ToLower(http.StatusText(status))
208 }
209 _ = json.NewEncoder(w).Encode(map[string]interface{}{
210 "error": short,
211 "reason": reason,
212 })
213 }
214
View as plain text