PageRenderTime 32ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/handler.go

https://gitlab.com/magan/gobits
Go | 354 lines | 255 code | 46 blank | 53 comment | 79 complexity | 6c605a93313946cb29c9325348613721 MD5 | raw file
  1. package gobits
  2. import (
  3. "io/ioutil"
  4. "net/http"
  5. "os"
  6. "path"
  7. "path/filepath"
  8. "regexp"
  9. "strconv"
  10. "strings"
  11. )
  12. // ServeHTTP handler
  13. func (b *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  14. // Only allow BITS requests
  15. if r.Method != b.cfg.AllowedMethod {
  16. http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
  17. return
  18. }
  19. // get packet type and session id
  20. packetType := strings.ToLower(r.Header.Get("BITS-Packet-Type"))
  21. sessionID := r.Header.Get("BITS-Session-Id")
  22. // Take appropriate action based on what type of packet we got
  23. switch packetType {
  24. case "ping":
  25. b.bitsPing(w, r)
  26. case "create-session":
  27. b.bitsCreate(w, r)
  28. case "cancel-session":
  29. b.bitsCancel(w, r, sessionID)
  30. case "close-session":
  31. b.bitsClose(w, r, sessionID)
  32. case "fragment":
  33. b.bitsFragment(w, r, sessionID)
  34. default:
  35. bitsError(w, "", http.StatusBadRequest, 0, ErrorContextRemoteFile)
  36. }
  37. }
  38. // use the Ping packet to establish a connection and negotiate security with the server.
  39. // https://msdn.microsoft.com/en-us/library/aa363135(v=vs.85).aspx
  40. func (b *Handler) bitsPing(w http.ResponseWriter, r *http.Request) {
  41. w.Header().Add("BITS-Packet-Type", "Ack")
  42. w.Write(nil)
  43. }
  44. // use the Create-Session packet to request an upload session with the BITS server.
  45. // https://msdn.microsoft.com/en-us/library/aa362833(v=vs.85).aspx
  46. func (b *Handler) bitsCreate(w http.ResponseWriter, r *http.Request) {
  47. // Check for correct protocol
  48. var protocol string
  49. protocols := strings.Split(r.Header.Get("BITS-Supported-Protocols"), " ")
  50. for _, protocol = range protocols {
  51. if protocol == b.cfg.AllowedMethod {
  52. break
  53. }
  54. }
  55. if protocol != b.cfg.Protocol {
  56. // no matching protocol found
  57. bitsError(w, "", http.StatusBadRequest, 0, ErrorContextRemoteFile)
  58. return
  59. }
  60. // Create new session UUID
  61. uuid, err := newUUID()
  62. if err != nil {
  63. bitsError(w, "", http.StatusInternalServerError, 0, ErrorContextRemoteFile)
  64. return
  65. }
  66. // Create session directory
  67. tmpDir := path.Join(b.cfg.TempDir, uuid)
  68. if err = os.MkdirAll(tmpDir, 0600); err != nil {
  69. bitsError(w, "", http.StatusInternalServerError, 0, ErrorContextRemoteFile)
  70. return
  71. }
  72. // make sure we actually have a callback before calling it
  73. if b.callback != nil {
  74. b.callback(EventCreateSession, uuid, tmpDir)
  75. }
  76. // https://msdn.microsoft.com/en-us/library/aa362771(v=vs.85).aspx
  77. w.Header().Add("BITS-Packet-Type", "Ack")
  78. w.Header().Add("BITS-Protocol", protocol)
  79. w.Header().Add("BITS-Session-Id", uuid)
  80. w.Header().Add("Accept-Encoding", "Identity")
  81. w.Write(nil)
  82. }
  83. // Use the Fragment packet to send a fragment of the upload file to the server
  84. // https://msdn.microsoft.com/en-us/library/aa362842(v=vs.85).aspx
  85. func (b *Handler) bitsFragment(w http.ResponseWriter, r *http.Request, uuid string) {
  86. // Check for correct session
  87. if uuid == "" {
  88. bitsError(w, "", http.StatusBadRequest, 0, ErrorContextRemoteFile)
  89. return
  90. }
  91. // Check for existing session
  92. var srcDir string
  93. srcDir = path.Join(b.cfg.TempDir, uuid)
  94. if b, _ := exists(srcDir); !b {
  95. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  96. return
  97. }
  98. // Get filename and make sure the path is correct
  99. _, filename := path.Split(r.RequestURI)
  100. if filename == "" {
  101. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  102. return
  103. }
  104. var err error
  105. var match bool
  106. // See if filename is blacklisted. If so, return an error
  107. for _, reg := range b.cfg.Disallowed {
  108. match, err = regexp.MatchString(reg, filename)
  109. if err != nil {
  110. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  111. return
  112. }
  113. if match {
  114. // File is blacklisted
  115. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  116. return
  117. }
  118. }
  119. // See if filename is whitelisted
  120. allowed := false
  121. for _, reg := range b.cfg.Allowed {
  122. match, err = regexp.MatchString(reg, filename)
  123. if err != nil {
  124. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  125. return
  126. }
  127. if match {
  128. allowed = true
  129. break
  130. }
  131. }
  132. if !allowed {
  133. // No whitelisting rules matched!
  134. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  135. return
  136. }
  137. var src string
  138. // Get absolute paths to file
  139. src, err = filepath.Abs(filepath.Join(srcDir, filename))
  140. if err != nil {
  141. src = filepath.Join(srcDir, filename)
  142. }
  143. // Parse range
  144. var rangeStart, rangeEnd, fileLength uint64
  145. rangeStart, rangeEnd, fileLength, err = parseRange(r.Header.Get("Content-Range"))
  146. if err != nil {
  147. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  148. return
  149. }
  150. // Check filesize
  151. if b.cfg.MaxSize > 0 && fileLength > b.cfg.MaxSize {
  152. bitsError(w, uuid, http.StatusRequestEntityTooLarge, 0, ErrorContextRemoteFile)
  153. return
  154. }
  155. // Get the length of the posted data
  156. var fragmentSize uint64
  157. fragmentSize, err = strconv.ParseUint(r.Header.Get("Content-Length"), 10, 64)
  158. if err != nil {
  159. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  160. return
  161. }
  162. // Get posted data and confirm size
  163. data, err := ioutil.ReadAll(r.Body) // should probably not read everything into memory like this
  164. if err != nil {
  165. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  166. return
  167. }
  168. if uint64(len(data)) != fragmentSize {
  169. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  170. return
  171. }
  172. // Check that content-range size matches content-length
  173. if rangeEnd-rangeStart+1 != fragmentSize {
  174. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  175. return
  176. }
  177. // Open or create file
  178. var file *os.File
  179. var fileSize uint64
  180. var exist bool
  181. exist, err = exists(src)
  182. if err != nil {
  183. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  184. return
  185. }
  186. if exist {
  187. // Create file
  188. file, err = os.OpenFile(src, os.O_CREATE|os.O_WRONLY, 0600)
  189. if err != nil {
  190. bitsError(w, uuid, http.StatusInternalServerError, 0, ErrorContextRemoteFile)
  191. return
  192. }
  193. defer file.Close()
  194. // New file, size is zero
  195. fileSize = 0
  196. } else {
  197. // Open file for append
  198. file, err = os.OpenFile(src, os.O_APPEND|os.O_WRONLY, 0666)
  199. if err != nil {
  200. bitsError(w, uuid, http.StatusInternalServerError, 0, ErrorContextRemoteFile)
  201. return
  202. }
  203. defer file.Close()
  204. // Get size on disk
  205. var info os.FileInfo
  206. info, err = file.Stat()
  207. if err != nil {
  208. bitsError(w, uuid, http.StatusInternalServerError, 0, ErrorContextRemoteFile)
  209. return
  210. }
  211. fileSize = uint64(info.Size())
  212. }
  213. // Sanity checks
  214. if rangeEnd < fileSize {
  215. // The range is already written to disk
  216. w.Header().Add("BITS-Recieved-Content-Range", strconv.FormatUint(fileSize, 10))
  217. bitsError(w, uuid, http.StatusRequestedRangeNotSatisfiable, 0, ErrorContextRemoteFile)
  218. return
  219. } else if rangeStart > fileSize {
  220. // start must be <= fileSize, else there will be a gap
  221. w.Header().Add("BITS-Recieved-Content-Range", strconv.FormatUint(fileSize, 10))
  222. bitsError(w, uuid, http.StatusRequestedRangeNotSatisfiable, 0, ErrorContextRemoteFile)
  223. return
  224. }
  225. // Calculate the offset in the slice, if overlapping
  226. var dataOffset = fileSize - rangeStart
  227. // Write the data to file
  228. var written uint64
  229. var wr int
  230. wr, err = file.Write(data[dataOffset:])
  231. if err != nil {
  232. bitsError(w, uuid, http.StatusInternalServerError, 0, ErrorContextRemoteFile)
  233. return
  234. }
  235. written = uint64(wr)
  236. // Make sure we wrote everything we wanted
  237. if written != fragmentSize-dataOffset {
  238. bitsError(w, uuid, http.StatusInternalServerError, 0, ErrorContextRemoteFile)
  239. return
  240. }
  241. // Check if we have written everything
  242. if rangeEnd+1 == fileLength {
  243. // File is done! Manually close it, since the callback probably don't wnat the file to be open
  244. file.Close()
  245. // Call the callback
  246. if b.callback != nil {
  247. b.callback(EventRecieveFile, uuid, src)
  248. }
  249. }
  250. // https://msdn.microsoft.com/en-us/library/aa362773(v=vs.85).aspx
  251. w.Header().Add("BITS-Packet-Type", "Ack")
  252. w.Header().Add("BITS-Session-Id", uuid)
  253. w.Header().Add("BITS-Received-Content-Range", strconv.FormatUint(fileSize+uint64(written), 10))
  254. w.Write(nil)
  255. }
  256. // Use the Cancel-Session packet to terminate the upload session with the BITS server.
  257. // https://msdn.microsoft.com/en-us/library/aa362829(v=vs.85).aspx
  258. func (b *Handler) bitsCancel(w http.ResponseWriter, r *http.Request, uuid string) {
  259. // Check for correct session
  260. if uuid == "" {
  261. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  262. return
  263. }
  264. destDir := path.Join(b.cfg.TempDir, uuid)
  265. exist, err := exists(destDir)
  266. if err != nil {
  267. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  268. return
  269. }
  270. if !exist {
  271. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  272. return
  273. }
  274. // do the callback
  275. if b.callback != nil {
  276. b.callback(EventCancelSession, uuid, destDir)
  277. }
  278. w.Header().Add("BITS-Packet-Type", "Ack")
  279. w.Header().Add("BITS-Session-Id", uuid)
  280. w.Write(nil)
  281. }
  282. // Use the Close-Session packet to tell the BITS server that file upload is complete and to end the session.
  283. // https://msdn.microsoft.com/en-us/library/aa362830(v=vs.85).aspx
  284. func (b *Handler) bitsClose(w http.ResponseWriter, r *http.Request, uuid string) {
  285. // Check for correct session
  286. if uuid == "" {
  287. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  288. return
  289. }
  290. destDir := path.Join(b.cfg.TempDir, uuid)
  291. exist, err := exists(destDir)
  292. if err != nil {
  293. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  294. return
  295. }
  296. if !exist {
  297. bitsError(w, uuid, http.StatusBadRequest, 0, ErrorContextRemoteFile)
  298. return
  299. }
  300. // do the callback
  301. if b.callback != nil {
  302. b.callback(EventCloseSession, uuid, destDir)
  303. }
  304. // https://msdn.microsoft.com/en-us/library/aa362712(v=vs.85).aspx
  305. w.Header().Add("BITS-Packet-Type", "Ack")
  306. w.Header().Add("BITS-Session-Id", uuid)
  307. w.Write(nil)
  308. }