Skip to content

Commit 8d41d65

Browse files
committed
Add user-agent requirement
1 parent 1a149fc commit 8d41d65

9 files changed

Lines changed: 399 additions & 54 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@ go run ./cmd/client
2727

2828
## Client examples
2929

30+
**Important**: All clients must provide a User-Agent header in the format `client-name/version` (e.g., `myapp/v1.0.0`). Connections without a valid User-Agent will be rejected with a 400 Bad Request error.
31+
3032
### Subscribe to organization events
3133
```javascript
3234
const ws = new WebSocket('wss://your-server/ws', {
33-
headers: { 'Authorization': 'Bearer ghp_your_github_token' }
35+
headers: {
36+
'Authorization': 'Bearer ghp_your_github_token',
37+
'User-Agent': 'myapp/v1.0.0'
38+
}
3439
});
3540

3641
ws.on('open', () => {

cmd/client/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func run() error {
112112
// Create client configuration
113113
config := client.Config{
114114
ServerURL: url,
115+
UserAgent: fmt.Sprintf("sprinkler-cli/%s", client.Version),
115116
Organization: *org,
116117
Token: githubToken,
117118
EventTypes: eventTypesList,

cmd/server/main.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ const (
3535
// contextKey is a custom type for context keys to avoid collisions.
3636
type contextKey string
3737

38-
const reservationTokenKey contextKey = "reservation_token"
38+
const (
39+
reservationTokenKey contextKey = "reservation_token"
40+
userAgentKey contextKey = "user_agent"
41+
)
3942

4043
var (
4144
webhookSecret = flag.String("webhook-secret", os.Getenv("GITHUB_WEBHOOK_SECRET"), "GitHub webhook secret for signature verification")
@@ -185,6 +188,22 @@ func main() {
185188
return
186189
}
187190

191+
// Validate User-Agent header format
192+
userAgent, err := security.ParseUserAgent(r)
193+
if err != nil {
194+
log.Printf("WebSocket 400: invalid user-agent ip=%s error=%q user_agent=%q",
195+
ip, err.Error(), r.UserAgent())
196+
w.WriteHeader(http.StatusBadRequest)
197+
msg := fmt.Sprintf("400 Bad Request: %s\n", err.Error())
198+
if _, writeErr := w.Write([]byte(msg)); writeErr != nil {
199+
log.Printf("failed to write 400 response: %v", writeErr)
200+
}
201+
return
202+
}
203+
204+
// Store parsed User-Agent in context for handler to use
205+
r = r.WithContext(context.WithValue(r.Context(), userAgentKey, userAgent))
206+
188207
// Pre-validate authentication before WebSocket upgrade
189208
authHeader := r.Header.Get("Authorization")
190209
if !wsHandler.PreValidateAuth(r) {
@@ -235,7 +254,7 @@ func main() {
235254
}
236255

237256
// Set reservation token in request context so websocket handler can commit it
238-
r = r.WithContext(context.WithValue(r.Context(), reservationTokenKey, reservationToken))
257+
r = r.WithContext(context.WithValue(r.Context(), reservationTokenKey, reservationToken)) //nolint:contextcheck // We're properly using r.Context() to derive the new context
239258

240259
// Log successful auth and proceed to upgrade
241260
log.Printf("WebSocket UPGRADE: ip=%s duration=%v", ip, time.Since(startTime))

pkg/client/client.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const (
3030
// DefaultServerAddress is the default webhook sprinkler server address.
3131
DefaultServerAddress = "webhook.github.codegroove.app"
3232

33+
// Version is the client library version.
34+
Version = "v0.5.0"
35+
3336
// UI constants for logging.
3437
separatorLine = "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
3538
msgTypeField = "type"
@@ -65,6 +68,7 @@ type Config struct {
6568
ServerURL string
6669
Token string
6770
TokenProvider func() (string, error) // Optional: dynamically provide fresh tokens for reconnection
71+
UserAgent string // Required: User-Agent in format "client-name/version" (e.g., "myapp/v1.0.0")
6872
Organization string
6973
EventTypes []string
7074
PullRequests []string
@@ -112,6 +116,9 @@ func New(config Config) (*Client, error) {
112116
if config.ServerURL == "" {
113117
return nil, errors.New("serverURL is required")
114118
}
119+
if config.UserAgent == "" {
120+
return nil, errors.New("userAgent is required (format: client-name/version, e.g., myapp/v1.0.0)")
121+
}
115122
if config.Organization == "" && len(config.PullRequests) == 0 {
116123
return nil, errors.New("organization or pull requests required")
117124
}
@@ -291,9 +298,10 @@ func (c *Client) connect(ctx context.Context) error {
291298
return fmt.Errorf("config: %w", err)
292299
}
293300

294-
// Add Authorization header
301+
// Add Authorization and User-Agent headers
295302
wsConfig.Header = make(map[string][]string)
296303
wsConfig.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", token)}
304+
wsConfig.Header["User-Agent"] = []string{c.config.UserAgent}
297305

298306
// Dial the server
299307
ws, err := websocket.DialConfig(wsConfig)

0 commit comments

Comments
 (0)