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 }