github.com/safing/portbase@v0.19.5/api/api_bridge.go (about)

     1  package api
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"net/url"
     9  	"path"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/safing/portbase/database"
    14  	"github.com/safing/portbase/database/record"
    15  	"github.com/safing/portbase/database/storage"
    16  )
    17  
    18  const (
    19  	endpointBridgeRemoteAddress = "websocket-bridge"
    20  	apiDatabaseName             = "api"
    21  )
    22  
    23  func registerEndpointBridgeDB() error {
    24  	if _, err := database.Register(&database.Database{
    25  		Name:        apiDatabaseName,
    26  		Description: "API Bridge",
    27  		StorageType: "injected",
    28  	}); err != nil {
    29  		return err
    30  	}
    31  
    32  	_, err := database.InjectDatabase("api", &endpointBridgeStorage{})
    33  	return err
    34  }
    35  
    36  type endpointBridgeStorage struct {
    37  	storage.InjectBase
    38  }
    39  
    40  // EndpointBridgeRequest holds a bridged request API request.
    41  type EndpointBridgeRequest struct {
    42  	record.Base
    43  	sync.Mutex
    44  
    45  	Method   string
    46  	Path     string
    47  	Query    map[string]string
    48  	Data     []byte
    49  	MimeType string
    50  }
    51  
    52  // EndpointBridgeResponse holds a bridged request API response.
    53  type EndpointBridgeResponse struct {
    54  	record.Base
    55  	sync.Mutex
    56  
    57  	MimeType string
    58  	Body     string
    59  }
    60  
    61  // Get returns a database record.
    62  func (ebs *endpointBridgeStorage) Get(key string) (record.Record, error) {
    63  	if key == "" {
    64  		return nil, database.ErrNotFound
    65  	}
    66  
    67  	return callAPI(&EndpointBridgeRequest{
    68  		Method: http.MethodGet,
    69  		Path:   key,
    70  	})
    71  }
    72  
    73  // Get returns the metadata of a database record.
    74  func (ebs *endpointBridgeStorage) GetMeta(key string) (*record.Meta, error) {
    75  	// This interface is an API, always return a fresh copy.
    76  	m := &record.Meta{}
    77  	m.Update()
    78  	return m, nil
    79  }
    80  
    81  // Put stores a record in the database.
    82  func (ebs *endpointBridgeStorage) Put(r record.Record) (record.Record, error) {
    83  	if r.DatabaseKey() == "" {
    84  		return nil, database.ErrNotFound
    85  	}
    86  
    87  	// Prepare data.
    88  	var ebr *EndpointBridgeRequest
    89  	if r.IsWrapped() {
    90  		// Only allocate a new struct, if we need it.
    91  		ebr = &EndpointBridgeRequest{}
    92  		err := record.Unwrap(r, ebr)
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  	} else {
    97  		var ok bool
    98  		ebr, ok = r.(*EndpointBridgeRequest)
    99  		if !ok {
   100  			return nil, fmt.Errorf("record not of type *EndpointBridgeRequest, but %T", r)
   101  		}
   102  	}
   103  
   104  	// Override path with key to mitigate sneaky stuff.
   105  	ebr.Path = r.DatabaseKey()
   106  	return callAPI(ebr)
   107  }
   108  
   109  // ReadOnly returns whether the database is read only.
   110  func (ebs *endpointBridgeStorage) ReadOnly() bool {
   111  	return false
   112  }
   113  
   114  func callAPI(ebr *EndpointBridgeRequest) (record.Record, error) {
   115  	// Add API prefix to path.
   116  	requestURL := path.Join(apiV1Path, ebr.Path)
   117  	// Check if path is correct. (Defense in depth)
   118  	if !strings.HasPrefix(requestURL, apiV1Path) {
   119  		return nil, fmt.Errorf("bridged request for %q violates scope", ebr.Path)
   120  	}
   121  
   122  	// Apply default Method.
   123  	if ebr.Method == "" {
   124  		if len(ebr.Data) > 0 {
   125  			ebr.Method = http.MethodPost
   126  		} else {
   127  			ebr.Method = http.MethodGet
   128  		}
   129  	}
   130  
   131  	// Build URL.
   132  	u, err := url.ParseRequestURI(requestURL)
   133  	if err != nil {
   134  		return nil, fmt.Errorf("failed to build bridged request url: %w", err)
   135  	}
   136  	// Build query values.
   137  	if ebr.Query != nil && len(ebr.Query) > 0 {
   138  		query := url.Values{}
   139  		for k, v := range ebr.Query {
   140  			query.Set(k, v)
   141  		}
   142  		u.RawQuery = query.Encode()
   143  	}
   144  
   145  	// Create request and response objects.
   146  	r := httptest.NewRequest(ebr.Method, u.String(), bytes.NewBuffer(ebr.Data))
   147  	r.RemoteAddr = endpointBridgeRemoteAddress
   148  	if ebr.MimeType != "" {
   149  		r.Header.Set("Content-Type", ebr.MimeType)
   150  	}
   151  	w := httptest.NewRecorder()
   152  	// Let the API handle the request.
   153  	server.Handler.ServeHTTP(w, r)
   154  	switch w.Code {
   155  	case 200:
   156  		// Everything okay, continue.
   157  	case 500:
   158  		// A Go error was returned internally.
   159  		// We can safely return this as an error.
   160  		return nil, fmt.Errorf("bridged api call failed: %s", w.Body.String())
   161  	default:
   162  		return nil, fmt.Errorf("bridged api call returned unexpected error code %d", w.Code)
   163  	}
   164  
   165  	response := &EndpointBridgeResponse{
   166  		MimeType: w.Header().Get("Content-Type"),
   167  		Body:     w.Body.String(),
   168  	}
   169  	response.SetKey(apiDatabaseName + ":" + ebr.Path)
   170  	response.UpdateMeta()
   171  
   172  	return response, nil
   173  }