...

Source file src/github.com/go-kivik/kivik/v4/x/kivikd/serve.go

Documentation: github.com/go-kivik/kivik/v4/x/kivikd

     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  }
   214  

View as plain text