├── .github └── ISSUE_TEMPLATE │ ├── bugreport.yml │ ├── config.yml │ └── featurerequest.yml ├── README.md ├── README_zh.md ├── worker.js └── worker_zh.js /.github/ISSUE_TEMPLATE/bugreport.yml: -------------------------------------------------------------------------------- 1 | name: Bug Repor 2 | description: create a bug report to help us inprove. 3 | labels: bug, waiting for review 4 | title: "[BUG REPORT] " 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | A clear and brief description of what the bug is. 10 | 11 | - type: textarea 12 | attributes: 13 | label: Description 14 | description: A short description of the bug. 15 | validations: 16 | required: true 17 | 18 | - type: textarea 19 | attributes: 20 | label: Steps to reproduce 21 | placeholder: | 22 | 1. [First Step] 23 | 2. [Second Step] 24 | 3. [and so on...] 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | attributes: 30 | label: Expected behavior 31 | description: What you expected to happen. 32 | validations: 33 | required: true 34 | 35 | - type: textarea 36 | attributes: 37 | label: Actual behavior 38 | description: What actually happened. 39 | validations: 40 | required: true 41 | 42 | - type: checkboxes 43 | attributes: 44 | label: Requisites 45 | description: | 46 | To rule out invalid issues, confirm and check each one of the checkboxes. 47 | options: 48 | - label: | 49 | This is not a support issue or a question. For any support, questions or help, visit our [Discussions](https://github.com/PencilNavigator/freenom-workers/discussions). 50 | required: true 51 | - label: | 52 | I performed a [cursory search of the issue tracker](https://github.com/PencilNavigator/Freenom-Workers/issues?q=is%3Aissue) to avoid opening a duplicate issue. 53 | required: true 54 | - label: | 55 | I checked the [documentation](https://freenom-workers.999857.xyz) to understand that the issue I am reporting is not normal behavior. 56 | required: true 57 | - label: | 58 | I understand that not filling out this template correctly will lead to the issue being closed and locked. 59 | required: true 60 | 61 | - type: textarea 62 | attributes: 63 | label: Additional content 64 | description: anything you want to add. 65 | validations: 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ideas 4 | url: https://github.com/PencilNavigator/freenom-workers/discussions/new?category=ideas 5 | about: Any ideas for this project. 6 | - name: Support 7 | url: https://github.com/PencilNavigator/freenom-workers/discussions/new?category=support 8 | about: nswers to questions and support is provided on the discussions page. 9 | - name: General Discussions 10 | url: https://github.com/PencilNavigator/freenom-workers/discussions/categories/general 11 | about: Discuss project related topics on the discussions page. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/featurerequest.yml: -------------------------------------------------------------------------------- 1 | name: "[FEATURE REQUEST] " 2 | description: create a feature request to help us inprove. 3 | labels: enhancement 4 | title: Feature request 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | A clear and concise description of your feature request. 10 | 11 | - type: textarea 12 | attributes: 13 | label: What is the reason you are requestiing for this feature? 14 | description: A short paragraph describing why you want to request this feature. 15 | placeholder: | 16 | A clear and concise description of what the problem is. E.g. When I open [blank] it hangs/freezes. 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | attributes: 22 | label: Describe the solution you would like. 23 | placeholder: | 24 | A clear and concise description of what you want to happen. 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | attributes: 30 | label: Describe alternatives you have considered. 31 | description: If you have found a temporary fix, list it here. 32 | validations: 33 | required: true 34 | 35 | - type: checkboxes 36 | attributes: 37 | label: requisites 38 | description: | 39 | To rule out invalid issues, confirm and check each one of the checkboxes. 40 | options: 41 | - label: | 42 | This is not a support issue or a question. For any support, questions or help, visit our [Discussions](https://github.com/PencilNavigator/freenom-workers/discussions). 43 | required: true 44 | - label: | 45 | I performed a [cursory search of the issue tracker](https://github.com/PencilNavigator/Freenom-Workers/issues?q=is%3Aissue) to avoid opening a duplicate issue. 46 | required: true 47 | - label: | 48 | I understand that not filling out this template will lead to the issue being closed and locked. 49 | required: true 50 | 51 | - type: textarea 52 | attributes: 53 | label: Additional context. 54 | description: Add any other context or screenshots about the feature request here. 55 | validations: 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚠ ANNOUNCEMENT 2 | 3 | #### On Feb 7th 2024, freenom has revoked all it's free ccTLD's management perms and started deleting NS & DNS records on all domains, This really is the end. 😢 Project is archived. 4 | 5 | #### 2024 年 2 月 7 日, Freenom 回收了所有免费域名的管理权限并开始批量删除所有域名的NS和DNS记录,Freenom免费域名不再可用。本项目封存。 6 | --- 7 | 8 |

Renew your Freenom domain (.cf .ga .gq .ml .tk) automaticly with Cloudflare Workers.

9 | 10 |

11 | 12 |

13 | 14 | ## Set-up 15 | 16 | Open your [Cloudflare Dashboard](https://dash.cloudflare.com) 17 | 18 | 19 | Select "Workers" in the left sidebar on the homepage. 20 | 21 | 22 | On the Workers tab,choose "Create a Service",choose your service name,and select a starter (HTTP Handler)。 23 | 24 | 25 | On the Workers you just created, select "Quick edit". 26 | 27 | 28 | In the Quick edit interface, copy and paste the code in [worker.js](https://cdn.jsdelivr.net/gh/PencilNavigator/freenom-workers@main/worker.js) and click Save. 29 | 30 | 31 | Go back to the Workers page you just created and select "Settings" and then "Variables". 32 | 33 | 34 | On the variables page, add the following variable name and value. 35 | 36 | - SECRET_USERNAME" with your Freenom username. 37 | - SECRET_PASSWORD" with your Freenom password. 38 | 39 | 40 | (Optional) Select the Encryption option for both variables to reduce the probability of leakage for your Freenom username and password. 41 | 42 | 43 | Return to the created Workers page and select Triggers. 44 | 45 | 46 | On the Trigger screen, click "Add Cron Trigger". On the Add Cron Trigger page, set up the trigger and save the Settings. The recommended execution time is once a day. 47 | 48 | 49 | On the same interface, Disable the default route (e.g. servicename.subdomain.worker.dev) in Routes. 50 | 51 | 52 | ## Test 53 | 54 | (Access through Quick edit) Access your deployed Workers service in the Quice edit interface. You should see the remaining dates of all domain names in your account. 55 | _Please note that access through preview does not trigger renewal. it should only be used for testing purposes._ 56 | 57 | (Trigger scheduled event) Enter "Quick Edit", select "Set Time", and then select "Trigger scheduled event". You should see the console outputing the remaining date of the domain. (If a renewable domain is detected, the console will output renewal results.) 58 | 59 | ## Showcase 60 | ![Image](https://user-images.githubusercontent.com/85282140/207813815-99af2574-910d-40d1-908c-5f18de1a5648.png) 61 | 62 | (Successfully renewed on 2022/12/15) 63 | 64 | ## Known Issues 65 | 66 | Please check out this [Wiki](https://github.com/PencilNavigator/freenom-workers/wiki/Known-Issues) page. 67 | 68 | ## Planned enhancement 69 | 70 | Please check out this [Wiki](https://github.com/PencilNavigator/freenom-workers/wiki/Planned-Enhancement) page. 71 | 72 | ## Simliar Projects 73 | https://github.com/luolongfei/freenom (PHP) 74 | 75 | https://github.com/Oreomeow/freenom-py (Python) 76 | 77 | ## LICENSE 78 | Currently no LICENSE. 79 | 80 | mona-loading 81 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | ## IMPORTANT NOTICE 重要通知 2 | Freenom recently updated their site and added a AWS Captcha, Causing ALL automatic renew sortware to not work. 3 | For now, please renew your domain manually. Get updates [HERE](https://github.com/PencilNavigator/freenom-workers/issues/9). 4 | 5 | Freenom 最近更新加上了亚马逊 CAPTCHA 用于各个页面的验证,目前基本上所有脚本均无法自动续期。 6 | 各位可以先暂时手动续期。获取该问题的更新信息,请点 [这里](https://github.com/PencilNavigator/freenom-workers/issues/9)。 7 | 8 |

通过Cloudflare Workers自动续期Freenom域名(.cf .ga .gq .ml .tk)。

9 | 10 |

11 | English README 12 | • 13 | Issues 14 | • 15 | Wiki 16 | • 17 | Discussions 18 |

19 |

20 | 喜欢这个项目?给颗Star吧! 21 |

22 | 23 | ## 部署 24 | 25 | 打开你的 [Cloudflare管理面板](https://dash.cloudflare.com) 26 | 27 | 28 | 在账号主页左侧侧边栏选择Workers 29 | 30 | 31 | 在Workers页面,选择创建服务,设置好服务名称,选择HTTP处理程序。 32 | 33 | 34 | 在刚刚创建的Workers界面,选择“快速编辑”。 35 | 36 | 37 | 在编辑界面,粘贴[worker_zh.js](https://cdn.jsdelivr.net/gh/PencilNavigator/freenom-workers@main/worker_zh.js)内代码,点击保存。([英文版](https://cdn.jsdelivr.net/gh/PencilNavigator/freenom-workers@main/worker.js)) 38 | 39 | 40 | 返回刚刚创建的Workers页面,选择“设置”,再选择“变量”。 41 | 42 | 43 | 在变量页面,添加以下变量和变量值: 44 | 45 | - SECRET_USERNAME变量,填入Freenom用户名 46 | 47 | - SECRET_PASSWORD变量,填入Freenom密码 48 | 49 | 50 | (可选)勾选两个变量的“加密”选项(可极大程度降低Freenom用户名和密码泄露的概率)。 51 | 52 | 53 | 返回创建的Workers页面,选择“触发器”。 54 | 55 | 56 | 在触发器界面,选择添加Cron触发器。在“添加Cron触发器”界面,设置触发器,保存。推荐执行时间为一天一次。 57 | 58 | 59 | 在同一界面的路由选项中禁用默认路由(通常为 服务名.子域名.workers.dev)。 60 | 61 | ## 测试 62 | 63 | (快速编辑-预览 访问)在快速编辑界面中的“预览”访问已部署的Workers。顺利的话,你将看到你账户内所有域名的剩余日期。 64 | _请注意,通过预览访问不会触发续期任务,仅用于测试是否可以获取账户内所有域名的剩余日期。_ 65 | 66 | (触发Cron)进入“快速编辑”,选择“设定时间”,再选择“触发计划的事件”。查看下方Console是否有输出域名剩余日期。(如有可续期的域名,会输出是否续期成功。) 67 | 68 | ## 效果展示 69 | ![图片](https://user-images.githubusercontent.com/85282140/207813815-99af2574-910d-40d1-908c-5f18de1a5648.png) 70 | 71 | (2022/12/15测试) 72 | 73 | ## 已知问题 74 | 75 | 请访问该[Wiki](https://github.com/PencilNavigator/freenom-workers/wiki/Known-Issues)页面。 76 | 77 | ## 待实现的功能 78 | 79 | 请访问该[Wiki](https://github.com/PencilNavigator/freenom-workers/wiki/Planned-Enhancement)页面。 80 | 81 | ## 类似项目 82 | https://github.com/luolongfei/freenom (PHP) 83 | 84 | https://github.com/Oreomeow/freenom-py (Python) 85 | 86 | 87 | ## LICENSE 88 | 目前没有决定好用什么LICENSE. 89 | 90 | mona-loading 91 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | // url 2 | const FREENOM = 'https://my.freenom.com' 3 | const CLIENT_AREA = `${FREENOM}/clientarea.php` 4 | const LOGIN_URL = `${FREENOM}/dologin.php` 5 | const DOMAIN_STATUS_URL = `${FREENOM}/domains.php?a=renewals` 6 | const RENEW_REFERER_URL = `${FREENOM}/domains.php?a=renewdomain` 7 | const RENEW_DOMAIN_URL = `${FREENOM}/domains.php?submitrenewals=true` 8 | 9 | // default headers 10 | const headers = { 11 | 'content-type': 'application/x-www-form-urlencoded', 12 | 'user-agent': 13 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/103.0.5060.134 Safari/537.36', 14 | } 15 | 16 | async function login() { 17 | headers['referer'] = CLIENT_AREA 18 | const resp = await fetch(LOGIN_URL, { 19 | method: 'POST', 20 | body: `username=${SECRET_USERNAME}&password=${SECRET_PASSWORD}`, 21 | headers: headers, 22 | redirect: 'manual', 23 | }) 24 | const setCookie = resp.headers 25 | .get('set-cookie') 26 | .replace(/([Hh]ttp[Oo]nly(,|)|([Pp]ath|expires|Max-Age)=.*?;)/g, '') 27 | .replace('WHMCSUser=deleted;', '') 28 | .replace(/\s+/g, '') 29 | .split(';') 30 | .reduce((pre, cur) => { 31 | const [k, v] = cur.split('=') 32 | if (k && v) pre[k] = v 33 | return pre 34 | }, {}) 35 | const cookie = [] 36 | for (const key in setCookie) { 37 | cookie.push(`${key}=${setCookie[key]}`) 38 | } 39 | return cookie.join(';') 40 | } 41 | 42 | async function getDomainInfo() { 43 | const res = { token: null, domains: {} } 44 | // request 45 | headers['referer'] = CLIENT_AREA 46 | const resp = await fetch(DOMAIN_STATUS_URL, { headers: headers }) 47 | const html = await resp.text() 48 | // login check 49 | if (/Logout<\/a>/i.test(html) == false) { 50 | console.error('get login status failed') 51 | return res 52 | } 53 | // get page token 54 | const tokenMatch = /name="token" value="(.*?)"/i.exec(html) 55 | if (tokenMatch.index == -1) { 56 | console.error('get page token failed') 57 | return res 58 | } 59 | res.token = tokenMatch[1] 60 | // get domains 61 | for (const item of html.match(/[^&]+&domain=.*?<\/tr>/g)) { 62 | const domain = /(.*?)<\/td>[^<]+<\/td>/i.exec(item)[1] 63 | const days = /]+>(\d+).Days<\/span>/i.exec(item)[1] 64 | const renewalId = /[^&]+&domain=(\d+?)"/i.exec(item)[1] 65 | res.domains[domain] = { days, renewalId } 66 | } 67 | return res 68 | } 69 | 70 | async function renewDomains(domainInfo) { 71 | const token = domainInfo.token 72 | for (const domain in domainInfo.domains) { 73 | const days = domainInfo.domains[domain].days 74 | if (parseInt(days) < 14) { 75 | const renewalId = domainInfo.domains[domain].renewalId 76 | headers['referer'] = `${RENEW_REFERER_URL}&domain=${renewalId}` 77 | const resp = await fetch(RENEW_DOMAIN_URL, { 78 | method: 'POST', 79 | body: `token=${token}&renewalid=${renewalId}&renewalperiod[${renewalId}]=12M&paymentmethod=credit`, 80 | headers: headers, 81 | }) 82 | const html = await resp.text() 83 | console.log( 84 | domain, 85 | /Order Confirmation/i.test(html) ? 'Renewal Success' : 'Renewal Failed' 86 | ) 87 | } else { 88 | console.log(`Domain ${domain} still has ${days} days until renewal.`) 89 | } 90 | } 91 | } 92 | 93 | async function handleSchedule(scheduledDate) { 94 | console.log('scheduled date', scheduledDate) 95 | const cookie = await login() 96 | console.log('cookie', cookie) 97 | headers['cookie'] = cookie 98 | const domainInfo = await getDomainInfo() 99 | console.log('token', domainInfo.token) 100 | console.log('domains', domainInfo.domains) 101 | await renewDomains(domainInfo) 102 | } 103 | 104 | addEventListener('scheduled', (event) => { 105 | event.waitUntil(handleSchedule(event.scheduledTime)) 106 | }) 107 | 108 | async function handleRequest() { 109 | const cookie = await login() 110 | console.log('cookie', cookie) 111 | headers['cookie'] = cookie 112 | const domainInfo = await getDomainInfo() 113 | const domains = domainInfo.domains 114 | const domainHtml = [] 115 | for (const domain in domains) { 116 | const days = domains[domain].days 117 | domainHtml.push(`

Domain ${domain} still has ${days} days until renewal.

`) 118 | } 119 | const html = ` 120 | 121 | 122 | Freenom Renew Workers 123 | 124 | Project Repository:https://github.com/PencilNavigator/freenom-workers
125 | ${domainHtml.join('')} 126 | 127 | 128 | ` 129 | return new Response(html, { 130 | headers: { 'content-type': 'text/html; charset=utf-8' }, 131 | }) 132 | } 133 | 134 | addEventListener('fetch', (event) => { 135 | event.respondWith(handleRequest()) 136 | }) 137 | -------------------------------------------------------------------------------- /worker_zh.js: -------------------------------------------------------------------------------- 1 | // url 2 | const FREENOM = 'https://my.freenom.com' 3 | const CLIENT_AREA = `${FREENOM}/clientarea.php` 4 | const LOGIN_URL = `${FREENOM}/dologin.php` 5 | const DOMAIN_STATUS_URL = `${FREENOM}/domains.php?a=renewals` 6 | const RENEW_REFERER_URL = `${FREENOM}/domains.php?a=renewdomain` 7 | const RENEW_DOMAIN_URL = `${FREENOM}/domains.php?submitrenewals=true` 8 | 9 | // default headers 10 | const headers = { 11 | 'content-type': 'application/x-www-form-urlencoded', 12 | 'user-agent': 13 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/103.0.5060.134 Safari/537.36', 14 | } 15 | 16 | async function login() { 17 | headers['referer'] = CLIENT_AREA 18 | const resp = await fetch(LOGIN_URL, { 19 | method: 'POST', 20 | body: `username=${SECRET_USERNAME}&password=${SECRET_PASSWORD}`, 21 | headers: headers, 22 | redirect: 'manual', 23 | }) 24 | const setCookie = resp.headers 25 | .get('set-cookie') 26 | .replace(/([Hh]ttp[Oo]nly(,|)|([Pp]ath|expires|Max-Age)=.*?;)/g, '') 27 | .replace('WHMCSUser=deleted;', '') 28 | .replace(/\s+/g, '') 29 | .split(';') 30 | .reduce((pre, cur) => { 31 | const [k, v] = cur.split('=') 32 | if (k && v) pre[k] = v 33 | return pre 34 | }, {}) 35 | const cookie = [] 36 | for (const key in setCookie) { 37 | cookie.push(`${key}=${setCookie[key]}`) 38 | } 39 | return cookie.join(';') 40 | } 41 | 42 | async function getDomainInfo() { 43 | const res = { token: null, domains: {} } 44 | // request 45 | headers['referer'] = CLIENT_AREA 46 | const resp = await fetch(DOMAIN_STATUS_URL, { headers: headers }) 47 | const html = await resp.text() 48 | // login check 49 | if (/
Logout<\/a>/i.test(html) == false) { 50 | console.error('get login status failed') 51 | return res 52 | } 53 | // get page token 54 | const tokenMatch = /name="token" value="(.*?)"/i.exec(html) 55 | if (tokenMatch.index == -1) { 56 | console.error('get page token failed') 57 | return res 58 | } 59 | res.token = tokenMatch[1] 60 | // get domains 61 | for (const item of html.match(/[^&]+&domain=.*?<\/tr>/g)) { 62 | const domain = /(.*?)<\/td>[^<]+<\/td>/i.exec(item)[1] 63 | const days = /]+>(\d+).Days<\/span>/i.exec(item)[1] 64 | const renewalId = /[^&]+&domain=(\d+?)"/i.exec(item)[1] 65 | res.domains[domain] = { days, renewalId } 66 | } 67 | return res 68 | } 69 | 70 | async function renewDomains(domainInfo) { 71 | const token = domainInfo.token 72 | for (const domain in domainInfo.domains) { 73 | const days = domainInfo.domains[domain].days 74 | if (parseInt(days) < 14) { 75 | const renewalId = domainInfo.domains[domain].renewalId 76 | headers['referer'] = `${RENEW_REFERER_URL}&domain=${renewalId}` 77 | const resp = await fetch(RENEW_DOMAIN_URL, { 78 | method: 'POST', 79 | body: `token=${token}&renewalid=${renewalId}&renewalperiod[${renewalId}]=12M&paymentmethod=credit`, 80 | headers: headers, 81 | }) 82 | const html = await resp.text() 83 | console.log( 84 | domain, 85 | /Order Confirmation/i.test(html) ? '续期成功' : '续期失败' 86 | ) 87 | } else { 88 | console.log(`域名 ${domain} 还有 ${days} 天续期`) 89 | } 90 | } 91 | } 92 | 93 | async function handleSchedule(scheduledDate) { 94 | console.log('scheduled date', scheduledDate) 95 | const cookie = await login() 96 | console.log('cookie', cookie) 97 | headers['cookie'] = cookie 98 | const domainInfo = await getDomainInfo() 99 | console.log('token', domainInfo.token) 100 | console.log('domains', domainInfo.domains) 101 | await renewDomains(domainInfo) 102 | } 103 | 104 | addEventListener('scheduled', (event) => { 105 | event.waitUntil(handleSchedule(event.scheduledTime)) 106 | }) 107 | 108 | async function handleRequest() { 109 | const cookie = await login() 110 | console.log('cookie', cookie) 111 | headers['cookie'] = cookie 112 | const domainInfo = await getDomainInfo() 113 | const domains = domainInfo.domains 114 | const domainHtml = [] 115 | for (const domain in domains) { 116 | const days = domains[domain].days 117 | domainHtml.push(`

域名 ${domain} 还有 ${days} 天到期

`) 118 | } 119 | const html = ` 120 | 121 | 122 | Freenom Renew Workers 123 | 124 | 本项目仓库地址:https://github.com/PencilNavigator/freenom-forkers
125 | ${domainHtml.join('')} 126 | 127 | 128 | ` 129 | return new Response(html, { 130 | headers: { 'content-type': 'text/html; charset=utf-8' }, 131 | }) 132 | } 133 | 134 | addEventListener('fetch', (event) => { 135 | event.respondWith(handleRequest()) 136 | }) 137 | --------------------------------------------------------------------------------