Skip to content

Commit 523e275

Browse files
authored
Merge pull request #269 from anywherelan/invite-link
invites: add invite links for automatic peer acceptance
2 parents 78001e9 + bcc6f94 commit 523e275

25 files changed

Lines changed: 3066 additions & 96 deletions

api/api.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
http_pprof "net/http/pprof"
1010
"runtime/pprof"
1111
"strings"
12+
"unicode"
1213

1314
"github.com/go-playground/validator/v10"
1415
"github.com/ipfs/go-log/v2"
@@ -114,6 +115,10 @@ func (h *Handler) setupRouter(address string) (*echo.Echo, error) {
114115
if err != nil {
115116
return nil, err
116117
}
118+
err = val.RegisterValidation("no_control_chars", validateNoControlChars, false)
119+
if err != nil {
120+
return nil, err
121+
}
117122

118123
e.Validator = &customValidator{validator: val}
119124

@@ -150,6 +155,9 @@ func (h *Handler) setupRouter(address string) (*echo.Echo, error) {
150155
e.POST(RemovePeerSettingsPath, h.RemovePeer)
151156
e.GET(GetAuthRequestsPath, h.GetAuthRequests)
152157
e.GET(GetBlockedPeersPath, h.GetBlockedPeers)
158+
e.POST(CreateInvitePath, h.CreateInvite)
159+
e.GET(GetInvitesPath, h.GetInvites)
160+
e.POST(RevokeInvitePath, h.RevokeInvite)
153161

154162
// Settings
155163
e.GET(GetMyPeerInfoPath, h.GetMyPeerInfo)
@@ -263,3 +271,7 @@ func validateTrimmedStringNotEmpty(fl validator.FieldLevel) bool {
263271
str = strings.TrimSpace(str)
264272
return len(str) > 0
265273
}
274+
275+
func validateNoControlChars(fl validator.FieldLevel) bool {
276+
return !strings.ContainsFunc(fl.Field().String(), unicode.IsControl)
277+
}

api/apiclient/client.go

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,22 +99,11 @@ func (c *Client) UpdateProxySettings(usingPeerID string) error {
9999
return c.sendPostRequest(api.UpdateProxySettingsPath, request, nil)
100100
}
101101

102-
func (c *Client) SendFriendRequest(peerID, alias string, ipAddr string) error {
103-
request := entity.FriendRequest{
104-
PeerID: peerID,
105-
Alias: alias,
106-
IPAddr: ipAddr,
107-
}
102+
func (c *Client) SendFriendRequest(request entity.FriendRequest) error {
108103
return c.sendPostRequest(api.SendFriendRequestPath, request, nil)
109104
}
110105

111-
func (c *Client) ReplyFriendRequest(peerID, alias string, decline bool, ipAddr string) error {
112-
request := entity.FriendRequestReply{
113-
PeerID: peerID,
114-
Alias: alias,
115-
Decline: decline,
116-
IPAddr: ipAddr,
117-
}
106+
func (c *Client) ReplyFriendRequest(request entity.FriendRequestReply) error {
118107
return c.sendPostRequest(api.AcceptPeerInvitationPath, request, nil)
119108
}
120109

@@ -127,6 +116,29 @@ func (c *Client) AuthRequests() ([]entity.AuthRequest, error) {
127116
return authRequests, nil
128117
}
129118

119+
func (c *Client) CreateInvite(request entity.CreateInviteRequest) (*entity.InviteResponse, error) {
120+
invite := new(entity.InviteResponse)
121+
err := c.sendPostRequest(api.CreateInvitePath, request, invite)
122+
if err != nil {
123+
return nil, err
124+
}
125+
return invite, nil
126+
}
127+
128+
func (c *Client) Invites() ([]entity.InviteResponse, error) {
129+
invites := make([]entity.InviteResponse, 0)
130+
err := c.sendGetRequest(api.GetInvitesPath, &invites)
131+
if err != nil {
132+
return nil, err
133+
}
134+
return invites, nil
135+
}
136+
137+
func (c *Client) RevokeInvite(id string) error {
138+
request := entity.RevokeInviteRequest{ID: id}
139+
return c.sendPostRequest(api.RevokeInvitePath, request, nil)
140+
}
141+
130142
func (c *Client) BlockedPeers() ([]config.BlockedPeer, error) {
131143
blocked := make([]config.BlockedPeer, 0)
132144
err := c.sendGetRequest(api.GetBlockedPeersPath, &blocked)

api/const.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ const (
1515
AcceptPeerInvitationPath = V0Prefix + "peers/accept_peer"
1616
GetAuthRequestsPath = V0Prefix + "peers/auth_requests"
1717

18+
// Invite links
19+
CreateInvitePath = V0Prefix + "peers/invites/create"
20+
GetInvitesPath = V0Prefix + "peers/invites/list"
21+
RevokeInvitePath = V0Prefix + "peers/invites/revoke"
22+
1823
// Settings
1924
GetMyPeerInfoPath = V0Prefix + "settings/peer_info"
2025
UpdateMyInfoPath = V0Prefix + "settings/update"

api/invites.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
"time"
7+
8+
"github.com/labstack/echo/v4"
9+
10+
"github.com/anywherelan/awl/config"
11+
"github.com/anywherelan/awl/entity"
12+
)
13+
14+
const defaultInviteMaxUses = 1
15+
16+
// @Tags Peers
17+
// @Summary Create an invite link
18+
// @Accept json
19+
// @Produce json
20+
// @Param body body entity.CreateInviteRequest true "Params"
21+
// @Success 200 {object} entity.InviteResponse
22+
// @Failure 400 {object} api.Error
23+
// @Failure 500 {object} api.Error
24+
// @Router /peers/invites/create [POST]
25+
func (h *Handler) CreateInvite(c echo.Context) (err error) {
26+
req := entity.CreateInviteRequest{}
27+
err = c.Bind(&req)
28+
if err != nil {
29+
return c.JSON(http.StatusBadRequest, ErrorMessage(err.Error()))
30+
}
31+
if err = c.Validate(req); err != nil {
32+
return c.JSON(http.StatusBadRequest, ErrorMessage(err.Error()))
33+
}
34+
35+
if req.MaxUses == 0 {
36+
req.MaxUses = defaultInviteMaxUses
37+
}
38+
req.Alias = strings.TrimSpace(req.Alias)
39+
// One alias cannot name several peers, so it only makes sense for a
40+
// single-use link. Uniqueness itself is not checked here: the peer list will
41+
// have moved on by the time the link is redeemed, and a collision is
42+
// resolved then (GenUniqPeerAlias), so checking now would only produce
43+
// false rejections.
44+
if req.Alias != "" && req.MaxUses != 1 {
45+
return c.JSON(http.StatusBadRequest,
46+
ErrorMessage("alias can only be set for a single-use invite"))
47+
}
48+
49+
var expiresAt time.Time
50+
if req.ExpiresInSeconds > 0 {
51+
expiresAt = time.Now().Add(time.Duration(req.ExpiresInSeconds) * time.Second)
52+
}
53+
54+
invite, err := h.conf.CreateInvite(config.CreateInviteParams{
55+
Label: req.Label,
56+
Alias: req.Alias,
57+
AllowUsingAsExitNode: req.AllowUsingAsExitNode,
58+
MaxUses: req.MaxUses,
59+
ExpiresAt: expiresAt,
60+
})
61+
if err != nil {
62+
return c.JSON(http.StatusInternalServerError, ErrorMessage(err.Error()))
63+
}
64+
65+
return c.JSON(http.StatusOK, h.inviteResponse(invite))
66+
}
67+
68+
// @Tags Peers
69+
// @Summary Get invite links
70+
// @Accept json
71+
// @Produce json
72+
// @Success 200 {array} entity.InviteResponse
73+
// @Router /peers/invites/list [GET]
74+
func (h *Handler) GetInvites(c echo.Context) (err error) {
75+
invites := h.conf.ListInvites()
76+
77+
result := make([]entity.InviteResponse, 0, len(invites))
78+
for _, invite := range invites {
79+
result = append(result, h.inviteResponse(invite))
80+
}
81+
82+
return c.JSON(http.StatusOK, result)
83+
}
84+
85+
// @Tags Peers
86+
// @Summary Revoke an invite link
87+
// @Description Stops new connections through the link; peers already added stay.
88+
// @Accept json
89+
// @Produce json
90+
// @Param body body entity.RevokeInviteRequest true "Params"
91+
// @Success 200 "OK"
92+
// @Failure 400 {object} api.Error
93+
// @Failure 404 {object} api.Error
94+
// @Router /peers/invites/revoke [POST]
95+
func (h *Handler) RevokeInvite(c echo.Context) (err error) {
96+
req := entity.RevokeInviteRequest{}
97+
err = c.Bind(&req)
98+
if err != nil {
99+
return c.JSON(http.StatusBadRequest, ErrorMessage(err.Error()))
100+
}
101+
if err = c.Validate(req); err != nil {
102+
return c.JSON(http.StatusBadRequest, ErrorMessage(err.Error()))
103+
}
104+
105+
if !h.conf.RevokeInvite(req.ID) {
106+
return c.JSON(http.StatusNotFound, ErrorMessage("invite not found"))
107+
}
108+
109+
return c.NoContent(http.StatusOK)
110+
}
111+
112+
// inviteResponse renders an invite for the API, building the link from our
113+
// current peer ID and node name.
114+
func (h *Handler) inviteResponse(invite config.Invite) entity.InviteResponse {
115+
h.conf.RLock()
116+
peerID := h.conf.P2pNode.PeerID
117+
nodeName := h.conf.P2pNode.Name
118+
h.conf.RUnlock()
119+
120+
return entity.InviteResponse{
121+
ID: invite.ID,
122+
Label: invite.Label,
123+
Link: entity.BuildInviteLink(peerID, invite.Token, nodeName),
124+
Alias: invite.Alias,
125+
AllowUsingAsExitNode: invite.WeAllowUsingAsExitNode,
126+
MaxUses: invite.MaxUses,
127+
UsedCount: invite.UsedCount,
128+
ExpiresAt: invite.ExpiresAt,
129+
CreatedAt: invite.CreatedAt,
130+
Revoked: invite.Revoked,
131+
Status: inviteStatus(invite),
132+
}
133+
}
134+
135+
func inviteStatus(invite config.Invite) string {
136+
switch {
137+
case invite.Revoked:
138+
return entity.InviteStatusRevoked
139+
case invite.IsExpired(time.Now()):
140+
return entity.InviteStatusExpired
141+
case invite.IsUsedUp():
142+
return entity.InviteStatusUsedUp
143+
}
144+
return entity.InviteStatusActive
145+
}

api/peers.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/anywherelan/awl/awldns"
1212
"github.com/anywherelan/awl/config"
1313
"github.com/anywherelan/awl/entity"
14+
"github.com/anywherelan/awl/service"
1415
)
1516

1617
const ErrorPeerAliasIsNotUniq = "peer name is not unique"
@@ -56,6 +57,7 @@ func (h *Handler) getKnownPeers() []entity.KnownPeersResponse {
5657
WeAllowUsingAsExitNode: knownPeer.WeAllowUsingAsExitNode,
5758
AllowedUsingAsExitNode: knownPeer.AllowedUsingAsExitNode,
5859
RemoteVPNGatewayServerEnabled: knownPeer.RemoteVPNGatewayServerEnabled,
60+
InviteID: knownPeer.InviteID,
5961
LastSeen: knownPeer.LastSeen,
6062
Connections: h.p2p.PeerConnectionsInfo(id),
6163
NetworkStats: netStats,
@@ -182,7 +184,13 @@ func (h *Handler) SendFriendRequest(c echo.Context) (err error) {
182184
ErrorMessage("You can't add yourself"))
183185
}
184186

185-
err = h.authStatus.AddPeer(h.ctx, peerId, "", req.Alias, false, req.IPAddr)
187+
err = h.authStatus.AddPeer(h.ctx, service.AddPeerParams{
188+
PeerID: peerId,
189+
Alias: req.Alias,
190+
IPAddr: req.IPAddr,
191+
AllowUsingAsExitNode: req.AllowUsingAsExitNode,
192+
PendingInviteToken: req.Token,
193+
})
186194
if err != nil {
187195
return c.JSON(http.StatusBadRequest, ErrorMessage(err.Error()))
188196
}
@@ -230,7 +238,14 @@ func (h *Handler) AcceptFriend(c echo.Context) (err error) {
230238
return c.NoContent(http.StatusOK)
231239
}
232240

233-
err = h.authStatus.AddPeer(h.ctx, peerId, auth.Name, req.Alias, true, req.IPAddr)
241+
err = h.authStatus.AddPeer(h.ctx, service.AddPeerParams{
242+
PeerID: peerId,
243+
Name: auth.Name,
244+
Alias: req.Alias,
245+
Confirmed: true,
246+
IPAddr: req.IPAddr,
247+
AllowUsingAsExitNode: req.AllowUsingAsExitNode,
248+
})
234249
if err != nil {
235250
return c.JSON(http.StatusBadRequest, ErrorMessage(err.Error()))
236251
}

0 commit comments

Comments
 (0)