Add Surge subscription support#179
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
PR Summary
|
Greptile SummaryThis PR adds end-to-end Surge subscription support: a new
Confidence Score: 3/5Safe to merge after the httpupgrade/WebSocket transport mismatch in the Surge generator is resolved; all other changes are additive and well-isolated. The generator emits src/modules/subscription-template/generators/surge.generator.service.ts — specifically the buildTrojanProxy method and its handling of the httpupgrade transport. Important Files Changed
Sequence DiagramsequenceDiagram
participant C as Surge Client
participant S as Subscription Endpoint
participant RM as ResponseRulesMatcherService
participant RT as RenderTemplatesService
participant SG as SurgeGeneratorService
participant TS as SubscriptionTemplateService
participant DB as Database/Cache
C->>S: GET /sub/:uuid (User-Agent: Surge/5.x)
S->>RM: matchRules(headers, rules)
RM-->>S: "{ matched: true, responseType: 'SURGE' }"
S->>RT: generateSubscription(SURGE, hosts)
RT->>SG: generateConfig(formattedHosts, templateName)
SG->>TS: getCachedTemplateByType('SURGE')
TS->>DB: check cache
alt cache miss
DB-->>TS: null
TS->>DB: "query templateYaml where templateType=SURGE"
DB-->>TS: raw plain-text template
TS->>DB: set cache (raw string, 1hr)
end
DB-->>TS: template string
TS-->>SG: template (string)
SG->>SG: filter hosts (exclude vless, grpc/kcp/xhttp, reality)
SG->>SG: buildProxyLine per host (trojan/ss/hysteria2)
SG->>SG: renderTemplate (replace markers)
SG-->>RT: Surge config text
RT-->>S: "{ subscription, contentType: 'text/plain' }"
S-->>C: 200 text/plain Surge config
Reviews (1): Last reviewed commit: "checkpoint: add backend dockerignore" | Re-trigger Greptile |
| if (host.transport === 'ws' || host.transport === 'httpupgrade') { | ||
| params.ws = true; |
There was a problem hiding this comment.
HTTP Upgrade and WebSocket are distinct transport protocols. Surge's
ws = true enables standard WebSocket framing (RFC 6455), while xray-core's httpupgrade transport uses a plain HTTP/1.1 Upgrade handshake without WebSocket framing. Generating ws = true for an httpupgrade-backed server will cause Surge to handshake incorrectly and the connection will fail. The safest fix is to return null for httpupgrade transports (skipping the proxy) rather than emitting a broken config line.
| if (host.transport === 'ws' || host.transport === 'httpupgrade') { | |
| params.ws = true; | |
| if (host.transport === 'ws') { | |
| params.ws = true; |
| const isYamlTemplate = | ||
| template.templateType === 'MIHOMO' || | ||
| template.templateType === 'STASH' || | ||
| template.templateType === 'CLASH'; | ||
| template.templateType === 'CLASH' || | ||
| template.templateType === 'SURGE'; |
There was a problem hiding this comment.
Surge templates are plain text, not YAML, but grouping them under
isYamlTemplate means any update attempt with a JSON payload returns the error TEMPLATE_JSON_NOT_ALLOWED_FOR_YAML_TEMPLATE. While the validation logic itself is correct (SURGE templates live in templateYaml), this error message will confuse users who try to update a Surge template with JSON. Consider a more accurate variable name or a separate isPlainTextTemplate check.
| const isYamlTemplate = | |
| template.templateType === 'MIHOMO' || | |
| template.templateType === 'STASH' || | |
| template.templateType === 'CLASH'; | |
| template.templateType === 'CLASH' || | |
| template.templateType === 'SURGE'; | |
| const isYamlTemplate = | |
| template.templateType === 'MIHOMO' || | |
| template.templateType === 'STASH' || | |
| template.templateType === 'CLASH'; | |
| const isPlainTextTemplate = template.templateType === 'SURGE'; | |
| const isTextBasedTemplate = isYamlTemplate || isPlainTextTemplate; |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| { | ||
| headerName: 'user-agent', | ||
| operator: 'REGEX', | ||
| value: '^Surge', | ||
| caseSensitive: false, | ||
| }, |
There was a problem hiding this comment.
All other user-agent rules use lowercase patterns with
caseSensitive: false (e.g., ^clash, ^stash, ^sfa|sfi|…). The matchCondition implementation lowercases the regex pattern itself when caseSensitive: false, so ^Surge becomes ^surge at match time. This is functionally correct, but the uppercase S is inconsistent and could mislead anyone who reads the seed and assumes the pattern is matched as-written. Aligning to the lowercase convention makes the intent clearer.
| { | |
| headerName: 'user-agent', | |
| operator: 'REGEX', | |
| value: '^Surge', | |
| caseSensitive: false, | |
| }, | |
| { | |
| headerName: 'user-agent', | |
| operator: 'REGEX', | |
| value: '^surge', | |
| caseSensitive: false, | |
| }, |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Companion PRs
Validation
node -r ts-node/register -r tsconfig-paths/register test/surge-generator.test.tsnode -r ts-node/register -r tsconfig-paths/register test/surge-support.test.tsnpm run build[General],[Proxy],[Proxy Group],[Rule]and 6 proxy entries for a Surge user agent.