github.com/la5nta/wl2k-go@v0.11.8/fbb/handshake.go (about) 1 // Copyright 2015 Martin Hebnes Pedersen (LA5NTA). All rights reserved. 2 // Use of this source code is governed by the MIT-license that can be 3 // found in the LICENSE file. 4 5 package fbb 6 7 import ( 8 "bufio" 9 "errors" 10 "fmt" 11 "io" 12 "os" 13 "regexp" 14 "strings" 15 ) 16 17 var ErrNoFB2 = errors.New("Remote does not support B2 Forwarding Protocol") 18 19 // IsLoginFailure returns a boolean indicating whether the error is known to 20 // report that the secure login failed. 21 func IsLoginFailure(err error) bool { 22 if err == nil { 23 return false 24 } 25 errStr := strings.ToLower(err.Error()) 26 return strings.Contains(errStr, "secure login failed") 27 } 28 29 func (s *Session) handshake(rw io.ReadWriter) error { 30 if s.master { 31 // Send MOTD lines 32 for _, line := range s.motd { 33 fmt.Fprintf(rw, "%s\r", line) 34 } 35 36 if err := s.sendHandshake(rw, ""); err != nil { 37 return err 38 } 39 } 40 41 hs, err := s.readHandshake() 42 if err != nil { 43 return err 44 } 45 46 // Did we get SID codes? 47 if hs.SID == "" { 48 return errors.New("No sid in handshake") 49 } 50 51 s.remoteSID = hs.SID 52 s.remoteFW = hs.FW 53 54 if !s.master { 55 return s.sendHandshake(rw, hs.SecureChallenge) 56 } else { 57 return nil 58 } 59 } 60 61 type handshakeData struct { 62 SID sid 63 FW []Address 64 SecureChallenge string 65 } 66 67 func (s *Session) readHandshake() (handshakeData, error) { 68 data := handshakeData{} 69 70 for { 71 if bytes, err := s.rd.Peek(1); err != nil { 72 return data, err 73 } else if bytes[0] == 'F' && s.master { 74 return data, nil // Next line is a protocol command, handshake is done 75 } 76 77 // Ignore remote errors here, as the server sometimes sends lines like 78 // '*** MTD Stats Total connects = 2580 Total messages = 3900', which 79 // are not errors 80 line, err := s.nextLineRemoteErr(false) 81 if err != nil { 82 return data, err 83 } 84 85 //REVIEW: We should probably be more strict on what to allow here, 86 // to ensure we disconnect early if the remote is not talking the expected 87 // protocol. (We should at least allow unknown ; prefixed lines aka "comments") 88 switch { 89 // Header with sid (ie. [WL2K-2.8.4.8-B2FWIHJM$]) 90 case isSID(line): 91 data.SID, err = parseSID(line) 92 if err != nil { 93 return data, err 94 } 95 96 // Do we support the remote's SID codes? 97 if !data.SID.Has(sFBComp2) { // We require FBB compressed protocol v2 for now 98 return data, ErrNoFB2 99 } 100 case strings.HasPrefix(line, ";FW"): // Forwarders 101 data.FW, err = parseFW(line) 102 if err != nil { 103 return data, err 104 } 105 case strings.HasPrefix(line, ";PQ"): // Secure password challenge 106 data.SecureChallenge = line[5:] 107 108 case strings.HasSuffix(line, ">"): // Prompt 109 return data, nil 110 default: 111 // Ignore 112 } 113 } 114 } 115 116 func (s *Session) sendHandshake(writer io.Writer, secureChallenge string) error { 117 if secureChallenge != "" && s.secureLoginHandleFunc == nil { 118 return errors.New("Got secure login challenge, please register a SecureLoginHandleFunc.") 119 } 120 121 w := bufio.NewWriter(writer) 122 123 // Request messages on behalf of every localFW 124 fmt.Fprintf(w, ";FW:") 125 for i, addr := range s.localFW { 126 switch { 127 case secureChallenge != "" && i > 0: 128 // Include passwordhash for auxiliary addresses (required by WL2K-4.x or later) 129 if password, _ := s.secureLoginHandleFunc(addr); password != "" { 130 resp := secureLoginResponse(secureChallenge, password) 131 // In the B2F specs they use space as delimiter, but Winlink Express uses pipe. 132 // I'm not sure space as a delimiter would even work when passwords for aux addresses 133 // are optional (according to the very same document). 134 fmt.Fprintf(w, " %s|%s", addr.Addr, resp) 135 break 136 } 137 // Password is not required for all aux addresses according to Winlink's B2F specs. 138 fallthrough 139 default: 140 fmt.Fprintf(w, " %s", addr.Addr) 141 } 142 } 143 fmt.Fprintf(w, "\r") 144 145 writeSID(w, s.ua.Name, s.ua.Version) 146 147 if secureChallenge != "" { 148 password, err := s.secureLoginHandleFunc(s.localFW[0]) 149 if err != nil { 150 return err 151 } 152 resp := secureLoginResponse(secureChallenge, password) 153 writeSecureLoginResponse(w, resp) 154 } 155 156 fmt.Fprintf(w, "; %s DE %s (%s)", s.targetcall, s.mycall, s.locator) 157 if s.master { 158 fmt.Fprintf(w, ">\r") 159 } else { 160 fmt.Fprintf(w, "\r") 161 } 162 163 return w.Flush() 164 } 165 166 func parseFW(line string) ([]Address, error) { 167 if !strings.HasPrefix(line, ";FW: ") { 168 return nil, errors.New("Malformed forward line") 169 } 170 171 fws := strings.Split(line[5:], " ") 172 addrs := make([]Address, 0, len(fws)) 173 174 for _, str := range strings.Split(line[5:], " ") { 175 str = strings.Split(str, "|")[0] // Strip password hashes (unsupported) 176 addrs = append(addrs, AddressFromString(str)) 177 } 178 179 return addrs, nil 180 } 181 182 type sid string 183 184 const localSID = sFBComp2 + sFBBasic + sHL + sMID + sBID 185 186 // The SID codes 187 const ( 188 sAckForPM = "A" // Acknowledge for person messages 189 sFBBasic = "F" // FBB basic ascii protocol supported 190 sFBComp0 = "B" // FBB compressed protocol v0 supported 191 sFBComp1 = "B1" // FBB compressed protocol v1 supported 192 sFBComp2 = "B2" // FBB compressed protocol v2 (aka B2F) supported 193 sHL = "H" // Hierarchical Location designators supported 194 sMID = "M" // Message identifier supported 195 sCompBatchF = "X" // Compressed batch forwarding supported 196 sI = "I" // "Identify"? Palink-unix sends ";target de mycall QTC n" when remote has this 197 sBID = "$" // BID supported (must be last character in SID) 198 199 sGzip = "G" // Gzip compressed messages supported (GZIP_EXPERIMENT) 200 ) 201 202 func gzipExperimentEnabled() bool { return os.Getenv("GZIP_EXPERIMENT") == "1" } 203 204 func writeSID(w io.Writer, appName, appVersion string) error { 205 sid := localSID 206 207 if gzipExperimentEnabled() { 208 sid = sid[0:len(sid)-1] + sGzip + sid[len(sid)-1:] 209 } 210 211 _, err := fmt.Fprintf(w, "[%s-%s-%s]\r", appName, appVersion, sid) 212 return err 213 } 214 215 func writeSecureLoginResponse(w io.Writer, response string) error { 216 _, err := fmt.Fprintf(w, ";PR: %s\r", response) 217 return err 218 } 219 220 func isSID(str string) bool { 221 return strings.HasPrefix(str, `[`) && strings.HasSuffix(str, `]`) 222 } 223 224 func parseSID(str string) (sid, error) { 225 code := regexp.MustCompile(`\[.*-(.*)\]`).FindStringSubmatch(str) 226 if len(code) != 2 { 227 return sid(""), errors.New(`Bad SID line: ` + str) 228 } 229 230 return sid( 231 strings.ToUpper(code[len(code)-1]), 232 ), nil 233 } 234 235 func (s sid) Has(code string) bool { 236 return strings.Contains(string(s), strings.ToUpper(code)) 237 }