]>
Commit | Line | Data |
---|---|---|
1 | package hotline | |
2 | ||
3 | import ( | |
4 | "bufio" | |
5 | "bytes" | |
6 | "context" | |
7 | "crypto/rand" | |
8 | "encoding/binary" | |
9 | "errors" | |
10 | "fmt" | |
11 | "github.com/go-playground/validator/v10" | |
12 | "golang.org/x/text/encoding/charmap" | |
13 | "gopkg.in/yaml.v3" | |
14 | "io" | |
15 | "log" | |
16 | "log/slog" | |
17 | "net" | |
18 | "os" | |
19 | "path" | |
20 | "path/filepath" | |
21 | "strings" | |
22 | "sync" | |
23 | "sync/atomic" | |
24 | "time" | |
25 | ) | |
26 | ||
27 | type contextKey string | |
28 | ||
29 | var contextKeyReq = contextKey("req") | |
30 | ||
31 | type requestCtx struct { | |
32 | remoteAddr string | |
33 | } | |
34 | ||
35 | // Converts bytes from Mac Roman encoding to UTF-8 | |
36 | var txtDecoder = charmap.Macintosh.NewDecoder() | |
37 | ||
38 | // Converts bytes from UTF-8 to Mac Roman encoding | |
39 | var txtEncoder = charmap.Macintosh.NewEncoder() | |
40 | ||
41 | type Server struct { | |
42 | NetInterface string | |
43 | Port int | |
44 | Accounts map[string]*Account | |
45 | Agreement []byte | |
46 | ||
47 | Clients map[[2]byte]*ClientConn | |
48 | fileTransfers map[[4]byte]*FileTransfer | |
49 | ||
50 | Config *Config | |
51 | ConfigDir string | |
52 | Logger *slog.Logger | |
53 | banner []byte | |
54 | ||
55 | PrivateChatsMu sync.Mutex | |
56 | PrivateChats map[[4]byte]*PrivateChat | |
57 | ||
58 | nextClientID atomic.Uint32 | |
59 | TrackerPassID [4]byte | |
60 | ||
61 | statsMu sync.Mutex | |
62 | Stats *Stats | |
63 | ||
64 | FS FileStore // Storage backend to use for File storage | |
65 | ||
66 | outbox chan Transaction | |
67 | mux sync.Mutex | |
68 | ||
69 | threadedNewsMux sync.Mutex | |
70 | ThreadedNews *ThreadedNews | |
71 | ||
72 | flatNewsMux sync.Mutex | |
73 | FlatNews []byte | |
74 | ||
75 | banListMU sync.Mutex | |
76 | banList map[string]*time.Time | |
77 | } | |
78 | ||
79 | func (s *Server) CurrentStats() Stats { | |
80 | s.statsMu.Lock() | |
81 | defer s.statsMu.Unlock() | |
82 | ||
83 | stats := s.Stats | |
84 | stats.CurrentlyConnected = len(s.Clients) | |
85 | ||
86 | return *stats | |
87 | } | |
88 | ||
89 | type PrivateChat struct { | |
90 | Subject string | |
91 | ClientConn map[[2]byte]*ClientConn | |
92 | } | |
93 | ||
94 | func (s *Server) ListenAndServe(ctx context.Context) error { | |
95 | var wg sync.WaitGroup | |
96 | ||
97 | wg.Add(1) | |
98 | go func() { | |
99 | ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port)) | |
100 | if err != nil { | |
101 | log.Fatal(err) | |
102 | } | |
103 | ||
104 | log.Fatal(s.Serve(ctx, ln)) | |
105 | }() | |
106 | ||
107 | wg.Add(1) | |
108 | go func() { | |
109 | ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port+1)) | |
110 | if err != nil { | |
111 | log.Fatal(err) | |
112 | } | |
113 | ||
114 | log.Fatal(s.ServeFileTransfers(ctx, ln)) | |
115 | }() | |
116 | ||
117 | wg.Wait() | |
118 | ||
119 | return nil | |
120 | } | |
121 | ||
122 | func (s *Server) ServeFileTransfers(ctx context.Context, ln net.Listener) error { | |
123 | for { | |
124 | conn, err := ln.Accept() | |
125 | if err != nil { | |
126 | return err | |
127 | } | |
128 | ||
129 | go func() { | |
130 | defer func() { _ = conn.Close() }() | |
131 | ||
132 | err = s.handleFileTransfer( | |
133 | context.WithValue(ctx, contextKeyReq, requestCtx{remoteAddr: conn.RemoteAddr().String()}), | |
134 | conn, | |
135 | ) | |
136 | ||
137 | if err != nil { | |
138 | s.Logger.Error("file transfer error", "reason", err) | |
139 | } | |
140 | }() | |
141 | } | |
142 | } | |
143 | ||
144 | func (s *Server) sendTransaction(t Transaction) error { | |
145 | s.mux.Lock() | |
146 | client, ok := s.Clients[t.clientID] | |
147 | s.mux.Unlock() | |
148 | ||
149 | if !ok || client == nil { | |
150 | return nil | |
151 | } | |
152 | ||
153 | _, err := io.Copy(client.Connection, &t) | |
154 | if err != nil { | |
155 | return fmt.Errorf("failed to send transaction to client %v: %v", t.clientID, err) | |
156 | } | |
157 | ||
158 | return nil | |
159 | } | |
160 | ||
161 | func (s *Server) processOutbox() { | |
162 | for { | |
163 | t := <-s.outbox | |
164 | go func() { | |
165 | if err := s.sendTransaction(t); err != nil { | |
166 | s.Logger.Error("error sending transaction", "err", err) | |
167 | } | |
168 | }() | |
169 | } | |
170 | } | |
171 | ||
172 | func (s *Server) Serve(ctx context.Context, ln net.Listener) error { | |
173 | go s.processOutbox() | |
174 | ||
175 | for { | |
176 | conn, err := ln.Accept() | |
177 | if err != nil { | |
178 | s.Logger.Error("error accepting connection", "err", err) | |
179 | } | |
180 | connCtx := context.WithValue(ctx, contextKeyReq, requestCtx{ | |
181 | remoteAddr: conn.RemoteAddr().String(), | |
182 | }) | |
183 | ||
184 | go func() { | |
185 | s.Logger.Info("Connection established", "RemoteAddr", conn.RemoteAddr()) | |
186 | ||
187 | defer conn.Close() | |
188 | if err := s.handleNewConnection(connCtx, conn, conn.RemoteAddr().String()); err != nil { | |
189 | if err == io.EOF { | |
190 | s.Logger.Info("Client disconnected", "RemoteAddr", conn.RemoteAddr()) | |
191 | } else { | |
192 | s.Logger.Error("error serving request", "RemoteAddr", conn.RemoteAddr(), "err", err) | |
193 | } | |
194 | } | |
195 | }() | |
196 | } | |
197 | } | |
198 | ||
199 | const ( | |
200 | agreementFile = "Agreement.txt" | |
201 | ) | |
202 | ||
203 | // NewServer constructs a new Server from a config dir | |
204 | // TODO: move config file reads out of this function | |
205 | func NewServer(configDir, netInterface string, netPort int, logger *slog.Logger, fs FileStore) (*Server, error) { | |
206 | server := Server{ | |
207 | NetInterface: netInterface, | |
208 | Port: netPort, | |
209 | Accounts: make(map[string]*Account), | |
210 | Config: new(Config), | |
211 | Clients: make(map[[2]byte]*ClientConn), | |
212 | fileTransfers: make(map[[4]byte]*FileTransfer), | |
213 | PrivateChats: make(map[[4]byte]*PrivateChat), | |
214 | ConfigDir: configDir, | |
215 | Logger: logger, | |
216 | outbox: make(chan Transaction), | |
217 | Stats: &Stats{Since: time.Now()}, | |
218 | ThreadedNews: &ThreadedNews{}, | |
219 | FS: fs, | |
220 | banList: make(map[string]*time.Time), | |
221 | } | |
222 | ||
223 | var err error | |
224 | ||
225 | // generate a new random passID for tracker registration | |
226 | if _, err := rand.Read(server.TrackerPassID[:]); err != nil { | |
227 | return nil, err | |
228 | } | |
229 | ||
230 | server.Agreement, err = os.ReadFile(filepath.Join(configDir, agreementFile)) | |
231 | if err != nil { | |
232 | return nil, err | |
233 | } | |
234 | ||
235 | if server.FlatNews, err = os.ReadFile(filepath.Join(configDir, "MessageBoard.txt")); err != nil { | |
236 | return nil, err | |
237 | } | |
238 | ||
239 | // try to load the ban list, but ignore errors as this file may not be present or may be empty | |
240 | //_ = server.loadBanList(filepath.Join(configDir, "Banlist.yaml")) | |
241 | ||
242 | _ = loadFromYAMLFile(filepath.Join(configDir, "Banlist.yaml"), &server.banList) | |
243 | ||
244 | err = loadFromYAMLFile(filepath.Join(configDir, "ThreadedNews.yaml"), &server.ThreadedNews) | |
245 | if err != nil { | |
246 | return nil, fmt.Errorf("error loading threaded news: %w", err) | |
247 | } | |
248 | ||
249 | err = server.loadConfig(filepath.Join(configDir, "config.yaml")) | |
250 | if err != nil { | |
251 | return nil, fmt.Errorf("error loading config: %w", err) | |
252 | } | |
253 | ||
254 | if err := server.loadAccounts(filepath.Join(configDir, "Users/")); err != nil { | |
255 | return nil, err | |
256 | } | |
257 | ||
258 | // If the FileRoot is an absolute path, use it, otherwise treat as a relative path to the config dir. | |
259 | if !filepath.IsAbs(server.Config.FileRoot) { | |
260 | server.Config.FileRoot = filepath.Join(configDir, server.Config.FileRoot) | |
261 | } | |
262 | ||
263 | server.banner, err = os.ReadFile(filepath.Join(server.ConfigDir, server.Config.BannerFile)) | |
264 | if err != nil { | |
265 | return nil, fmt.Errorf("error opening banner: %w", err) | |
266 | } | |
267 | ||
268 | if server.Config.EnableTrackerRegistration { | |
269 | server.Logger.Info( | |
270 | "Tracker registration enabled", | |
271 | "frequency", fmt.Sprintf("%vs", trackerUpdateFrequency), | |
272 | "trackers", server.Config.Trackers, | |
273 | ) | |
274 | ||
275 | go func() { | |
276 | for { | |
277 | tr := &TrackerRegistration{ | |
278 | UserCount: server.userCount(), | |
279 | PassID: server.TrackerPassID, | |
280 | Name: server.Config.Name, | |
281 | Description: server.Config.Description, | |
282 | } | |
283 | binary.BigEndian.PutUint16(tr.Port[:], uint16(server.Port)) | |
284 | for _, t := range server.Config.Trackers { | |
285 | if err := register(&RealDialer{}, t, tr); err != nil { | |
286 | server.Logger.Error("unable to register with tracker %v", "error", err) | |
287 | } | |
288 | server.Logger.Debug("Sent Tracker registration", "addr", t) | |
289 | } | |
290 | ||
291 | time.Sleep(trackerUpdateFrequency * time.Second) | |
292 | } | |
293 | }() | |
294 | } | |
295 | ||
296 | // Start Client Keepalive go routine | |
297 | go server.keepaliveHandler() | |
298 | ||
299 | return &server, nil | |
300 | } | |
301 | ||
302 | func (s *Server) userCount() int { | |
303 | s.mux.Lock() | |
304 | defer s.mux.Unlock() | |
305 | ||
306 | return len(s.Clients) | |
307 | } | |
308 | ||
309 | func (s *Server) keepaliveHandler() { | |
310 | for { | |
311 | time.Sleep(idleCheckInterval * time.Second) | |
312 | s.mux.Lock() | |
313 | ||
314 | for _, c := range s.Clients { | |
315 | c.IdleTime += idleCheckInterval | |
316 | if c.IdleTime > userIdleSeconds && !c.Idle { | |
317 | c.Idle = true | |
318 | ||
319 | c.flagsMU.Lock() | |
320 | c.Flags.Set(UserFlagAway, 1) | |
321 | c.flagsMU.Unlock() | |
322 | c.sendAll( | |
323 | TranNotifyChangeUser, | |
324 | NewField(FieldUserID, c.ID[:]), | |
325 | NewField(FieldUserFlags, c.Flags[:]), | |
326 | NewField(FieldUserName, c.UserName), | |
327 | NewField(FieldUserIconID, c.Icon), | |
328 | ) | |
329 | } | |
330 | } | |
331 | s.mux.Unlock() | |
332 | } | |
333 | } | |
334 | ||
335 | func (s *Server) writeBanList() error { | |
336 | s.banListMU.Lock() | |
337 | defer s.banListMU.Unlock() | |
338 | ||
339 | out, err := yaml.Marshal(s.banList) | |
340 | if err != nil { | |
341 | return err | |
342 | } | |
343 | err = os.WriteFile( | |
344 | filepath.Join(s.ConfigDir, "Banlist.yaml"), | |
345 | out, | |
346 | 0666, | |
347 | ) | |
348 | return err | |
349 | } | |
350 | ||
351 | func (s *Server) writeThreadedNews() error { | |
352 | s.threadedNewsMux.Lock() | |
353 | defer s.threadedNewsMux.Unlock() | |
354 | ||
355 | out, err := yaml.Marshal(s.ThreadedNews) | |
356 | if err != nil { | |
357 | return err | |
358 | } | |
359 | err = s.FS.WriteFile( | |
360 | filepath.Join(s.ConfigDir, "ThreadedNews.yaml"), | |
361 | out, | |
362 | 0666, | |
363 | ) | |
364 | return err | |
365 | } | |
366 | ||
367 | func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn { | |
368 | s.mux.Lock() | |
369 | defer s.mux.Unlock() | |
370 | ||
371 | clientConn := &ClientConn{ | |
372 | Icon: []byte{0, 0}, // TODO: make array type | |
373 | Connection: conn, | |
374 | Server: s, | |
375 | RemoteAddr: remoteAddr, | |
376 | transfers: map[int]map[[4]byte]*FileTransfer{ | |
377 | FileDownload: {}, | |
378 | FileUpload: {}, | |
379 | FolderDownload: {}, | |
380 | FolderUpload: {}, | |
381 | bannerDownload: {}, | |
382 | }, | |
383 | } | |
384 | ||
385 | s.nextClientID.Add(1) | |
386 | ||
387 | binary.BigEndian.PutUint16(clientConn.ID[:], uint16(s.nextClientID.Load())) | |
388 | s.Clients[clientConn.ID] = clientConn | |
389 | ||
390 | return clientConn | |
391 | } | |
392 | ||
393 | // NewUser creates a new user account entry in the server map and config file | |
394 | func (s *Server) NewUser(login, name, password string, access accessBitmap) error { | |
395 | s.mux.Lock() | |
396 | defer s.mux.Unlock() | |
397 | ||
398 | account := NewAccount(login, name, password, access) | |
399 | ||
400 | // Create account file, returning an error if one already exists. | |
401 | file, err := os.OpenFile( | |
402 | filepath.Join(s.ConfigDir, "Users", path.Join("/", login)+".yaml"), | |
403 | os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644, | |
404 | ) | |
405 | if err != nil { | |
406 | return fmt.Errorf("error creating account file: %w", err) | |
407 | } | |
408 | defer file.Close() | |
409 | ||
410 | b, err := yaml.Marshal(account) | |
411 | if err != nil { | |
412 | return err | |
413 | } | |
414 | ||
415 | _, err = file.Write(b) | |
416 | if err != nil { | |
417 | return fmt.Errorf("error writing account file: %w", err) | |
418 | } | |
419 | ||
420 | s.Accounts[login] = account | |
421 | ||
422 | return nil | |
423 | } | |
424 | ||
425 | func (s *Server) UpdateUser(login, newLogin, name, password string, access accessBitmap) error { | |
426 | s.mux.Lock() | |
427 | defer s.mux.Unlock() | |
428 | ||
429 | // If the login has changed, rename the account file. | |
430 | if login != newLogin { | |
431 | err := os.Rename( | |
432 | filepath.Join(s.ConfigDir, "Users", path.Join("/", login)+".yaml"), | |
433 | filepath.Join(s.ConfigDir, "Users", path.Join("/", newLogin)+".yaml"), | |
434 | ) | |
435 | if err != nil { | |
436 | return fmt.Errorf("error renaming account file: %w", err) | |
437 | } | |
438 | s.Accounts[newLogin] = s.Accounts[login] | |
439 | s.Accounts[newLogin].Login = newLogin | |
440 | delete(s.Accounts, login) | |
441 | } | |
442 | ||
443 | account := s.Accounts[newLogin] | |
444 | account.Access = access | |
445 | account.Name = name | |
446 | account.Password = password | |
447 | ||
448 | out, err := yaml.Marshal(&account) | |
449 | if err != nil { | |
450 | return err | |
451 | } | |
452 | ||
453 | if err := os.WriteFile(filepath.Join(s.ConfigDir, "Users", newLogin+".yaml"), out, 0666); err != nil { | |
454 | return fmt.Errorf("error writing account file: %w", err) | |
455 | } | |
456 | ||
457 | return nil | |
458 | } | |
459 | ||
460 | // DeleteUser deletes the user account | |
461 | func (s *Server) DeleteUser(login string) error { | |
462 | s.mux.Lock() | |
463 | defer s.mux.Unlock() | |
464 | ||
465 | err := s.FS.Remove(filepath.Join(s.ConfigDir, "Users", path.Join("/", login)+".yaml")) | |
466 | if err != nil { | |
467 | return err | |
468 | } | |
469 | ||
470 | delete(s.Accounts, login) | |
471 | ||
472 | return nil | |
473 | } | |
474 | ||
475 | func (s *Server) connectedUsers() []Field { | |
476 | //s.mux.Lock() | |
477 | //defer s.mux.Unlock() | |
478 | ||
479 | var connectedUsers []Field | |
480 | for _, c := range sortedClients(s.Clients) { | |
481 | b, err := io.ReadAll(&User{ | |
482 | ID: c.ID, | |
483 | Icon: c.Icon, | |
484 | Flags: c.Flags[:], | |
485 | Name: string(c.UserName), | |
486 | }) | |
487 | if err != nil { | |
488 | return nil | |
489 | } | |
490 | connectedUsers = append(connectedUsers, NewField(FieldUsernameWithInfo, b)) | |
491 | } | |
492 | return connectedUsers | |
493 | } | |
494 | ||
495 | // loadFromYAMLFile loads data from a YAML file into the provided data structure. | |
496 | func loadFromYAMLFile(path string, data interface{}) error { | |
497 | fh, err := os.Open(path) | |
498 | if err != nil { | |
499 | return err | |
500 | } | |
501 | defer fh.Close() | |
502 | ||
503 | decoder := yaml.NewDecoder(fh) | |
504 | return decoder.Decode(data) | |
505 | } | |
506 | ||
507 | // loadAccounts loads account data from disk | |
508 | func (s *Server) loadAccounts(userDir string) error { | |
509 | matches, err := filepath.Glob(filepath.Join(userDir, "*.yaml")) | |
510 | if err != nil { | |
511 | return err | |
512 | } | |
513 | ||
514 | if len(matches) == 0 { | |
515 | return fmt.Errorf("no accounts found in directory: %s", userDir) | |
516 | } | |
517 | ||
518 | for _, file := range matches { | |
519 | var account Account | |
520 | if err = loadFromYAMLFile(file, &account); err != nil { | |
521 | return fmt.Errorf("error loading account %s: %w", file, err) | |
522 | } | |
523 | ||
524 | s.Accounts[account.Login] = &account | |
525 | } | |
526 | return nil | |
527 | } | |
528 | ||
529 | func (s *Server) loadConfig(path string) error { | |
530 | fh, err := s.FS.Open(path) | |
531 | if err != nil { | |
532 | return err | |
533 | } | |
534 | ||
535 | decoder := yaml.NewDecoder(fh) | |
536 | err = decoder.Decode(s.Config) | |
537 | if err != nil { | |
538 | return err | |
539 | } | |
540 | ||
541 | validate := validator.New() | |
542 | err = validate.Struct(s.Config) | |
543 | if err != nil { | |
544 | return err | |
545 | } | |
546 | return nil | |
547 | } | |
548 | ||
549 | func sendBanMessage(rwc io.Writer, message string) { | |
550 | t := NewTransaction( | |
551 | TranServerMsg, | |
552 | [2]byte{0, 0}, | |
553 | NewField(FieldData, []byte(message)), | |
554 | NewField(FieldChatOptions, []byte{0, 0}), | |
555 | ) | |
556 | _, _ = io.Copy(rwc, &t) | |
557 | time.Sleep(1 * time.Second) | |
558 | } | |
559 | ||
560 | // handleNewConnection takes a new net.Conn and performs the initial login sequence | |
561 | func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser, remoteAddr string) error { | |
562 | defer dontPanic(s.Logger) | |
563 | ||
564 | // Check if remoteAddr is present in the ban list | |
565 | ipAddr := strings.Split(remoteAddr, ":")[0] | |
566 | if banUntil, ok := s.banList[ipAddr]; ok { | |
567 | // permaban | |
568 | if banUntil == nil { | |
569 | sendBanMessage(rwc, "You are permanently banned on this server") | |
570 | s.Logger.Debug("Disconnecting permanently banned IP", "remoteAddr", ipAddr) | |
571 | return nil | |
572 | } | |
573 | ||
574 | // temporary ban | |
575 | if time.Now().Before(*banUntil) { | |
576 | sendBanMessage(rwc, "You are temporarily banned on this server") | |
577 | s.Logger.Debug("Disconnecting temporarily banned IP", "remoteAddr", ipAddr) | |
578 | return nil | |
579 | } | |
580 | } | |
581 | ||
582 | if err := performHandshake(rwc); err != nil { | |
583 | return fmt.Errorf("error performing handshake: %w", err) | |
584 | } | |
585 | ||
586 | // Create a new scanner for parsing incoming bytes into transaction tokens | |
587 | scanner := bufio.NewScanner(rwc) | |
588 | scanner.Split(transactionScanner) | |
589 | ||
590 | scanner.Scan() | |
591 | ||
592 | // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the | |
593 | // scanner re-uses the buffer for subsequent scans. | |
594 | buf := make([]byte, len(scanner.Bytes())) | |
595 | copy(buf, scanner.Bytes()) | |
596 | ||
597 | var clientLogin Transaction | |
598 | if _, err := clientLogin.Write(buf); err != nil { | |
599 | return fmt.Errorf("error writing login transaction: %w", err) | |
600 | } | |
601 | ||
602 | c := s.NewClientConn(rwc, remoteAddr) | |
603 | defer c.Disconnect() | |
604 | ||
605 | encodedPassword := clientLogin.GetField(FieldUserPassword).Data | |
606 | c.Version = clientLogin.GetField(FieldVersion).Data | |
607 | ||
608 | login := string(encodeString(clientLogin.GetField(FieldUserLogin).Data)) | |
609 | if login == "" { | |
610 | login = GuestAccount | |
611 | } | |
612 | ||
613 | c.logger = s.Logger.With("remoteAddr", remoteAddr, "login", login) | |
614 | ||
615 | // If authentication fails, send error reply and close connection | |
616 | if !c.Authenticate(login, encodedPassword) { | |
617 | t := c.NewErrReply(&clientLogin, "Incorrect login.")[0] | |
618 | ||
619 | _, err := io.Copy(rwc, &t) | |
620 | if err != nil { | |
621 | return err | |
622 | } | |
623 | ||
624 | c.logger.Info("Login failed", "clientVersion", fmt.Sprintf("%x", c.Version)) | |
625 | ||
626 | return nil | |
627 | } | |
628 | ||
629 | if clientLogin.GetField(FieldUserIconID).Data != nil { | |
630 | c.Icon = clientLogin.GetField(FieldUserIconID).Data | |
631 | } | |
632 | ||
633 | c.Lock() | |
634 | c.Account = c.Server.Accounts[login] | |
635 | c.Unlock() | |
636 | ||
637 | if clientLogin.GetField(FieldUserName).Data != nil { | |
638 | if c.Authorize(accessAnyName) { | |
639 | c.UserName = clientLogin.GetField(FieldUserName).Data | |
640 | } else { | |
641 | c.UserName = []byte(c.Account.Name) | |
642 | } | |
643 | } | |
644 | ||
645 | if c.Authorize(accessDisconUser) { | |
646 | c.Flags.Set(UserFlagAdmin, 1) | |
647 | } | |
648 | ||
649 | s.outbox <- c.NewReply(&clientLogin, | |
650 | NewField(FieldVersion, []byte{0x00, 0xbe}), | |
651 | NewField(FieldCommunityBannerID, []byte{0, 0}), | |
652 | NewField(FieldServerName, []byte(s.Config.Name)), | |
653 | ) | |
654 | ||
655 | // Send user access privs so client UI knows how to behave | |
656 | c.Server.outbox <- NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:])) | |
657 | ||
658 | // Accounts with accessNoAgreement do not receive the server agreement on login. The behavior is different between | |
659 | // client versions. For 1.2.3 client, we do not send TranShowAgreement. For other client versions, we send | |
660 | // TranShowAgreement but with the NoServerAgreement field set to 1. | |
661 | if c.Authorize(accessNoAgreement) { | |
662 | // If client version is nil, then the client uses the 1.2.3 login behavior | |
663 | if c.Version != nil { | |
664 | c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldNoServerAgreement, []byte{1})) | |
665 | } | |
666 | } else { | |
667 | c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldData, s.Agreement)) | |
668 | } | |
669 | ||
670 | // If the client has provided a username as part of the login, we can infer that it is using the 1.2.3 login | |
671 | // flow and not the 1.5+ flow. | |
672 | if len(c.UserName) != 0 { | |
673 | // Add the client username to the logger. For 1.5+ clients, we don't have this information yet as it comes as | |
674 | // part of TranAgreed | |
675 | c.logger = c.logger.With("Name", string(c.UserName)) | |
676 | c.logger.Info("Login successful", "clientVersion", "Not sent (probably 1.2.3)") | |
677 | ||
678 | // Notify other clients on the server that the new user has logged in. For 1.5+ clients we don't have this | |
679 | // information yet, so we do it in TranAgreed instead | |
680 | for _, t := range c.notifyOthers( | |
681 | NewTransaction( | |
682 | TranNotifyChangeUser, [2]byte{0, 0}, | |
683 | NewField(FieldUserName, c.UserName), | |
684 | NewField(FieldUserID, c.ID[:]), | |
685 | NewField(FieldUserIconID, c.Icon), | |
686 | NewField(FieldUserFlags, c.Flags[:]), | |
687 | ), | |
688 | ) { | |
689 | c.Server.outbox <- t | |
690 | } | |
691 | } | |
692 | ||
693 | c.Server.mux.Lock() | |
694 | c.Server.Stats.ConnectionCounter += 1 | |
695 | if len(s.Clients) > c.Server.Stats.ConnectionPeak { | |
696 | c.Server.Stats.ConnectionPeak = len(s.Clients) | |
697 | } | |
698 | c.Server.mux.Unlock() | |
699 | ||
700 | // Scan for new transactions and handle them as they come in. | |
701 | for scanner.Scan() { | |
702 | // Copy the scanner bytes to a new slice to it to avoid a data race when the scanner re-uses the buffer. | |
703 | buf := make([]byte, len(scanner.Bytes())) | |
704 | copy(buf, scanner.Bytes()) | |
705 | ||
706 | var t Transaction | |
707 | if _, err := t.Write(buf); err != nil { | |
708 | return err | |
709 | } | |
710 | ||
711 | c.handleTransaction(t) | |
712 | } | |
713 | return nil | |
714 | } | |
715 | ||
716 | func (s *Server) NewPrivateChat(cc *ClientConn) [4]byte { | |
717 | s.PrivateChatsMu.Lock() | |
718 | defer s.PrivateChatsMu.Unlock() | |
719 | ||
720 | var randID [4]byte | |
721 | _, _ = rand.Read(randID[:]) | |
722 | ||
723 | s.PrivateChats[randID] = &PrivateChat{ | |
724 | ClientConn: make(map[[2]byte]*ClientConn), | |
725 | } | |
726 | s.PrivateChats[randID].ClientConn[cc.ID] = cc | |
727 | ||
728 | return randID | |
729 | } | |
730 | ||
731 | const dlFldrActionSendFile = 1 | |
732 | const dlFldrActionResumeFile = 2 | |
733 | const dlFldrActionNextFile = 3 | |
734 | ||
735 | // handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection | |
736 | func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error { | |
737 | defer dontPanic(s.Logger) | |
738 | ||
739 | // The first 16 bytes contain the file transfer. | |
740 | var t transfer | |
741 | if _, err := io.CopyN(&t, rwc, 16); err != nil { | |
742 | return fmt.Errorf("error reading file transfer: %w", err) | |
743 | } | |
744 | ||
745 | defer func() { | |
746 | s.mux.Lock() | |
747 | delete(s.fileTransfers, t.ReferenceNumber) | |
748 | s.mux.Unlock() | |
749 | ||
750 | // Wait a few seconds before closing the connection: this is a workaround for problems | |
751 | // observed with Windows clients where the client must initiate close of the TCP connection before | |
752 | // the server does. This is gross and seems unnecessary. TODO: Revisit? | |
753 | time.Sleep(3 * time.Second) | |
754 | }() | |
755 | ||
756 | s.mux.Lock() | |
757 | fileTransfer, ok := s.fileTransfers[t.ReferenceNumber] | |
758 | s.mux.Unlock() | |
759 | if !ok { | |
760 | return errors.New("invalid transaction ID") | |
761 | } | |
762 | ||
763 | defer func() { | |
764 | fileTransfer.ClientConn.transfersMU.Lock() | |
765 | delete(fileTransfer.ClientConn.transfers[fileTransfer.Type], t.ReferenceNumber) | |
766 | fileTransfer.ClientConn.transfersMU.Unlock() | |
767 | }() | |
768 | ||
769 | rLogger := s.Logger.With( | |
770 | "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr, | |
771 | "login", fileTransfer.ClientConn.Account.Login, | |
772 | "Name", string(fileTransfer.ClientConn.UserName), | |
773 | ) | |
774 | ||
775 | fullPath, err := readPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName) | |
776 | if err != nil { | |
777 | return err | |
778 | } | |
779 | ||
780 | switch fileTransfer.Type { | |
781 | case bannerDownload: | |
782 | if _, err := io.Copy(rwc, bytes.NewBuffer(s.banner)); err != nil { | |
783 | return fmt.Errorf("error sending banner: %w", err) | |
784 | } | |
785 | case FileDownload: | |
786 | s.Stats.DownloadCounter += 1 | |
787 | s.Stats.DownloadsInProgress += 1 | |
788 | defer func() { | |
789 | s.Stats.DownloadsInProgress -= 1 | |
790 | }() | |
791 | ||
792 | err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true) | |
793 | if err != nil { | |
794 | return fmt.Errorf("file download error: %w", err) | |
795 | } | |
796 | ||
797 | case FileUpload: | |
798 | s.Stats.UploadCounter += 1 | |
799 | s.Stats.UploadsInProgress += 1 | |
800 | defer func() { s.Stats.UploadsInProgress -= 1 }() | |
801 | ||
802 | err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks) | |
803 | if err != nil { | |
804 | return fmt.Errorf("file upload error: %w", err) | |
805 | } | |
806 | ||
807 | case FolderDownload: | |
808 | s.Stats.DownloadCounter += 1 | |
809 | s.Stats.DownloadsInProgress += 1 | |
810 | defer func() { s.Stats.DownloadsInProgress -= 1 }() | |
811 | ||
812 | err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks) | |
813 | if err != nil { | |
814 | return fmt.Errorf("file upload error: %w", err) | |
815 | } | |
816 | ||
817 | case FolderUpload: | |
818 | s.Stats.UploadCounter += 1 | |
819 | s.Stats.UploadsInProgress += 1 | |
820 | defer func() { s.Stats.UploadsInProgress -= 1 }() | |
821 | rLogger.Info( | |
822 | "Folder upload started", | |
823 | "dstPath", fullPath, | |
824 | "TransferSize", binary.BigEndian.Uint32(fileTransfer.TransferSize), | |
825 | "FolderItemCount", fileTransfer.FolderItemCount, | |
826 | ) | |
827 | ||
828 | err = UploadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks) | |
829 | if err != nil { | |
830 | return fmt.Errorf("file upload error: %w", err) | |
831 | } | |
832 | } | |
833 | return nil | |
834 | } |