index.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. /**
  2. * jsproxy cfworker api
  3. * https://github.com/EtherDream/jsproxy/
  4. */
  5. 'use strict'
  6. const JS_VER = 3
  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,purpose',
  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 proxy(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} retryTimes
  89. */
  90. async function proxy(urlObj, method, headers, acehOld, rawLen, retryTimes) {
  91. const res = await fetch(urlObj.href, {method, headers})
  92. const resHdrOld = res.headers
  93. const resHdrNew = new Headers(resHdrOld)
  94. let expose = '*'
  95. let vary = '--url'
  96. for (const [k, v] of resHdrOld.entries()) {
  97. if (k === 'access-control-allow-origin' ||
  98. k === 'access-control-expose-headers' ||
  99. k === 'location' ||
  100. k === 'set-cookie'
  101. ) {
  102. const x = '--' + k
  103. resHdrNew.set(x, v)
  104. if (acehOld) {
  105. expose = expose + ',' + x
  106. }
  107. resHdrNew.delete(k)
  108. }
  109. else if (k === 'vary') {
  110. vary = vary + ',' + v
  111. }
  112. else if (acehOld &&
  113. k !== 'cache-control' &&
  114. k !== 'content-language' &&
  115. k !== 'content-type' &&
  116. k !== 'expires' &&
  117. k !== 'last-modified' &&
  118. k !== 'pragma'
  119. ) {
  120. expose = expose + ',' + k
  121. }
  122. }
  123. if (acehOld) {
  124. expose = expose + ',--s'
  125. resHdrNew.set('--t', '1')
  126. }
  127. resHdrNew.set('access-control-expose-headers', expose)
  128. resHdrNew.set('access-control-allow-origin', '*')
  129. resHdrNew.set('vary', vary)
  130. resHdrNew.set('--s', res.status)
  131. // verify
  132. const newLen = resHdrOld.get('content-length') || ''
  133. const badLen = (rawLen !== newLen)
  134. let status = 200
  135. let body = res.body
  136. if (badLen) {
  137. if (retryTimes < 1) {
  138. urlObj = await parseYtVideoRedir(urlObj, newLen, res)
  139. if (urlObj) {
  140. return proxy(urlObj, method, headers, acehOld, rawLen, retryTimes + 1)
  141. }
  142. }
  143. status = 400
  144. body = `bad len (old: ${rawLen} new: ${newLen})`
  145. resHdrNew.set('cache-control', 'no-cache')
  146. }
  147. resHdrNew.set('--retry', retryTimes)
  148. resHdrNew.set('--ver', JS_VER)
  149. return new Response(body, {
  150. status,
  151. headers: resHdrNew,
  152. })
  153. }
  154. /**
  155. * @param {URL} urlObj
  156. */
  157. function isYtUrl(urlObj) {
  158. return (
  159. urlObj.host.endsWith('.googlevideo.com') &&
  160. urlObj.pathname.startsWith('/videoplayback')
  161. )
  162. }
  163. /**
  164. * @param {URL} urlObj
  165. * @param {number} newLen
  166. * @param {Response} res
  167. */
  168. async function parseYtVideoRedir(urlObj, newLen, res) {
  169. if (newLen > 2000) {
  170. return null
  171. }
  172. if (!isYtUrl(urlObj)) {
  173. return null
  174. }
  175. try {
  176. const data = await res.text()
  177. urlObj = new URL(data)
  178. } catch (err) {
  179. return null
  180. }
  181. if (!isYtUrl(urlObj)) {
  182. return null
  183. }
  184. return urlObj
  185. }