4646)
4747
4848const baseURL = "https://api.tailscale.com"
49- const contentType = "application/json"
49+ const defaultContentType = "application/json"
5050const defaultHttpClientTimeout = time .Minute
5151const defaultUserAgent = "tailscale-client-go"
5252
@@ -134,18 +134,57 @@ func WithUserAgent(ua string) ClientOption {
134134 }
135135}
136136
137- // TODO: consider setting `headers` and `body` via opts to decrease the number of arguments.
138- func (c * Client ) buildRequest (ctx context.Context , method , uri string , headers map [string ]string , body interface {}) (* http.Request , error ) {
137+ type requestParams struct {
138+ headers map [string ]string
139+ body any
140+ contentType string
141+ }
142+
143+ type requestOption func (* requestParams )
144+
145+ func requestBody (body any ) requestOption {
146+ return func (rof * requestParams ) {
147+ rof .body = body
148+ }
149+ }
150+
151+ func requestHeaders (headers map [string ]string ) requestOption {
152+ return func (rof * requestParams ) {
153+ rof .headers = headers
154+ }
155+ }
156+
157+ func requestContentType (ct string ) requestOption {
158+ return func (rof * requestParams ) {
159+ rof .contentType = ct
160+ }
161+ }
162+
163+ func (c * Client ) buildRequest (ctx context.Context , method , uri string , opts ... requestOption ) (* http.Request , error ) {
164+ rof := & requestParams {
165+ contentType : defaultContentType ,
166+ }
167+ for _ , opt := range opts {
168+ opt (rof )
169+ }
170+
139171 u , err := c .baseURL .Parse (uri )
140172 if err != nil {
141173 return nil , err
142174 }
143175
144176 var bodyBytes []byte
145- if body != nil {
146- bodyBytes , err = json .MarshalIndent (body , "" , " " )
147- if err != nil {
148- return nil , err
177+ if rof .body != nil {
178+ switch body := rof .body .(type ) {
179+ case string :
180+ bodyBytes = []byte (body )
181+ case []byte :
182+ bodyBytes = body
183+ default :
184+ bodyBytes , err = json .MarshalIndent (rof .body , "" , " " )
185+ if err != nil {
186+ return nil , err
187+ }
149188 }
150189 }
151190
@@ -158,15 +197,15 @@ func (c *Client) buildRequest(ctx context.Context, method, uri string, headers m
158197 req .Header .Set ("User-Agent" , c .userAgent )
159198 }
160199
161- for k , v := range headers {
200+ for k , v := range rof . headers {
162201 req .Header .Set (k , v )
163202 }
164203
165204 switch {
166- case body == nil :
167- req .Header .Set ("Accept" , contentType )
205+ case rof . body == nil :
206+ req .Header .Set ("Accept" , rof . contentType )
168207 default :
169- req .Header .Set ("Content-Type" , contentType )
208+ req .Header .Set ("Content-Type" , rof . contentType )
170209 }
171210
172211 // c.apiKey will not be set on the client was configured with WithOAuthClientCredentials()
@@ -197,6 +236,12 @@ func (c *Client) performRequest(req *http.Request, out interface{}) error {
197236 return nil
198237 }
199238
239+ // If we're expected to write result into a []byte, do not attempt to parse it.
240+ if o , ok := out .(* []byte ); ok {
241+ * o = bytes .Clone (body )
242+ return nil
243+ }
244+
200245 // If we've got hujson back, convert it to JSON, so we can natively parse it.
201246 if ! json .Valid (body ) {
202247 body , err = hujson .Standardize (body )
@@ -229,9 +274,9 @@ func (err APIError) Error() string {
229274func (c * Client ) SetDNSSearchPaths (ctx context.Context , searchPaths []string ) error {
230275 const uriFmt = "/api/v2/tailnet/%v/dns/searchpaths"
231276
232- req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), nil , map [string ][]string {
277+ req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), requestBody ( map [string ][]string {
233278 "searchPaths" : searchPaths ,
234- })
279+ }))
235280 if err != nil {
236281 return err
237282 }
@@ -243,7 +288,7 @@ func (c *Client) SetDNSSearchPaths(ctx context.Context, searchPaths []string) er
243288func (c * Client ) DNSSearchPaths (ctx context.Context ) ([]string , error ) {
244289 const uriFmt = "/api/v2/tailnet/%v/dns/searchpaths"
245290
246- req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ), nil , nil )
291+ req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ))
247292 if err != nil {
248293 return nil , err
249294 }
@@ -261,9 +306,9 @@ func (c *Client) DNSSearchPaths(ctx context.Context) ([]string, error) {
261306func (c * Client ) SetDNSNameservers (ctx context.Context , dns []string ) error {
262307 const uriFmt = "/api/v2/tailnet/%v/dns/nameservers"
263308
264- req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), nil , map [string ][]string {
309+ req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), requestBody ( map [string ][]string {
265310 "dns" : dns ,
266- })
311+ }))
267312 if err != nil {
268313 return err
269314 }
@@ -275,7 +320,7 @@ func (c *Client) SetDNSNameservers(ctx context.Context, dns []string) error {
275320func (c * Client ) DNSNameservers (ctx context.Context ) ([]string , error ) {
276321 const uriFmt = "/api/v2/tailnet/%v/dns/nameservers"
277322
278- req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ), nil , nil )
323+ req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ))
279324 if err != nil {
280325 return nil , err
281326 }
@@ -389,7 +434,7 @@ type (
389434func (c * Client ) ACL (ctx context.Context ) (* ACL , error ) {
390435 const uriFmt = "/api/v2/tailnet/%s/acl"
391436
392- req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ), nil , nil )
437+ req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ))
393438 if err != nil {
394439 return nil , err
395440 }
@@ -402,6 +447,24 @@ func (c *Client) ACL(ctx context.Context) (*ACL, error) {
402447 return & resp , nil
403448}
404449
450+ // RawACL retrieves the ACL that is currently set for the given tailnet
451+ // as a HuJSON string.
452+ func (c * Client ) RawACL (ctx context.Context ) (string , error ) {
453+ const uriFmt = "/api/v2/tailnet/%s/acl"
454+
455+ req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ), requestContentType ("application/hujson" ))
456+ if err != nil {
457+ return "" , err
458+ }
459+
460+ var resp []byte
461+ if err = c .performRequest (req , & resp ); err != nil {
462+ return "" , err
463+ }
464+
465+ return string (resp ), nil
466+ }
467+
405468type setACLParams struct {
406469 headers map [string ]string
407470}
@@ -415,28 +478,53 @@ func WithETag(etag string) SetACLOption {
415478 }
416479}
417480
418- // SetACL sets the ACL for the given tailnet.
419- func (c * Client ) SetACL (ctx context.Context , acl ACL , opts ... SetACLOption ) error {
481+ // SetACL sets the ACL for the given tailnet. `acl` can either be an [ACL],
482+ // or a HuJSON string.
483+ func (c * Client ) SetACL (ctx context.Context , acl any , opts ... SetACLOption ) error {
420484 const uriFmt = "/api/v2/tailnet/%s/acl"
421485
422486 p := & setACLParams {headers : make (map [string ]string )}
423487 for _ , opt := range opts {
424488 opt (p )
425489 }
426490
427- req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), p .headers , acl )
491+ reqOpts := []requestOption {
492+ requestHeaders (p .headers ),
493+ requestBody (acl ),
494+ }
495+ switch v := acl .(type ) {
496+ case ACL :
497+ case string :
498+ reqOpts = append (reqOpts , requestContentType ("application/hujson" ))
499+ default :
500+ return fmt .Errorf ("expected ACL content as a string or as ACL struct; got %T" , v )
501+ }
502+
503+ req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), reqOpts ... )
428504 if err != nil {
429505 return err
430506 }
431507
432508 return c .performRequest (req , nil )
433509}
434510
435- // ValidateACL validates the provided ACL via the API.
436- func (c * Client ) ValidateACL (ctx context.Context , acl ACL ) error {
511+ // ValidateACL validates the provided ACL via the API. `acl` can either be an [ACL],
512+ // or a HuJSON string.
513+ func (c * Client ) ValidateACL (ctx context.Context , acl any ) error {
437514 const uriFmt = "/api/v2/tailnet/%s/acl/validate"
438515
439- req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), nil , acl )
516+ reqOpts := []requestOption {
517+ requestBody (acl ),
518+ }
519+ switch v := acl .(type ) {
520+ case ACL :
521+ case string :
522+ reqOpts = append (reqOpts , requestContentType ("application/hujson" ))
523+ default :
524+ return fmt .Errorf ("expected ACL content as a string or as ACL struct; got %T" , v )
525+ }
526+
527+ req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), reqOpts ... )
440528 if err != nil {
441529 return err
442530 }
@@ -460,7 +548,7 @@ type DNSPreferences struct {
460548func (c * Client ) DNSPreferences (ctx context.Context ) (* DNSPreferences , error ) {
461549 const uriFmt = "/api/v2/tailnet/%s/dns/preferences"
462550
463- req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ), nil , nil )
551+ req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ))
464552 if err != nil {
465553 return nil , err
466554 }
@@ -478,7 +566,7 @@ func (c *Client) DNSPreferences(ctx context.Context) (*DNSPreferences, error) {
478566func (c * Client ) SetDNSPreferences (ctx context.Context , preferences DNSPreferences ) error {
479567 const uriFmt = "/api/v2/tailnet/%s/dns/preferences"
480568
481- req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), nil , preferences )
569+ req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), requestBody ( preferences ) )
482570 if err != nil {
483571 return nil
484572 }
@@ -498,9 +586,9 @@ type (
498586func (c * Client ) SetDeviceSubnetRoutes (ctx context.Context , deviceID string , routes []string ) error {
499587 const uriFmt = "/api/v2/device/%s/routes"
500588
501- req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , deviceID ), nil , map [string ][]string {
589+ req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , deviceID ), requestBody ( map [string ][]string {
502590 "routes" : routes ,
503- })
591+ }))
504592 if err != nil {
505593 return err
506594 }
@@ -514,7 +602,7 @@ func (c *Client) SetDeviceSubnetRoutes(ctx context.Context, deviceID string, rou
514602func (c * Client ) DeviceSubnetRoutes (ctx context.Context , deviceID string ) (* DeviceRoutes , error ) {
515603 const uriFmt = "/api/v2/device/%s/routes"
516604
517- req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , deviceID ), nil , nil )
605+ req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , deviceID ))
518606 if err != nil {
519607 return nil , err
520608 }
@@ -576,7 +664,7 @@ type Device struct {
576664func (c * Client ) Devices (ctx context.Context ) ([]Device , error ) {
577665 const uriFmt = "/api/v2/tailnet/%s/devices"
578666
579- req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ), nil , nil )
667+ req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ))
580668 if err != nil {
581669 return nil , err
582670 }
@@ -598,9 +686,9 @@ func (c *Client) AuthorizeDevice(ctx context.Context, deviceID string) error {
598686func (c * Client ) SetDeviceAuthorized (ctx context.Context , deviceID string , authorized bool ) error {
599687 const uriFmt = "/api/v2/device/%s/authorized"
600688
601- req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , deviceID ), nil , map [string ]bool {
689+ req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , deviceID ), requestBody ( map [string ]bool {
602690 "authorized" : authorized ,
603- })
691+ }))
604692 if err != nil {
605693 return err
606694 }
@@ -611,7 +699,7 @@ func (c *Client) SetDeviceAuthorized(ctx context.Context, deviceID string, autho
611699// DeleteDevice deletes the device given its deviceID.
612700func (c * Client ) DeleteDevice (ctx context.Context , deviceID string ) error {
613701 const uriFmt = "/api/v2/device/%s"
614- req , err := c .buildRequest (ctx , http .MethodDelete , fmt .Sprintf (uriFmt , deviceID ), nil , nil )
702+ req , err := c .buildRequest (ctx , http .MethodDelete , fmt .Sprintf (uriFmt , deviceID ))
615703 if err != nil {
616704 return err
617705 }
@@ -686,7 +774,7 @@ func (c *Client) CreateKey(ctx context.Context, capabilities KeyCapabilities, op
686774 }
687775 }
688776
689- req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), nil , ckr )
777+ req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , c .tailnet ), requestBody ( ckr ) )
690778 if err != nil {
691779 return Key {}, err
692780 }
@@ -700,7 +788,7 @@ func (c *Client) CreateKey(ctx context.Context, capabilities KeyCapabilities, op
700788func (c * Client ) GetKey (ctx context.Context , id string ) (Key , error ) {
701789 const uriFmt = "/api/v2/tailnet/%s/keys/%s"
702790
703- req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet , id ), nil , nil )
791+ req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet , id ))
704792 if err != nil {
705793 return Key {}, err
706794 }
@@ -714,7 +802,7 @@ func (c *Client) GetKey(ctx context.Context, id string) (Key, error) {
714802func (c * Client ) Keys (ctx context.Context ) ([]Key , error ) {
715803 const uriFmt = "/api/v2/tailnet/%s/keys"
716804
717- req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ), nil , nil )
805+ req , err := c .buildRequest (ctx , http .MethodGet , fmt .Sprintf (uriFmt , c .tailnet ))
718806 if err != nil {
719807 return nil , err
720808 }
@@ -731,7 +819,7 @@ func (c *Client) Keys(ctx context.Context) ([]Key, error) {
731819func (c * Client ) DeleteKey (ctx context.Context , id string ) error {
732820 const uriFmt = "/api/v2/tailnet/%s/keys/%s"
733821
734- req , err := c .buildRequest (ctx , http .MethodDelete , fmt .Sprintf (uriFmt , c .tailnet , id ), nil , nil )
822+ req , err := c .buildRequest (ctx , http .MethodDelete , fmt .Sprintf (uriFmt , c .tailnet , id ))
735823 if err != nil {
736824 return err
737825 }
@@ -743,9 +831,9 @@ func (c *Client) DeleteKey(ctx context.Context, id string) error {
743831func (c * Client ) SetDeviceTags (ctx context.Context , deviceID string , tags []string ) error {
744832 const uriFmt = "/api/v2/device/%s/tags"
745833
746- req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , deviceID ), nil , map [string ][]string {
834+ req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , deviceID ), requestBody ( map [string ][]string {
747835 "tags" : tags ,
748- })
836+ }))
749837 if err != nil {
750838 return err
751839 }
@@ -765,7 +853,7 @@ type (
765853func (c * Client ) SetDeviceKey (ctx context.Context , deviceID string , key DeviceKey ) error {
766854 const uriFmt = "/api/v2/device/%s/key"
767855
768- req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , deviceID ), nil , key )
856+ req , err := c .buildRequest (ctx , http .MethodPost , fmt .Sprintf (uriFmt , deviceID ), requestBody ( key ) )
769857 if err != nil {
770858 return err
771859 }
0 commit comments