-
Notifications
You must be signed in to change notification settings - Fork 89
/
protocol.go
206 lines (176 loc) · 4.48 KB
/
protocol.go
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
/* Copyright (c) 2021 Bram Vandenbogaerde And Contributors
* You may use, distribute or modify this code under the
* terms of the Mozilla Public License 2.0, which is distributed
* along with the source code.
*/
package scp
import (
"bufio"
"errors"
"fmt"
"io"
"strconv"
"strings"
)
type ResponseType = byte
const (
Ok ResponseType = 0
Warning ResponseType = 1
Error ResponseType = 2
Create ResponseType = 'C'
Time ResponseType = 'T'
)
// ParseResponse reads from the given reader (assuming it is the output of the remote) and parses it into a Response structure.
func ParseResponse(reader io.Reader, writer io.Writer) (*FileInfos, error) {
fileInfos := NewFileInfos()
buffer := make([]uint8, 1)
_, err := reader.Read(buffer)
if err != nil {
return fileInfos, err
}
responseType := buffer[0]
message := ""
if responseType > 0 {
bufferedReader := bufio.NewReader(reader)
message, err = bufferedReader.ReadString('\n')
if err != nil {
return fileInfos, err
}
if responseType == Warning || responseType == Error {
return fileInfos, errors.New(message)
}
// Exit early because we're only interested in the ok response
if responseType == Ok {
return fileInfos, nil
}
if !(responseType == Create || responseType == Time) {
return fileInfos, errors.New(
fmt.Sprintf(
"Message does not follow scp protocol: %s\n Cmmmm <length> <filename> or T<mtime> 0 <atime> 0",
message,
),
)
}
if responseType == Time {
err = ParseFileTime(message, fileInfos)
if err != nil {
return nil, err
}
// A custom ssh server can send both time, permissions and size information at once
// without needing an Ack response. Example: wish from charmbracelet while using their default scp implementation
// If the buffer is empty, then it's likely the default implementation for ssh, so send Ack
if bufferedReader.Buffered() == 0 {
err = Ack(writer)
if err != nil {
return fileInfos, err
}
}
message, err = bufferedReader.ReadString('\n')
if err != nil {
return fileInfos, err
}
responseType = message[0]
}
if responseType == Create {
err = ParseFileInfos(message, fileInfos)
if err != nil {
return nil, err
}
}
}
return fileInfos, nil
}
type FileInfos struct {
Message string
Filename string
Permissions uint32
Size int64
Atime int64
Mtime int64
}
func NewFileInfos() *FileInfos {
return &FileInfos{}
}
func (fileInfos *FileInfos) Update(new *FileInfos) {
if new == nil {
return
}
if new.Filename != "" {
fileInfos.Filename = new.Filename
}
if new.Permissions != 0 {
fileInfos.Permissions = new.Permissions
}
if new.Size != 0 {
fileInfos.Size = new.Size
}
if new.Atime != 0 {
fileInfos.Atime = new.Atime
}
if new.Mtime != 0 {
fileInfos.Mtime = new.Mtime
}
}
func ParseFileInfos(message string, fileInfos *FileInfos) error {
processMessage := strings.ReplaceAll(message, "\n", "")
parts := strings.Split(processMessage, " ")
if len(parts) < 3 {
return errors.New("unable to parse Chmod protocol")
}
permissions, err := strconv.ParseUint(parts[0][1:], 0, 32)
if err != nil {
return err
}
size, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return err
}
fileInfos.Update(&FileInfos{
Filename: parts[2],
Permissions: uint32(permissions),
Size: int64(size),
})
return nil
}
func ParseFileTime(
message string,
fileInfos *FileInfos,
) error {
processMessage := strings.ReplaceAll(message, "\n", "")
parts := strings.Split(processMessage, " ")
if len(parts) < 3 {
return errors.New("unable to parse Time protocol")
}
if len(parts[0]) != 10 {
return errors.New("length of ATime is not 10")
}
mTime, err := strconv.Atoi(parts[0][0:10])
if err != nil {
return errors.New("unable to parse ATime component of message")
}
if len(parts[2]) != 10 {
return errors.New("length of MTime is not 10")
}
aTime, err := strconv.Atoi(parts[2][0:10])
if err != nil {
return errors.New("unable to parse MTime component of message")
}
fileInfos.Update(&FileInfos{
Atime: int64(aTime),
Mtime: int64(mTime),
})
return nil
}
// Ack writes an `Ack` message to the remote, does not await its response, a seperate call to ParseResponse is
// therefore required to check if the acknowledgement succeeded.
func Ack(writer io.Writer) error {
var msg = []byte{0}
n, err := writer.Write(msg)
if err != nil {
return err
}
if n < len(msg) {
return errors.New("failed to write ack buffer")
}
return nil
}