Blame view

publish/skynet/lualib/http/internal.lua 4.61 KB
4d6f285d   zhouhaihai   增加发布功能
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
224
225
  local table = table
  local type = type
  
  local M = {}
  
  local LIMIT = 8192
  
  local function chunksize(readbytes, body)
  	while true do
  		local f,e = body:find("\r\n",1,true)
  		if f then
  			return tonumber(body:sub(1,f-1),16), body:sub(e+1)
  		end
  		if #body > 128 then
  			-- pervent the attacker send very long stream without \r\n
  			return
  		end
  		body = body .. readbytes()
  	end
  end
  
  local function readcrln(readbytes, body)
  	if #body >= 2 then
  		if body:sub(1,2) ~= "\r\n" then
  			return
  		end
  		return body:sub(3)
  	else
  		body = body .. readbytes(2-#body)
  		if body ~= "\r\n" then
  			return
  		end
  		return ""
  	end
  end
  
  function M.recvheader(readbytes, lines, header)
  	if #header >= 2 then
  		if header:find "^\r\n" then
  			return header:sub(3)
  		end
  	end
  	local result
  	local e = header:find("\r\n\r\n", 1, true)
  	if e then
  		result = header:sub(e+4)
  	else
  		while true do
  			local bytes = readbytes()
  			header = header .. bytes
  			e = header:find("\r\n\r\n", -#bytes-3, true)
  			if e then
  				result = header:sub(e+4)
  				break
  			end
  			if header:find "^\r\n" then
  				return header:sub(3)
  			end
  			if #header > LIMIT then
  				return
  			end
  		end
  	end
  	for v in header:gmatch("(.-)\r\n") do
  		if v == "" then
  			break
  		end
  		table.insert(lines, v)
  	end
  	return result
  end
  
  function M.parseheader(lines, from, header)
  	local name, value
  	for i=from,#lines do
  		local line = lines[i]
  		if line:byte(1) == 9 then	-- tab, append last line
  			if name == nil then
  				return
  			end
  			header[name] = header[name] .. line:sub(2)
  		else
  			name, value = line:match "^(.-):%s*(.*)"
  			if name == nil or value == nil then
  				return
  			end
  			name = name:lower()
  			if header[name] then
  				local v = header[name]
  				if type(v) == "table" then
  					table.insert(v, value)
  				else
  					header[name] = { v , value }
  				end
  			else
  				header[name] = value
  			end
  		end
  	end
  	return header
  end
  
  function M.recvchunkedbody(readbytes, bodylimit, header, body)
  	local result = ""
  	local size = 0
  
  	while true do
  		local sz
  		sz , body = chunksize(readbytes, body)
  		if not sz then
  			return
  		end
  		if sz == 0 then
  			break
  		end
  		size = size + sz
  		if bodylimit and size > bodylimit then
  			return
  		end
  		if #body >= sz then
  			result = result .. body:sub(1,sz)
  			body = body:sub(sz+1)
  		else
  			result = result .. body .. readbytes(sz - #body)
  			body = ""
  		end
  		body = readcrln(readbytes, body)
  		if not body then
  			return
  		end
  	end
  
  	local tmpline = {}
  	body = M.recvheader(readbytes, tmpline, body)
  	if not body then
  		return
  	end
  
  	header = M.parseheader(tmpline,1,header)
  
  	return result, header
  end
  
  
  function M.request(interface, method, host, url, recvheader, header, content)
  	local is_ws = interface.websocket
  	local read = interface.read
  	local write = interface.write
  	local header_content = ""
  	if header then
  		if not header.host then
  			header.host = host
  		end
  		for k,v in pairs(header) do
  			header_content = string.format("%s%s:%s\r\n", header_content, k, v)
  		end
  	else
  		header_content = string.format("host:%s\r\n",host)
  	end
  
  	if content then
  		local data = string.format("%s %s HTTP/1.1\r\n%scontent-length:%d\r\n\r\n", method, url, header_content, #content)
  		write(data)
  		write(content)
  	else
  		local request_header = string.format("%s %s HTTP/1.1\r\n%scontent-length:0\r\n\r\n", method, url, header_content)
  		write(request_header)
  	end
  
  	local tmpline = {}
  	local body = M.recvheader(read, tmpline, "")
  	if not body then
  		error(socket.socket_error)
  	end
  
  	local statusline = tmpline[1]
  	local code, info = statusline:match "HTTP/[%d%.]+%s+([%d]+)%s+(.*)$"
  	code = assert(tonumber(code))
  
  	local header = M.parseheader(tmpline,2,recvheader or {})
  	if not header then
  		error("Invalid HTTP response header")
  	end
  
  	local length = header["content-length"]
  	if length then
  		length = tonumber(length)
  	end
  	local mode = header["transfer-encoding"]
  	if mode then
  		if mode ~= "identity" and mode ~= "chunked" then
  			error ("Unsupport transfer-encoding")
  		end
  	end
  
  	if mode == "chunked" then
  		body, header = M.recvchunkedbody(read, nil, header, body)
  		if not body then
  			error("Invalid response body")
  		end
  	else
  		-- identity mode
  		if length then
  			if #body >= length then
  				body = body:sub(1,length)
  			else
  				local padding = read(length - #body)
  				body = body .. padding
  			end
  		elseif code == 204 or code == 304 or code < 200 then
  			body = ""
  			-- See https://stackoverflow.com/questions/15991173/is-the-content-length-header-required-for-a-http-1-0-response
  		elseif is_ws and code == 101 then
  			-- if websocket handshake success
  				return code, body
  		else
  			-- no content-length, read all
  			body = body .. interface.readall()
  		end
  	end
  
  	return code, body
  end
  
  return M