1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
|
diff --git a/README.md b/README.md
index 61a211bcedd1404d13948cb4bb6d2aad233fb15c..8649c21efc2a2ea097cc734dc96b1a41d5c2645c 100644
--- a/README.md
+++ b/README.md
@@ -13,9 +13,9 @@ | HEAD | mandatory | ☓ | RFC3977 |
| HELP | mandatory | ☓ | RFC3977 |
| QUIT | mandatory | ☓ | RFC3977 |
| STAT | mandatory | ☓ | RFC3977 |
-| AUTHINFO USER | AUTHINFO USER | ☓ | RFC4643 |
-| AUTHINFO PASS | AUTHINFO USER | ☓ | RFC4643 |
-| AUTHINFO SASL | AUTHINFO SASL | ☓ | RFC4643 |
+| AUTHINFO USER | AUTHINFO USER | 🗸 | RFC4643 |
+| AUTHINFO PASS | AUTHINFO USER | 🗸 | RFC4643 |
+| AUTHINFO SASL¹ | AUTHINFO SASL | ☓ | RFC4643 |
| HDR | HDR | ☓ | RFC3977 |
| LIST HEADERS | HDR | ☓ | RFC3977 |
| IHAVE | IHAVE | ☓ | RFC3977 |
@@ -37,6 +37,8 @@ | LAST | READER | ☓ | RFC3977 |
| LISTGROUP | READER | ☓ | RFC3977 |
| NEWGROUPS | READER | ☓ | RFC3977 |
| NEXT | READER | ☓ | RFC3977 |
+
+¹) if you happen to know a provider using SASL, please let me know
## Development
diff --git a/authinfo.go b/authinfo.go
new file mode 100644
index 0000000000000000000000000000000000000000..79551e1a4606ebc9803a40dca81e9287b42ed54e
--- /dev/null
+++ b/authinfo.go
@@ -0,0 +1,41 @@
+package nntp
+
+import (
+ "context"
+ "errors"
+ "fmt"
+)
+
+// Login implements RFC4643 authentication
+func (c *Conn) LoginUserPass(ctx context.Context, username, pass string) error {
+ if c.caps&CapAuthInfoUser == 0 {
+ return ErrCapabilityNotSupported
+ }
+
+ r, err := c.CmdNoBody(ctx, "AUTHINFO USER %s", username)
+ if err != nil {
+ return fmt.Errorf("failed to send username: %w", err)
+ }
+
+ if r.Status.Code == StatusAuthPassRequired {
+ r, err = c.CmdNoBody(ctx, "AUTHINFO PASS %s", pass)
+ if err != nil {
+ return fmt.Errorf("failed to send password: %w", err)
+ }
+ }
+
+ switch r.Status.Code {
+ case StatusAuthAccepted:
+ c.Capabilities(ctx)
+ return nil
+ case StatusAuthFailed:
+ return ErrAuthFailed
+ default:
+ return fmt.Errorf("%w: %d", ErrUnexpectedResponse, r.Status.Code)
+ }
+}
+
+var (
+ ErrSASLNotSupported = errors.New("SASL authentication is not yet supported. Please send your provider to the mailing list")
+ ErrAuthFailed = errors.New("authentication failed")
+)
diff --git a/authinfo_test.go b/authinfo_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a54dcc279b4c0be6dd39ac804a6590e9f977f75f
--- /dev/null
+++ b/authinfo_test.go
@@ -0,0 +1,52 @@
+package nntp_test
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "mpldr.codes/usenet/nntp"
+)
+
+func TestUserPass(t *testing.T) {
+ if NewsServerSecure == "" || NewsServerUser == "" || NewsServerPassword == "" {
+ t.Log("secure server address, username, and password required in variables_test.go")
+ t.SkipNow()
+ }
+ tests := []struct {
+ name string
+ passwordOverride string
+ expectedError error
+ }{
+ {
+ name: "valid",
+ expectedError: nil,
+ },
+ /*{ // eweka does not conform to the standard. therefore this test is commented out
+ name: "invalid",
+ passwordOverride: "lol not your password",
+ expectedError: nntp.ErrAuthFailed,
+ },*/
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ c, err := nntp.Dial(NewsServerPlain, nntp.OptionUnencrypted)
+ if err != nil {
+ t.Skipf("connection to '%s' failed: %v", NewsServerSecure, err)
+ }
+
+ if test.passwordOverride == "" {
+ test.passwordOverride = NewsServerPassword
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ err = c.LoginUserPass(ctx, NewsServerUser, test.passwordOverride)
+ if !errors.Is(err, test.expectedError) {
+ t.Errorf("unexpected result:\n\tgot: %v\n\texpected: %v\n", err, test.expectedError)
+ }
+ })
+ }
+}
diff --git a/capabilities_test.go b/capabilities_test.go
index bcf309bb467021780e99b89267a1774d25756659..519bcb853caaa7602a622fa2d1fe9108a438a879 100644
--- a/capabilities_test.go
+++ b/capabilities_test.go
@@ -2,6 +2,8 @@ package nntp_test
import (
"context"
+ "errors"
+ "strings"
"testing"
"time"
@@ -13,14 +15,39 @@ tests := []struct {
name string
host string
options []nntp.ConnOption
+ preSteps func(c *nntp.Conn) error
expected nntp.Capability
}{
{
name: "eweka-unauthenticated",
host: "news.eweka.nl:119",
options: []nntp.ConnOption{nntp.OptionUnencrypted},
+ preSteps: func(*nntp.Conn) error { return nil },
expected: nntp.CapAuthInfoUser,
},
+ {
+ name: "eweka-authenticated",
+ host: "news.eweka.nl:119",
+ options: []nntp.ConnOption{nntp.OptionUnencrypted},
+ preSteps: func(c *nntp.Conn) error {
+ if !strings.HasPrefix(NewsServerSecure, "news.eweka.nl") {
+ return errors.New("NewsServerSecure not by Eweka")
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ return c.LoginUserPass(ctx, NewsServerUser, NewsServerPassword)
+ },
+ expected: nntp.CapModeReader |
+ nntp.CapReader |
+ nntp.CapList |
+ nntp.CapListActive |
+ nntp.CapListActiveTimes |
+ nntp.CapListNewsgroups |
+ nntp.CapListOverviewFmt |
+ nntp.CapHDR |
+ nntp.CapOver |
+ nntp.CapPost,
+ },
}
for _, test := range tests {
@@ -31,6 +58,11 @@ t.Logf("failed to connect to host: %v", err)
t.SkipNow()
}
+ err = test.preSteps(c)
+ if err != nil {
+ t.Skipf("prestep failed: %v", err)
+ }
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
caps, err := c.Capabilities(ctx)
@@ -39,7 +71,7 @@ t.Errorf("failed to run command: %v", err)
t.FailNow()
}
- if caps&test.expected != test.expected {
+ if caps != test.expected {
t.Errorf("did not receive expected caps:\n\tgot: %s\n\texpected: %s", caps, test.expected)
t.FailNow()
}
diff --git a/status.go b/status.go
index a58f745ed06a656eab5361488e71b1089e38c33a..b1bd50beecfca17ae02655b56c0ba91ca80f9174 100644
--- a/status.go
+++ b/status.go
@@ -5,7 +5,13 @@
const (
StatusServiceAvailable = 200
StatusServiceNoPosting = 201
+ StatusAuthAccepted = 281
+ StatusAuthAcceptedWithData = 283
+ StatusAuthPassRequired = 381
+ StatusAuthSASLContinue = 383
StatusTemporarilyUnavailable = 400
+ StatusAuthFailed = 481
+ StatusAuthInvalid = 482
StatusPermanentlyUnavailable = 502
)
|