index.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. 'use strict'
  2. /**
  3. * static files (404.html, sw.js, conf.js)
  4. */
  5. const ASSET_URL = 'https://zjcqoo.github.io'
  6. const JS_VER = 8
  7. const MAX_RETRY = 1
  8. const PREFLIGHT_INIT = {
  9. status: 204,
  10. headers: new Headers({
  11. 'access-control-allow-origin': '*',
  12. 'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
  13. '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',
  14. 'access-control-max-age': '1728000',
  15. }),
  16. }
  17. /**
  18. * @param {string} message
  19. * @param {number} status
  20. * @param {any} headers
  21. */
  22. function makeRes(message, status = 200, headers = {}) {
  23. headers['cache-control'] = 'no-cache'
  24. return new Response(message, {status, headers})
  25. }
  26. addEventListener('fetch', e => {
  27. const req = e.request
  28. const urlStr = req.url
  29. const urlObj = new URL(urlStr)
  30. let ret
  31. switch (urlObj.pathname) {
  32. case '/http':
  33. ret = handler(req)
  34. break
  35. case '/works':
  36. ret = makeRes('it works')
  37. break
  38. default:
  39. // static files
  40. ret = fetch(ASSET_URL + urlObj.pathname)
  41. break
  42. }
  43. e.respondWith(ret)
  44. })
  45. /**
  46. * @param {Request} req
  47. */
  48. async function handler(req) {
  49. const reqHdrRaw = req.headers
  50. if (reqHdrRaw.has('x-jsproxy')) {
  51. return Response.error()
  52. }
  53. // preflight
  54. if (req.method === 'OPTIONS' &&
  55. reqHdrRaw.has('access-control-request-headers')
  56. ) {
  57. return new Response(null, PREFLIGHT_INIT)
  58. }
  59. let urlObj = null
  60. let extHdrs = null
  61. let acehOld = false
  62. let rawSvr = ''
  63. let rawLen = ''
  64. let rawEtag = ''
  65. const reqHdrNew = new Headers(reqHdrRaw)
  66. reqHdrNew.set('x-jsproxy', '1')
  67. for (const [k, v] of reqHdrRaw.entries()) {
  68. if (!k.startsWith('--')) {
  69. continue
  70. }
  71. reqHdrNew.delete(k)
  72. const k2 = k.substr(2)
  73. switch (k2) {
  74. case 'url':
  75. urlObj = new URL(v)
  76. break
  77. case 'aceh':
  78. acehOld = true
  79. break
  80. case 'raw-info':
  81. [rawSvr, rawLen, rawEtag] = v.split('|')
  82. break
  83. case 'level':
  84. case 'mode':
  85. case 'type':
  86. break
  87. case 'ext':
  88. extHdrs = JSON.parse(v)
  89. break
  90. default:
  91. if (v) {
  92. reqHdrNew.set(k2, v)
  93. } else {
  94. reqHdrNew.delete(k2)
  95. }
  96. break
  97. }
  98. }
  99. if (extHdrs) {
  100. for (const [k, v] of Object.entries(extHdrs)) {
  101. reqHdrNew.set(k, v)
  102. }
  103. }
  104. if (!urlObj) {
  105. return makeRes('missing url param', 403)
  106. }
  107. const reqInit = {
  108. method: req.method,
  109. headers: reqHdrNew,
  110. }
  111. return proxy(urlObj, reqInit, acehOld, rawLen, 0)
  112. }
  113. /**
  114. *
  115. * @param {URL} urlObj
  116. * @param {RequestInit} reqInit
  117. * @param {number} retryTimes
  118. */
  119. async function proxy(urlObj, reqInit, acehOld, rawLen, retryTimes) {
  120. const res = await fetch(urlObj.href, reqInit)
  121. const resHdrOld = res.headers
  122. const resHdrNew = new Headers(resHdrOld)
  123. let expose = '*'
  124. let vary = '--url'
  125. for (const [k, v] of resHdrOld.entries()) {
  126. if (k === 'access-control-allow-origin' ||
  127. k === 'access-control-expose-headers' ||
  128. k === 'location' ||
  129. k === 'set-cookie'
  130. ) {
  131. const x = '--' + k
  132. resHdrNew.set(x, v)
  133. if (acehOld) {
  134. expose = expose + ',' + x
  135. }
  136. resHdrNew.delete(k)
  137. }
  138. else if (k === 'vary') {
  139. vary = vary + ',' + v
  140. }
  141. else if (acehOld &&
  142. k !== 'cache-control' &&
  143. k !== 'content-language' &&
  144. k !== 'content-type' &&
  145. k !== 'expires' &&
  146. k !== 'last-modified' &&
  147. k !== 'pragma'
  148. ) {
  149. expose = expose + ',' + k
  150. }
  151. }
  152. if (acehOld) {
  153. expose = expose + ',--s'
  154. resHdrNew.set('--t', '1')
  155. }
  156. // verify
  157. if (rawLen) {
  158. const newLen = resHdrOld.get('content-length') || ''
  159. const badLen = (rawLen !== newLen)
  160. if (badLen) {
  161. if (retryTimes < MAX_RETRY) {
  162. urlObj = await parseYtVideoRedir(urlObj, newLen, res)
  163. if (urlObj) {
  164. return proxy(urlObj, reqInit, acehOld, rawLen, retryTimes + 1)
  165. }
  166. }
  167. return makeRes(`error`, 400, {
  168. '--error': 'bad len:' + newLen
  169. })
  170. }
  171. if (retryTimes > 1) {
  172. resHdrNew.set('--retry', retryTimes)
  173. }
  174. }
  175. let status = res.status
  176. resHdrNew.set('access-control-expose-headers', expose)
  177. resHdrNew.set('access-control-allow-origin', '*')
  178. resHdrNew.set('vary', vary)
  179. resHdrNew.set('--s', status)
  180. resHdrNew.set('--ver', JS_VER)
  181. resHdrNew.delete('content-security-policy')
  182. resHdrNew.delete('content-security-policy-report-only')
  183. if (status === 301 ||
  184. status === 302 ||
  185. status === 303 ||
  186. status === 307 ||
  187. status === 308
  188. ) {
  189. status = status + 10
  190. }
  191. return new Response(res.body, {
  192. status,
  193. headers: resHdrNew,
  194. })
  195. }
  196. /**
  197. * @param {URL} urlObj
  198. */
  199. function isYtUrl(urlObj) {
  200. return (
  201. urlObj.host.endsWith('.googlevideo.com') &&
  202. urlObj.pathname.startsWith('/videoplayback')
  203. )
  204. }
  205. /**
  206. * @param {URL} urlObj
  207. * @param {number} newLen
  208. * @param {Response} res
  209. */
  210. async function parseYtVideoRedir(urlObj, newLen, res) {
  211. if (newLen > 2000) {
  212. return null
  213. }
  214. if (!isYtUrl(urlObj)) {
  215. return null
  216. }
  217. try {
  218. const data = await res.text()
  219. urlObj = new URL(data)
  220. } catch (err) {
  221. return null
  222. }
  223. if (!isYtUrl(urlObj)) {
  224. return null
  225. }
  226. return urlObj
  227. }