index.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /**
  2. * jsproxy cfworker api
  3. * https://github.com/EtherDream/jsproxy/
  4. */
  5. 'use strict'
  6. const JS_VER = 2
  7. const PREFLIGHT_INIT = {
  8. status: 204,
  9. headers: new Headers({
  10. 'access-control-allow-origin': '*',
  11. 'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
  12. 'access-control-allow-headers': '--raw-info,--level,--url,--referer,--cookie,--origin,--ext,--aceh,--ver,--type,--mode,accept,accept-charset,accept-encoding,accept-language,accept-datetime,authorization,cache-control,content-length,content-type,date,if-match,if-modified-since,if-none-match,if-range,if-unmodified-since,max-forwards,pragma,range,te,upgrade,upgrade-insecure-requests,x-requested-with,chrome-proxy',
  13. 'access-control-max-age': '1728000',
  14. }),
  15. }
  16. const pairs = Object.entries
  17. addEventListener('fetch', e => {
  18. const ret = handler(e.request)
  19. .catch(err => new Response(err))
  20. e.respondWith(ret)
  21. })
  22. /**
  23. * @param {Request} req
  24. */
  25. async function handler(req) {
  26. const reqHdrRaw = req.headers
  27. if (reqHdrRaw.has('x-jsproxy')) {
  28. return Response.error()
  29. }
  30. // preflight
  31. if (req.method === 'OPTIONS' &&
  32. reqHdrRaw.has('access-control-request-headers')
  33. ) {
  34. return new Response(null, PREFLIGHT_INIT)
  35. }
  36. let urlObj = null
  37. let extHdrs = null
  38. let acehOld = false
  39. let rawSvr = ''
  40. let rawLen = ''
  41. let rawEtag = ''
  42. const reqHdrNew = new Headers(reqHdrRaw)
  43. reqHdrNew.set('x-jsproxy', '1')
  44. for (const [k, v] of reqHdrRaw.entries()) {
  45. if (!k.startsWith('--')) {
  46. continue
  47. }
  48. reqHdrNew.delete(k)
  49. const k2 = k.substr(2)
  50. switch (k2) {
  51. case 'url':
  52. urlObj = new URL(v)
  53. break
  54. case 'aceh':
  55. acehOld = true
  56. break
  57. case 'raw-info':
  58. [rawSvr, rawLen, rawEtag] = v.split(/[,|]/)
  59. break
  60. case 'level':
  61. case 'mode':
  62. case 'type':
  63. break
  64. case 'ext':
  65. extHdrs = JSON.parse(v)
  66. break
  67. default:
  68. if (v) {
  69. reqHdrNew.set(k2, v)
  70. } else {
  71. reqHdrNew.delete(k2)
  72. }
  73. break
  74. }
  75. }
  76. if (extHdrs) {
  77. for (const [k, v] of pairs(extHdrs)) {
  78. reqHdrNew.set(k, v)
  79. }
  80. }
  81. return tryUrl(urlObj, req.method, reqHdrNew, acehOld, rawLen, 0)
  82. }
  83. /**
  84. *
  85. * @param {URL} urlObj
  86. * @param {string} method
  87. * @param {Headers} headers
  88. * @param {number} retryNum
  89. */
  90. async function tryUrl(urlObj, method, headers, acehOld, rawLen, retryNum) {
  91. // proxy
  92. const res = await fetch(urlObj.href, {method, headers})
  93. // header filter
  94. const resHdrOld = res.headers
  95. const resHdrNew = new Headers(resHdrOld)
  96. let expose = '*'
  97. let vary = '--url'
  98. for (const [k, v] of resHdrOld.entries()) {
  99. if (k === 'access-control-allow-origin' ||
  100. k === 'access-control-expose-headers' ||
  101. k === 'location' ||
  102. k === 'set-cookie'
  103. ) {
  104. const x = '--' + k
  105. resHdrNew.set(x, v)
  106. if (acehOld) {
  107. expose = expose + ',' + x
  108. }
  109. resHdrNew.delete(k)
  110. }
  111. else if (k === 'vary') {
  112. vary = vary + ',' + v
  113. }
  114. else if (acehOld &&
  115. k !== 'cache-control' &&
  116. k !== 'content-language' &&
  117. k !== 'content-type' &&
  118. k !== 'expires' &&
  119. k !== 'last-modified' &&
  120. k !== 'pragma'
  121. ) {
  122. expose = expose + ',' + k
  123. }
  124. }
  125. if (acehOld) {
  126. expose = expose + ',--s'
  127. resHdrNew.set('--t', '1')
  128. }
  129. resHdrNew.set('access-control-expose-headers', expose)
  130. resHdrNew.set('access-control-allow-origin', '*')
  131. resHdrNew.set('vary', vary)
  132. resHdrNew.set('--s', res.status)
  133. // verify
  134. const newLen = resHdrOld.get('content-length') || ''
  135. const badLen = (rawLen !== newLen)
  136. let status = 200
  137. let body = res.body
  138. if (badLen) {
  139. if (retryNum < 1) {
  140. urlObj = await parseYtVideoRedir(urlObj, newLen, res)
  141. if (urlObj) {
  142. return tryUrl(urlObj, method, headers, acehOld, rawLen, retryNum + 1)
  143. }
  144. }
  145. status = 400
  146. body = `bad len (old: ${rawLen} new: ${newLen})`
  147. resHdrNew.set('cache-control', 'no-cache')
  148. }
  149. resHdrNew.set('--retry', retryNum)
  150. resHdrNew.set('--ver', JS_VER)
  151. return new Response(body, {
  152. status,
  153. headers: resHdrNew,
  154. })
  155. }
  156. /**
  157. * @param {URL} urlObj
  158. */
  159. function isYtUrl(urlObj) {
  160. const m =
  161. urlObj.host.endsWith('.googlevideo.com') &&
  162. urlObj.pathname.startsWith('/videoplayback')
  163. return m
  164. }
  165. /**
  166. * @param {URL} urlObj
  167. * @param {number} newLen
  168. * @param {Response} res
  169. */
  170. async function parseYtVideoRedir(urlObj, newLen, res) {
  171. if (newLen > 2000) {
  172. return null
  173. }
  174. if (!isYtUrl(urlObj)) {
  175. return null
  176. }
  177. try {
  178. const data = await res.text()
  179. urlObj = new URL(data)
  180. } catch (err) {
  181. return null
  182. }
  183. if (!isYtUrl(urlObj)) {
  184. return null
  185. }
  186. return urlObj
  187. }