index.js 5.7 KB

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