Compare commits

..

18 Commits

Author SHA1 Message Date
yusing
e44ecc0ccc fix(access_log): fix slice out-of-bound panic on log rotation 2025-12-16 17:20:59 +08:00
yusing
6f9f995100 fix(config): nil panic introduced in ff934a4bb2911f5fa3c23d8fe6fea252d881fdc3; remove duplicated log 2025-12-16 15:04:21 +08:00
yusing
496aec6bb6 refactor: simplify and optimize deserialization 2025-12-16 14:48:33 +08:00
yusing
4afed02fc2 refactor(pool): simplify and optimize SizedPool; remove sync pool 2025-12-16 14:08:38 +08:00
yusing
f7eb4b132a refactor(config): remove unused ActiveConfig 2025-12-16 11:57:09 +08:00
yusing
ff934a4bb2 fix(config): fix default values not applied 2025-12-16 11:55:47 +08:00
yusing
db0cbc6577 refactor(config): remove unnecessary indirection 2025-12-16 11:22:17 +08:00
yusing
de3f92246f feat(rules): add protocol matching functionality
- Introduced a new checker for HTTP protocols (http, https, h3) in the routing rules.
- Added corresponding test cases to validate protocol matching behavior in requests.
2025-12-16 10:22:00 +08:00
yusing
c143593284 fix(icons): add handling for dark icons for walkxcode 2025-12-15 15:42:59 +08:00
yusing
31bf889d4a refactor(favicon): enhance FindIcon function to support icon variants
- Updated FindIcon to accept an additional variant parameter for improved icon fetching.
- Adjusted FavIcon and GetFavIconFromAlias functions to utilize the new variant handling logic.
2025-12-15 14:54:05 +08:00
yusing
baa7e72ad6 refactor(icon): improve handling in WithVariant 2025-12-15 14:42:31 +08:00
yusing
f43e07fe60 fix(favicon): enhance variant handling in GetFavIconFromAlias function
- Added fallback logic to handle cases where the requested icon variant is unavailable.
- If variant not provided, do not call WithVariant.
2025-12-15 14:37:33 +08:00
yusing
d319ee99ad fix(favicon): correct icon cache key in FindIcon method 2025-12-15 14:31:16 +08:00
yusing
ab58559afc refactor(icon): add variant handling for absolute/relative icons in WithVariant method 2025-12-15 14:30:31 +08:00
yusing
a6bdbb5603 chore: update api swagger 2025-12-15 12:28:14 +08:00
yusing
a0c589c546 feat(favicon): add variant support for favicons
- Introduced a new Variant field in GetFavIconRequest to specify icon variants (light/dark).
- Updated GetFavIconFromAlias function to handle the variant when fetching favicons.
- Added WithVariant method in IconURL to manage icon variants effectively.
2025-12-15 12:28:03 +08:00
yusing
76b8252755 fix(socket-proxy): update golang version. fix Dockerfile 2025-12-10 17:56:04 +08:00
yusing
d547872a41 fix(ci): correct socket-proxy github workflow 2025-12-10 17:39:39 +08:00
24 changed files with 228 additions and 94 deletions

View File

@@ -6,13 +6,12 @@ on:
- main
paths:
- "socket-proxy/**"
- "socket-proxy.Dockerfile"
- ".github/workflows/docker-image-socket-proxy.yml"
tags-ignore:
- '**'
- "**"
workflow_dispatch:
permissions:
contents: read
jobs:
build:
uses: ./.github/workflows/docker-image.yml

Submodule goutils updated: 79a4b13087...6c698b1d55

View File

@@ -2881,7 +2881,7 @@
"x-omitempty": false
},
"retries": {
"description": "<0: immediate, >=0: threshold",
"description": "<0: immediate, 0: default, >0: threshold",
"type": "integer",
"x-nullable": false,
"x-omitempty": false

View File

@@ -269,7 +269,7 @@ definitions:
path:
type: string
retries:
description: '<0: immediate, >=0: threshold'
description: '<0: immediate, 0: default, >0: threshold'
type: integer
timeout:
type: integer

View File

@@ -13,8 +13,9 @@ import (
)
type GetFavIconRequest struct {
URL string `form:"url" binding:"required_without=Alias"`
Alias string `form:"alias" binding:"required_without=URL"`
URL string `form:"url" binding:"required_without=Alias"`
Alias string `form:"alias" binding:"required_without=URL"`
Variant homepage.IconVariant `form:"variant" binding:"omitempty,oneof=light dark"`
} // @name GetFavIconRequest
// @x-id "favicon"
@@ -46,7 +47,11 @@ func FavIcon(c *gin.Context) {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
return
}
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), &iconURL)
icon := &iconURL
if request.Variant != homepage.IconVariantNone {
icon = icon.WithVariant(request.Variant)
}
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), icon)
if err != nil {
homepage.GinFetchError(c, fetchResult.StatusCode, err)
return
@@ -56,7 +61,7 @@ func FavIcon(c *gin.Context) {
}
// try with alias
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias)
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias, request.Variant)
if err != nil {
homepage.GinFetchError(c, result.StatusCode, err)
return
@@ -65,7 +70,7 @@ func FavIcon(c *gin.Context) {
}
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error) {
func GetFavIconFromAlias(ctx context.Context, alias string, variant homepage.IconVariant) (homepage.FetchResult, error) {
// try with route.Icon
r, ok := routes.HTTP.Get(alias)
if !ok {
@@ -79,13 +84,19 @@ func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResul
hp := r.HomepageItem()
if hp.Icon != nil {
if hp.Icon.IconSource == homepage.IconSourceRelative {
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL)
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
} else if variant != homepage.IconVariantNone {
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
if err != nil {
// fallback to no variant
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(homepage.IconVariantNone))
}
} else {
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
}
} else {
// try extract from "link[rel=icon]"
result, err = homepage.FindIcon(ctx, r, "/")
result, err = homepage.FindIcon(ctx, r, "/", variant)
}
if result.StatusCode == 0 {
result.StatusCode = http.StatusOK

View File

@@ -57,6 +57,8 @@ func Load() error {
panic(errors.New("config already loaded"))
}
state := NewState()
config.WorkingState.Store(state)
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
initErr := state.InitFromFile(common.ConfigPath)
@@ -82,9 +84,12 @@ func Reload() gperr.Error {
defer reloadMu.Unlock()
newState := NewState()
config.WorkingState.Store(newState)
err := newState.InitFromFile(common.ConfigPath)
if err != nil {
newState.Task().FinishAndWait(err)
config.WorkingState.Store(GetState())
logNotifyError("reload", err)
return gperr.New(ansi.Warning("using last config")).With(err)
}
@@ -98,7 +103,6 @@ func Reload() gperr.Error {
SetState(newState)
if err := newState.StartProviders(); err != nil {
gperr.LogWarn("start providers error", err)
logNotifyError("start providers", err)
return nil // continue
}
@@ -113,7 +117,7 @@ func WatchChanges() {
configEventFlushInterval,
OnConfigChange,
func(err gperr.Error) {
gperr.LogError("config reload error", err)
logNotifyError("config reload", err)
},
)
eventQueue.Start(cfgWatcher.Events(t.Context()))

View File

@@ -70,7 +70,6 @@ func SetState(state config.State) {
defer stateMu.Unlock()
cfg := state.Value()
config.ActiveConfig.Store(cfg)
config.ActiveState.Store(state)
acl.ActiveConfig.Store(cfg.ACL)
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
@@ -87,13 +86,13 @@ func HasState() bool {
}
func Value() *config.Config {
return config.ActiveConfig.Load()
return config.ActiveState.Load().Value()
}
func (state *state) InitFromFile(filename string) error {
data, err := os.ReadFile(common.ConfigPath)
if err != nil {
state.Config = *config.DefaultConfig()
state.Config = config.DefaultConfig()
return err
}
return state.Init(data)

View File

@@ -2,7 +2,6 @@ package config
import (
"regexp"
"sync/atomic"
"github.com/go-playground/validator/v10"
"github.com/yusing/godoxy/agent/pkg/agent"
@@ -42,20 +41,13 @@ type (
}
)
// nil-safe
var ActiveConfig atomic.Pointer[Config]
func init() {
ActiveConfig.Store(DefaultConfig())
}
func Validate(data []byte) gperr.Error {
var model Config
return serialization.UnmarshalValidateYAML(data, &model)
}
func DefaultConfig() *Config {
return &Config{
func DefaultConfig() Config {
return Config{
TimeoutShutdown: 3,
Homepage: homepage.Config{
UseDefaultCategories: true,
@@ -66,7 +58,6 @@ func DefaultConfig() *Config {
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
func init() {
serialization.RegisterDefaultValueFactory(DefaultConfig)
serialization.MustRegisterValidation("domain_name", func(fl validator.FieldLevel) bool {
domains := fl.Field().Interface().([]string)
for _, domain := range domains {

View File

@@ -33,7 +33,10 @@ type State interface {
FlushTmpLog()
}
// could be nil
// could be nil before first call on Load
var ActiveState synk.Value[State]
// working state while loading config, same as ActiveState after successful load
var WorkingState synk.Value[State]
var ErrConfigChanged = errors.New("config changed")

View File

@@ -145,23 +145,32 @@ func fetchIcon(ctx context.Context, filename string) (FetchResult, error) {
return FetchResultWithErrorf(http.StatusNotFound, "no icon found")
}
func FindIcon(ctx context.Context, r route, uri string) (FetchResult, error) {
type contextValue struct {
r httpRoute
uri string
}
func FindIcon(ctx context.Context, r route, uri string, variant IconVariant) (FetchResult, error) {
for _, ref := range r.References() {
result, err := fetchIcon(ctx, sanitizeName(ref))
ref = sanitizeName(ref)
if variant != IconVariantNone {
ref += "-" + string(variant)
}
result, err := fetchIcon(ctx, ref)
if err == nil {
return result, err
}
}
if r, ok := r.(httpRoute); ok {
// fallback to parse html
return findIconSlowCached(context.WithValue(ctx, "route", r), uri)
return findIconSlowCached(context.WithValue(ctx, "route", contextValue{r: r, uri: uri}), r.Key())
}
return FetchResultWithErrorf(http.StatusNotFound, "no icon found")
}
var findIconSlowCached = cache.NewKeyFunc(func(ctx context.Context, key string) (FetchResult, error) {
r := ctx.Value("route").(httpRoute)
return findIconSlow(ctx, r, key, nil)
v := ctx.Value("route").(contextValue)
return findIconSlow(ctx, v.r, v.uri, nil)
}).WithMaxEntries(200).Build() // no retries, no ttl
func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) (FetchResult, error) {

View File

@@ -23,7 +23,8 @@ type (
IsDark bool `json:"is_dark"`
}
IconSource string
IconSource string
IconVariant string
)
const (
@@ -33,6 +34,12 @@ const (
IconSourceSelfhSt IconSource = "@selfhst"
)
const (
IconVariantNone IconVariant = ""
IconVariantLight IconVariant = "light"
IconVariantDark IconVariant = "dark"
)
var ErrInvalidIconURL = gperr.New("invalid icon url")
func NewIconURL(source IconSource, refOrName, format string) *IconURL {
@@ -76,6 +83,32 @@ func (u *IconURL) HasIcon() bool {
return HasIcon(u)
}
func (u *IconURL) WithVariant(variant IconVariant) *IconURL {
switch u.IconSource {
case IconSourceWalkXCode, IconSourceSelfhSt:
default:
return u // no variant for absolute/relative icons
}
var extra *IconExtra
if u.Extra != nil {
extra = &IconExtra{
Key: u.Extra.Key,
Ref: u.Extra.Ref,
FileType: u.Extra.FileType,
IsLight: variant == IconVariantLight,
IsDark: variant == IconVariantDark,
}
extra.Ref = strings.TrimSuffix(extra.Ref, "-light")
extra.Ref = strings.TrimSuffix(extra.Ref, "-dark")
}
return &IconURL{
IconSource: u.IconSource,
FullURL: u.FullURL,
Extra: extra,
}
}
// Parse implements strutils.Parser.
func (u *IconURL) Parse(v string) error {
return u.parse(v, true)

View File

@@ -219,8 +219,7 @@ func HasIcon(icon *IconURL) bool {
if common.IsTest {
return true
}
key := NewIconKey(icon.IconSource, icon.Extra.Ref)
meta, ok := ListAvailableIcons()[key]
meta, ok := ListAvailableIcons()[icon.Extra.Key]
if !ok {
return false
}
@@ -332,6 +331,10 @@ func UpdateWalkxCodeIcons(m IconMap) error {
if isLight {
f = strings.TrimSuffix(f, "-light")
}
isDark := strings.HasSuffix(f, "-dark")
if isDark {
f = strings.TrimSuffix(f, "-dark")
}
key := NewIconKey(IconSourceWalkXCode, f)
icon, ok := m[key]
if !ok {
@@ -342,6 +345,9 @@ func UpdateWalkxCodeIcons(m IconMap) error {
if isLight {
icon.Light = true
}
if isDark {
icon.Dark = true
}
}
}
return nil

View File

@@ -10,16 +10,22 @@ const walkxcodeIcons = `{
"png": [
"app1.png",
"app1-light.png",
"app2.png"
"app2.png",
"karakeep.png",
"karakeep-dark.png"
],
"svg": [
"app1.svg",
"app1-light.svg"
"app1-light.svg",
"karakeep.svg",
"karakeep-dark.svg"
],
"webp": [
"app1.webp",
"app1-light.webp",
"app2.webp"
"app2.webp",
"karakeep.webp",
"karakeep-dark.webp"
]
}`
@@ -98,8 +104,8 @@ func TestListWalkxCodeIcons(t *testing.T) {
if err := UpdateWalkxCodeIcons(m); err != nil {
t.Fatal(err)
}
if len(m) != 2 {
t.Fatalf("expect 2 icons, got %d", len(m))
if len(m) != 3 {
t.Fatalf("expect 3 icons, got %d", len(m))
}
test := []testCases{
{
@@ -118,6 +124,15 @@ func TestListWalkxCodeIcons(t *testing.T) {
WebP: true,
},
},
{
Key: NewIconKey(IconSourceWalkXCode, "karakeep"),
IconMeta: IconMeta{
SVG: true,
PNG: true,
WebP: true,
Dark: true,
},
},
}
runTests(t, m, test)
}

View File

@@ -104,13 +104,13 @@ func (w *Watcher) getFavIcon(ctx context.Context) (result homepage.FetchResult,
hp := r.HomepageItem()
if hp.Icon != nil {
if hp.Icon.IconSource == homepage.IconSourceRelative {
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL)
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, homepage.IconVariantNone)
} else {
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
}
} else {
// try extract from "link[rel=icon]"
result, err = homepage.FindIcon(ctx, r, "/")
result, err = homepage.FindIcon(ctx, r, "/", homepage.IconVariantNone)
}
if result.StatusCode == 0 {
result.StatusCode = http.StatusOK

View File

@@ -94,6 +94,7 @@ const (
)
var bytesPool = synk.GetUnsizedBytesPool()
var sizedPool = synk.GetSizedBytesPool()
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (AccessLogger, error) {
writers, err := cfg.Writers()

View File

@@ -3,12 +3,11 @@ package accesslog
import (
"bytes"
"errors"
"fmt"
"io"
"slices"
"time"
"github.com/rs/zerolog"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/mockable"
strutils "github.com/yusing/goutils/strings"
)
@@ -164,31 +163,14 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention, result *Rotate
// Read each line and write it to the beginning of the file
writePos := int64(0)
buf := bytesPool.Get()
defer func() {
bytesPool.Put(buf)
}()
// in reverse order to keep the order of the lines (from old to new)
for i := len(linesToKeep) - 1; i >= 0; i-- {
line := linesToKeep[i]
n := line.Size
if cap(buf) < int(n) {
buf = slices.Grow(buf, int(n)-cap(buf))
}
buf = buf[:n]
// Read the line from its original position
if _, err := file.ReadAt(buf, line.Pos); err != nil {
if err := fileContentMove(file, line.Pos, writePos, int(n)); err != nil {
return false, err
}
// Write it to the new position
if _, err := file.WriteAt(buf, writePos); err != nil {
return false, err
} else if n < line.Size {
return false, gperr.Errorf("%w, writing %d bytes, only %d written", io.ErrShortWrite, line.Size, n)
}
writePos += n
}
@@ -199,6 +181,34 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention, result *Rotate
return true, nil
}
// fileContentMove moves the content of the file from the source position to the destination position.
//
// this is only used for moving from the back to the front of the file.
func fileContentMove(file supportRotate, srcPos, dstPos int64, size int) error {
buf := sizedPool.GetSized(size)
defer sizedPool.Put(buf)
// Read the line from its original position
nRead, err := file.ReadAt(buf, srcPos)
if err != nil {
return err
}
if nRead != size {
return fmt.Errorf("%w, reading %d bytes, only %d read", io.ErrShortBuffer, size, nRead)
}
// Write it to the new position
nWritten, err := file.WriteAt(buf, dstPos)
if err != nil {
return err
}
if nWritten != size {
return fmt.Errorf("%w, writing %d bytes, only %d written", io.ErrShortWrite, size, nWritten)
}
return nil
}
// rotateLogFileBySize rotates the log file by size.
// It returns the result of the rotation and an error if any.
//

View File

@@ -774,7 +774,7 @@ func (r *Route) Finalize() {
}
r.Port.Listening, r.Port.Proxy = lp, pp
r.HealthCheck.ApplyDefaults(config.ActiveConfig.Load().Defaults.HealthCheck)
r.HealthCheck.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
}
func (r *Route) FinalizeHomepageConfig() {

View File

@@ -31,6 +31,7 @@ const (
OnCookie = "cookie"
OnForm = "form"
OnPostForm = "postform"
OnProto = "proto"
OnMethod = "method"
OnHost = "host"
OnPath = "path"
@@ -226,6 +227,41 @@ var checkers = map[string]struct {
}
},
},
OnProto: {
help: Help{
command: OnProto,
args: map[string]string{
"proto": "the http protocol (http, https, h3)",
},
},
validate: func(args []string) (any, gperr.Error) {
if len(args) != 1 {
return nil, ErrExpectOneArg
}
proto := args[0]
if proto != "http" && proto != "https" && proto != "h3" {
return nil, ErrInvalidArguments.Withf("proto: %q", proto)
}
return proto, nil
},
builder: func(args any) CheckFunc {
proto := args.(string)
switch proto {
case "http":
return func(w http.ResponseWriter, r *http.Request) bool {
return r.TLS == nil
}
case "https":
return func(w http.ResponseWriter, r *http.Request) bool {
return r.TLS != nil
}
default: // h3
return func(w http.ResponseWriter, r *http.Request) bool {
return r.TLS != nil && r.ProtoMajor == 3
}
}
},
},
OnMethod: {
help: Help{
command: OnMethod,

View File

@@ -1,6 +1,7 @@
package rules_test
import (
"crypto/tls"
"encoding/base64"
"fmt"
"net/http"
@@ -65,6 +66,30 @@ func genCorrectnessTestCases(field string, genRequest func(k, v string) *http.Re
func TestOnCorrectness(t *testing.T) {
tests := []testCorrectness{
{
name: "proto_match_http",
checker: "proto http",
input: &http.Request{TLS: nil},
want: true,
},
{
name: "proto_match_https",
checker: "proto https",
input: &http.Request{TLS: &tls.ConnectionState{}},
want: true,
},
{
name: "proto_match_h3",
checker: "proto h3",
input: &http.Request{TLS: &tls.ConnectionState{}, ProtoMajor: 3},
want: true,
},
{
name: "proto_no_match_h3",
checker: "proto h3",
input: &http.Request{TLS: &tls.ConnectionState{}, ProtoMajor: 2},
want: false,
},
{
name: "method_match",
checker: "method GET",

View File

@@ -8,6 +8,7 @@ import (
"strconv"
"strings"
"time"
"unsafe"
"github.com/bytedance/sonic"
"github.com/go-playground/validator/v10"
@@ -175,12 +176,10 @@ var getTypeInfo func(t reflect.Type) typeInfo
func init() {
m := xsync.NewMap[reflect.Type, typeInfo](xsync.WithGrowOnly(), xsync.WithPresize(100))
getTypeInfo = func(t reflect.Type) typeInfo {
if v, ok := m.Load(t); ok {
return v
}
v := initTypeKeyFieldIndexesMap(t)
m.Store(t, v)
return v
ti, _ := m.LoadOrCompute(t, func() (typeInfo, bool) {
return initTypeKeyFieldIndexesMap(t), false
})
return ti
}
}
@@ -537,16 +536,8 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
return true, nil
}
// Check for multiline without allocating
isMultiline := false
for i := range srcLen {
if src[i] == '\n' {
isMultiline = true
break
}
}
// one liner is comma separated list
isMultiline := strings.ContainsRune(src, '\n')
if !isMultiline && src[0] != '-' {
values := strutils.CommaSeperatedList(src)
gi.ReflectInitSlice(dst, len(values), len(values))
@@ -557,21 +548,19 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
errs.Add(err.Subjectf("[%d]", i))
}
}
if errs.HasError() {
return true, errs.Error()
}
return true, nil
err := errs.Error()
return true, err
}
sl := []any{}
err := yaml.Unmarshal([]byte(src), &sl)
err := yaml.Unmarshal(unsafe.Slice(unsafe.StringData(src), len(src)), &sl)
if err != nil {
return true, gperr.Wrap(err)
}
tmp = sl
case reflect.Map, reflect.Struct:
rawMap := SerializedObject{}
err := yaml.Unmarshal([]byte(src), &rawMap)
err := yaml.Unmarshal(unsafe.Slice(unsafe.StringData(src), len(src)), &rawMap)
if err != nil {
return true, gperr.Wrap(err)
}

View File

@@ -36,6 +36,9 @@ func (hc *HealthCheckConfig) ApplyDefaults(defaults HealthCheckConfig) {
}
}
if hc.Retries == 0 {
hc.Retries = int64(HealthCheckDownNotifyDelayDefault / hc.Interval)
hc.Retries = defaults.Retries
if hc.Retries == 0 {
hc.Retries = max(1, int64(HealthCheckDownNotifyDelayDefault/hc.Interval))
}
}
}

View File

@@ -74,7 +74,7 @@ func NewMonitor(r types.Route) types.HealthMonCheck {
}
func newMonitor(u *url.URL, cfg types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
cfg.ApplyDefaults(config.DefaultConfig().Defaults.HealthCheck)
cfg.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
mon := &monitor{
config: cfg,
checkHealth: healthCheckFunc,

View File

@@ -1,5 +1,5 @@
# Stage 1: deps
FROM golang:1.24.3-alpine AS deps
FROM golang:1.25.5-alpine AS deps
HEALTHCHECK NONE
# package version does not matter
@@ -12,7 +12,8 @@ WORKDIR /src
COPY socket-proxy/go.mod socket-proxy/go.sum ./
RUN go mod download -x
RUN sed -i '/^module github\.com\/yusing\/goutils/!{/github\.com\/yusing\/goutils/d}' go.mod && \
go mod download -x
# Stage 2: builder
FROM deps AS builder
@@ -21,6 +22,7 @@ WORKDIR /src
COPY Makefile ./
COPY socket-proxy ./socket-proxy
COPY goutils ./goutils
ARG VERSION
ENV VERSION=${VERSION}

View File

@@ -2,8 +2,6 @@ module github.com/yusing/godoxy/socketproxy
go 1.25.5
exclude github.com/yusing/goutils v0.4.2
replace github.com/yusing/goutils => ../goutils
require (