Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Build site
run: yarn docs:build
2 changes: 1 addition & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default {

footer: {
message: 'This website is released under the MIT License.',
copyright: 'Copyright © 2022 Shadowsocks contributors'
copyright: 'Copyright © 2022-present Shadowsocks contributors'
},

algolia: {
Expand Down
261 changes: 261 additions & 0 deletions docs/.vitepress/theme/components/SIP002Generator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'

const methods = {
'AEAD-2022 (Recommended)': [
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
'2022-blake3-chacha20-poly1305',
],
'AEAD': [
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
],
'Stream (Deprecated)': [
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'camellia-128-cfb',
'camellia-192-cfb',
'camellia-256-cfb',
'chacha20-ietf',
'bf-cfb',
'rc4-md5',
],
}

const method = ref('2022-blake3-aes-256-gcm')
const password = ref('')
const hostname = ref('')
const port = ref(8388)
const plugin = ref('')
const tag = ref('')
const qrDataUrl = ref('')
const copied = ref(false)

let toDataURL: ((text: string) => Promise<string>) | null = null

function isAead2022(m: string): boolean {
return m.startsWith('2022-blake3-')
}

function base64urlEncode(str: string): string {
const encoded = btoa(str)
return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}

const uri = computed(() => {
if (!password.value || !hostname.value || !port.value) return ''

let userinfo: string
if (isAead2022(method.value)) {
userinfo = encodeURIComponent(method.value) + ':' + encodeURIComponent(password.value)
} else {
userinfo = base64urlEncode(method.value + ':' + password.value)
}
Comment on lines +48 to +61
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base64urlEncode uses btoa, which is not available during VitePress SSR/build, and uri is computed during SSR render. This can cause yarn docs:build to fail with btoa is not defined. Use an SSR-safe base64 implementation (e.g., globalThis.btoa fallback to Buffer.from(...).toString('base64')) or gate base64 encoding behind a client-only check.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +61
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btoa(str) only supports Latin-1; if users enter a non-ASCII password/tag, this will throw and prevent URI generation. Consider base64-encoding UTF-8 bytes (e.g., via TextEncoder) before applying base64url transformations.

Copilot uses AI. Check for mistakes.

let result = `ss://${userinfo}@${hostname.value}:${port.value}`

if (plugin.value) {
result += '/?plugin=' + encodeURIComponent(plugin.value)
}

if (tag.value) {
result += '#' + encodeURIComponent(tag.value)
}

return result
})

onMounted(async () => {
const qr = await import('qrcode')
toDataURL = qr.toDataURL

watch(uri, async (val) => {
if (val && toDataURL) {
qrDataUrl.value = await toDataURL(val)
} else {
Comment on lines +76 to +83
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const qr = await import('qrcode') is likely to return a module namespace where the QRCode API is under default (CJS interop). Using qr.toDataURL can be undefined, breaking QR generation. Prefer const mod = await import('qrcode'); toDataURL = (mod.default ?? mod).toDataURL (or equivalent) to support both ESM/CJS shapes.

Copilot uses AI. Check for mistakes.
qrDataUrl.value = ''
}
}, { immediate: true })
})

async function copyUri() {
if (!uri.value) return
await navigator.clipboard.writeText(uri.value)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
Comment on lines +91 to +93
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

navigator.clipboard.writeText can throw/reject (permissions, non-secure context, unsupported browsers). Right now an exception will bubble and leave copied stuck false with an unhandled rejection. Wrap this in try/catch and provide a fallback (or at least a user-visible failure state).

Suggested change
await navigator.clipboard.writeText(uri.value)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
// Prefer modern Clipboard API when available
if (typeof navigator !== 'undefined' &&
navigator.clipboard &&
typeof navigator.clipboard.writeText === 'function') {
try {
await navigator.clipboard.writeText(uri.value)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
return
} catch (e) {
console.error('Failed to copy using navigator.clipboard:', e)
// fall through to legacy fallback
}
}
// Fallback: use a temporary textarea and document.execCommand('copy')
try {
const textarea = document.createElement('textarea')
textarea.value = uri.value
// Avoid scrolling to bottom
textarea.style.position = 'fixed'
textarea.style.top = '0'
textarea.style.left = '0'
textarea.style.width = '1px'
textarea.style.height = '1px'
textarea.style.padding = '0'
textarea.style.border = 'none'
textarea.style.outline = 'none'
textarea.style.boxShadow = 'none'
textarea.style.background = 'transparent'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
const successful = document.execCommand && document.execCommand('copy')
document.body.removeChild(textarea)
if (successful) {
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
return
}
} catch (e) {
console.error('Failed to copy using fallback method:', e)
}
// If we reach here, copying failed; provide a visible failure state
copied.value = false
if (typeof window !== 'undefined') {
alert('Failed to copy URI to clipboard. Please copy it manually.')
}

Copilot uses AI. Check for mistakes.
}
</script>

<template>
<div class="sip002-generator">
<div class="form-grid">
<div class="field">
<label for="sip002-method">Method</label>
<select id="sip002-method" v-model="method">
<optgroup v-for="(group, label) in methods" :key="label" :label="label">
<option v-for="m in group" :key="m" :value="m">{{ m }}</option>
</optgroup>
</select>
</div>

<div class="field">
<label for="sip002-password">Password / Key</label>
<input id="sip002-password" v-model="password" type="text" placeholder="Enter password or PSK" />
</div>

<div class="field">
<label for="sip002-hostname">Hostname</label>
<input id="sip002-hostname" v-model="hostname" type="text" placeholder="e.g. 192.168.100.1" />
</div>

<div class="field">
<label for="sip002-port">Port</label>
<input id="sip002-port" v-model.number="port" type="number" min="1" max="65535" />
</div>

<div class="field">
<label for="sip002-plugin">Plugin <span class="optional">(optional)</span></label>
<input id="sip002-plugin" v-model="plugin" type="text" placeholder="e.g. obfs-local;obfs=http" />
</div>

<div class="field">
<label for="sip002-tag">Tag <span class="optional">(optional)</span></label>
<input id="sip002-tag" v-model="tag" type="text" placeholder="e.g. My Server" />
</div>
</div>

<div v-if="uri" class="output">
<label>Generated URI</label>
<div class="uri-row">
<code class="uri-text">{{ uri }}</code>
<button class="copy-btn" @click="copyUri">{{ copied ? 'Copied!' : 'Copy' }}</button>
</div>

<div v-if="qrDataUrl" class="qr-wrapper">
<img :src="qrDataUrl" alt="QR Code" class="qr-code" />
</div>
</div>
</div>
</template>

<style scoped>
.sip002-generator {
margin-top: 1rem;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
}

.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}

@media (max-width: 640px) {
.form-grid {
grid-template-columns: 1fr;
}
}

.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}

.field label {
font-size: 0.875rem;
font-weight: 600;
color: var(--vp-c-text-1);
}

.optional {
font-weight: 400;
color: var(--vp-c-text-3);
}

.field input,
.field select {
padding: 0.5rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 0.875rem;
outline: none;
transition: border-color 0.2s;
}

.field input:focus,
.field select:focus {
border-color: var(--vp-c-brand-1);
}

.output {
margin-top: 1.25rem;
padding-top: 1.25rem;
border-top: 1px solid var(--vp-c-divider);
}

.output > label {
font-size: 0.875rem;
font-weight: 600;
color: var(--vp-c-text-1);
display: block;
margin-bottom: 0.5rem;
}

.uri-row {
display: flex;
align-items: stretch;
gap: 0.5rem;
}

.uri-text {
flex: 1;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.8125rem;
word-break: break-all;
color: var(--vp-c-text-1);
}

.copy-btn {
padding: 0.5rem 1rem;
background: var(--vp-c-brand-1);
color: var(--vp-c-white);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
transition: opacity 0.2s;
}

.copy-btn:hover {
opacity: 0.85;
}

.qr-wrapper {
margin-top: 1rem;
text-align: center;
}

.qr-code {
border-radius: 4px;
max-width: 200px;
}
</style>
9 changes: 9 additions & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import DefaultTheme from 'vitepress/theme'
import SIP002Generator from './components/SIP002Generator.vue'

export default {
...DefaultTheme,
enhanceApp({ app }) {
app.component('SIP002Generator', SIP002Generator)
Comment on lines +6 to +7
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enhanceApp overrides the default theme’s enhanceApp hook, so any setup performed by DefaultTheme.enhanceApp will be skipped. Call DefaultTheme.enhanceApp?.(ctx) inside your enhanceApp implementation (and keep the same ctx signature) before registering the global component.

Suggested change
enhanceApp({ app }) {
app.component('SIP002Generator', SIP002Generator)
enhanceApp(ctx) {
DefaultTheme.enhanceApp?.(ctx)
ctx.app.component('SIP002Generator', SIP002Generator)

Copilot uses AI. Check for mistakes.
}
}
3 changes: 1 addition & 2 deletions docs/doc/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Optimize the shadowsocks server on Linux

First of all, upgrade your Linux kernel to 3.5 or later.
First of all, make sure your Linux kernel is reasonably up to date (4.9 or later recommended).

### Step 1, increase the maximum number of open file descriptors

Expand Down Expand Up @@ -51,7 +51,6 @@ net.core.somaxconn = 4096

net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_keepalive_time = 1200
net.ipv4.ip_local_port_range = 10000 65000
Expand Down
19 changes: 9 additions & 10 deletions docs/doc/configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ Explanation of each field:

### Encryption Method

The strongest option is an [AEAD cipher](/doc/aead.html). The recommended
choice is "chacha20-ietf-poly1305" or "aes-256-gcm". Other
[stream ciphers](/doc/stream.html) are implemented but do not provide
integrity and authenticity. Unless otherwise specified the encryption method
defaults to "table", which is **not secure**.
The strongest option is an [AEAD cipher](/doc/aead). The recommended
choice is "chacha20-ietf-poly1305" or "aes-256-gcm". For the latest
AEAD-2022 ciphers, see [SIP022](/doc/sip022). [Stream ciphers](/doc/stream)
are deprecated and do not provide integrity or authenticity.

## URI and QR code

Expand All @@ -48,21 +47,21 @@ ss://method:password@hostname:port

Note that the above URI doesn't follow RFC3986. It means the password here should be plain text, not percent-encoded.

For example, we have a server at `192.168.100.1:8888` using `bf-cfb` encryption method and password `test/!@#:`. Then, with the plain URI `ss://bf-cfb:test/!@#:@192.168.100.1:8888`, we can generate the BASE64 encoded URI:
For example, we have a server at `192.168.100.1:8888` using `chacha20-ietf-poly1305` encryption method and password `test/!@#:`. Then, with the plain URI `ss://chacha20-ietf-poly1305:test/!@#:@192.168.100.1:8888`, we can generate the BASE64 encoded URI:

```
> console.log( "ss://" + btoa("bf-cfb:test/!@#:@192.168.100.1:8888") )
ss://YmYtY2ZiOnRlc3QvIUAjOkAxOTIuMTY4LjEwMC4xOjg4ODg
> console.log( "ss://" + btoa("chacha20-ietf-poly1305:test/!@#:@192.168.100.1:8888") )
ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTp0ZXN0LyFAIzpAMTkyLjE2OC4xMDAuMTo4ODg4
```

To help organize and identify these URIs, you can append a tag after the BASE64 encoded string:

```
ss://YmYtY2ZiOnRlc3QvIUAjOkAxOTIuMTY4LjEwMC4xOjg4ODg#example-server
ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTp0ZXN0LyFAIzpAMTkyLjE2OC4xMDAuMTo4ODg4#example-server
```

This URI can also be encoded to QR code. Then, just scan it with your Android / iOS devices:

### SIP002

There is also a new URI scheme proposed in <a href="/doc/sip002.html">SIP002</a>. Any client or server which supports SIP003 plugin should use SIP002 URI scheme instead.
There is also a new URI scheme proposed in [SIP002](/doc/sip002). Any client or server which supports SIP003 plugin should use the SIP002 URI scheme instead.
Loading
Loading