main.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. package main
  2. import (
  3. "bytes"
  4. "compress/gzip"
  5. "encoding/json"
  6. "flag"
  7. "fmt"
  8. "io"
  9. "log"
  10. "net/http"
  11. "net/http/httputil"
  12. "net/url"
  13. "os"
  14. "strconv"
  15. "strings"
  16. "time"
  17. )
  18. // PackageMeta - alias для удобства
  19. type PackageMeta map[string]interface{}
  20. var (
  21. targetURLStr = "https://registry.npmjs.org"
  22. cutoffDate time.Time
  23. )
  24. func main() {
  25. dateFlag := flag.String("date", "", "Cutoff date in YYYY-MM-DD format (e.g., 2023-12-01)")
  26. portFlag := flag.String("port", "8080", "Proxy server port")
  27. flag.Parse()
  28. if *dateFlag == "" {
  29. fmt.Println("Usage: nptm --date=2023-12-01")
  30. os.Exit(1)
  31. }
  32. var err error
  33. cutoffDate, err = time.Parse("2006-01-02", *dateFlag)
  34. if err != nil {
  35. log.Fatalf("Invalid date format: %v", err)
  36. }
  37. cutoffDate = cutoffDate.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
  38. log.Printf("Starting npm proxy on :%s", *portFlag)
  39. log.Printf("Time Machine Active! Packages after %s will be hidden.", cutoffDate.Format(time.RFC3339))
  40. targetURL, _ := url.Parse(targetURLStr)
  41. proxy := httputil.NewSingleHostReverseProxy(targetURL)
  42. originalDirector := proxy.Director
  43. proxy.Director = func(req *http.Request) {
  44. originalDirector(req)
  45. req.Host = targetURL.Host
  46. req.Header.Set("Accept-Encoding", "identity")
  47. req.Header.Set("Accept", "application/json")
  48. }
  49. proxy.ModifyResponse = func(resp *http.Response) error {
  50. if resp.StatusCode != http.StatusOK {
  51. return nil
  52. }
  53. contentType := resp.Header.Get("Content-Type")
  54. if !strings.Contains(contentType, "json") {
  55. return nil
  56. }
  57. var bodyReader io.ReadCloser = resp.Body
  58. var isGzipped bool
  59. if resp.Header.Get("Content-Encoding") == "gzip" {
  60. gzReader, err := gzip.NewReader(resp.Body)
  61. if err != nil {
  62. // Если не смогли распаковать, отдаем как есть
  63. // (скорее всего будет ошибка у клиента, но мы сделали что могли)
  64. resp.Body.Close()
  65. return nil
  66. }
  67. bodyReader = gzReader
  68. isGzipped = true
  69. }
  70. bodyBytes, err := io.ReadAll(bodyReader)
  71. bodyReader.Close()
  72. if err != nil {
  73. return err
  74. }
  75. // Вспомогательная функция для возврата тела
  76. // обратно в ответ с коррекцией заголовков
  77. setBody := func(data []byte) {
  78. resp.Body = io.NopCloser(bytes.NewReader(data))
  79. resp.ContentLength = int64(len(data))
  80. resp.Header.Set("Content-Length", strconv.Itoa(len(data)))
  81. if isGzipped {
  82. resp.Header.Del("Content-Encoding")
  83. }
  84. // Удаляем ETag, так как мы (возможно) трогали тело
  85. // (даже просто распаковка меняет байты)
  86. resp.Header.Del("ETag")
  87. }
  88. var meta PackageMeta
  89. if err := json.Unmarshal(bodyBytes, &meta); err != nil {
  90. // Если это не JSON, но мы его распаковали из Gzip,
  91. // нужно вернуть распакованные данные с правильным Content-Length
  92. setBody(bodyBytes)
  93. return nil
  94. }
  95. times, okT := meta["time"].(map[string]interface{})
  96. versions, okV := meta["versions"].(map[string]interface{})
  97. if !okT || !okV {
  98. // Аналогично, возвращаем распакованное тело с правильной длиной
  99. setBody(bodyBytes)
  100. return nil
  101. }
  102. modifiedMeta, err := filterPackageVersions(meta, times, versions)
  103. if err != nil {
  104. log.Printf("Filter error: %v", err)
  105. setBody(bodyBytes)
  106. return nil
  107. }
  108. finalVersions, _ := modifiedMeta["versions"].(map[string]interface{})
  109. if len(finalVersions) == 0 {
  110. resp.StatusCode = http.StatusNotFound
  111. notFoundBody := []byte(fmt.Sprintf(`{"error": "Not found. All versions published after %s"}`, cutoffDate.Format("2006-01-02")))
  112. // Сбрасываем флаг gzip, так как это наше новое тело
  113. isGzipped = false
  114. resp.Header.Del("Content-Encoding")
  115. resp.Body = io.NopCloser(bytes.NewReader(notFoundBody))
  116. resp.ContentLength = int64(len(notFoundBody))
  117. resp.Header.Set("Content-Length", strconv.Itoa(len(notFoundBody)))
  118. return nil
  119. }
  120. newBody, err := json.Marshal(modifiedMeta)
  121. if err != nil {
  122. return err
  123. }
  124. // Устанавливаем итоговое отфильтрованное тело
  125. setBody(newBody)
  126. resp.Header.Set("X-NPM-Time-Machine", "true")
  127. return nil
  128. }
  129. proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
  130. // Игнорируем ошибки "context canceled" (клиент отключился)
  131. if err != nil && !strings.Contains(err.Error(), "context canceled") {
  132. log.Printf("Proxy error for %s: %v", r.URL.Path, err)
  133. }
  134. w.WriteHeader(http.StatusBadGateway)
  135. }
  136. log.Fatal(http.ListenAndServe(":"+*portFlag, proxy))
  137. }
  138. func filterPackageVersions(meta PackageMeta, times map[string]interface{}, versions map[string]interface{}) (PackageMeta, error) {
  139. toDelete := []string{}
  140. var newLatestVer string
  141. var newLatestTime time.Time
  142. for ver, timeVal := range times {
  143. if ver == "created" || ver == "modified" {
  144. continue
  145. }
  146. timeStr, ok := timeVal.(string)
  147. if !ok {
  148. continue
  149. }
  150. pubDate, err := time.Parse(time.RFC3339, timeStr)
  151. if err != nil {
  152. continue
  153. }
  154. if pubDate.After(cutoffDate) {
  155. toDelete = append(toDelete, ver)
  156. } else {
  157. // Ищем самую свежую по дате среди оставшихся
  158. if pubDate.After(newLatestTime) {
  159. newLatestTime = pubDate
  160. newLatestVer = ver
  161. }
  162. }
  163. }
  164. for _, ver := range toDelete {
  165. delete(versions, ver)
  166. delete(times, ver)
  167. }
  168. if distTags, ok := meta["dist-tags"].(map[string]interface{}); ok {
  169. // Если нашли кандидата на latest
  170. if newLatestVer != "" {
  171. currentLatest, _ := distTags["latest"].(string)
  172. // Если текущий latest удален или (важно!) если по времени он в будущем
  173. // (но не удален по какой-то причине), заменяем его. Самая простая проверка
  174. // есть ли currentLatest в versions.
  175. if _, exists := versions[currentLatest]; !exists {
  176. distTags["latest"] = newLatestVer
  177. } else {
  178. // Дополнительная проверка: иногда latest не самый последний по времени,
  179. // но в рамках TimeMachine мы, скорее всего, хотим видеть последний
  180. // доступный на ту дату. Однако, безопаснее менять latest только если
  181. // старый удален.
  182. }
  183. }
  184. for tag, verInterface := range distTags {
  185. verStr, ok := verInterface.(string)
  186. if !ok {
  187. continue
  188. }
  189. if _, exists := versions[verStr]; !exists {
  190. delete(distTags, tag)
  191. }
  192. }
  193. }
  194. if !newLatestTime.IsZero() {
  195. times["modified"] = newLatestTime.Format(time.RFC3339)
  196. }
  197. return meta, nil
  198. }