github.com/go-kivik/kivik/v4@v4.3.2/x/kivikd/serve.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 // use this file except in compliance with the License. You may obtain a copy of 3 // the License at 4 // 5 // http://www.apache.org/licenses/LICENSE-2.0 6 // 7 // Unless required by applicable law or agreed to in writing, software 8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 // License for the specific language governing permissions and limitations under 11 // the License. 12 13 //go:build !js 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 // Service defines a CouchDB-like service to serve. You will define one of these 37 // per server endpoint. 38 type Service struct { 39 // Client is an instance of a driver.Client, which will be served. 40 Client *kivik.Client 41 // UserStore provides access to the user database. This is passed to auth 42 // handlers, and is used to authenticate sessions. If unset, a nil UserStore 43 // will be used which authenticates all uses. PERPETUAL ADMIN PARTY! 44 UserStore authdb.UserStore 45 // AuthHandler is a slice of authentication handlers. If no auth 46 // handlers are configured, the server will operate as a PERPETUAL 47 // ADMIN PARTY! 48 AuthHandlers []auth.Handler 49 // CompatVersion is the compatibility version to report to clients. Defaults 50 // to 1.6.1. 51 CompatVersion string 52 // VendorVersion is the vendor version string to report to clients. Defaults to the library 53 // version. 54 VendorVersion string 55 // VendorName is the vendor name string to report to clients. Defaults to the library 56 // vendor string. 57 VendorName string 58 // Favicon is the path to a file to serve as favicon.ico. If unset, a default 59 // image is used. 60 Favicon string 61 // RequestLogger receives logging information for each request. 62 RequestLogger logger.RequestLogger 63 64 // ConfigFile is the path to a config file to read during startup. 65 ConfigFile string 66 67 // Config is a complete config object. If this is set, config loading is 68 // bypassed. 69 Config *conf.Conf 70 71 conf *conf.Conf 72 confMU sync.RWMutex 73 74 // authHandlers is a map version of AuthHandlers for easier internal 75 // use. 76 authHandlers map[string]auth.Handler 77 authHandlerNames []string 78 } 79 80 // Init initializes a configured server. This is automatically called when 81 // Start() is called, so this is meant to be used if you want to bind the server 82 // yourself. 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 // Conf returns the initialized server configuration. 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 // Start begins serving connections. 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 // Bind sets the HTTP daemon bind address and port. 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 // typeText = "text/plain" 186 typeForm = "application/x-www-form-urlencoded" 187 // typeMForm = "multipart/form-data" 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 }