mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-13 20:12:57 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92b3f4ff76 | ||
|
|
1cdda3dda8 | ||
|
|
a8a03d3f87 | ||
|
|
5eebc9a5de |
No files matched your search
+1
-1
@@ -24,7 +24,7 @@ USER node
|
||||
RUN npm cache clean --force \
|
||||
&& npm install
|
||||
|
||||
EXPOSE 4000
|
||||
EXPOSE 4100
|
||||
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -52,9 +52,38 @@ docker compose up
|
||||
|
||||
<br/>
|
||||
|
||||
See [Configuration](#configuration) for next steps.
|
||||
|
||||
<br/>
|
||||
|
||||
## ⚠️ Self-Hosting ⚠️
|
||||
The self-hosted version of Puter is currently in alpha stage and should not be used in production yet. It is under active development and may contain bugs, other issues. Please exercise caution and use it for testing and evaluation purposes only.
|
||||
|
||||
## Configuration
|
||||
|
||||
Running the server will generate a configuration file at `volatile/config/config.json`.
|
||||
|
||||
### Domain Name
|
||||
|
||||
To access Puter on your device, you can simply go to the address printed in
|
||||
the server console (usually `puter.localhost:4100`).
|
||||
|
||||
To access Puter from another device, a domain name must be configured, as well as
|
||||
an `api` subdomain. For example, `example.local` might be the domain name pointing
|
||||
to the IP address of the server running puter, and `api.example.com` must point to
|
||||
this address as well. This domain must be specified in the configuration file
|
||||
(usually `volatile/config/config.json`) as well.
|
||||
|
||||
See [domain configuration](./doc/self-hosters/domains.md) for more information.
|
||||
|
||||
### Default User
|
||||
|
||||
By default, Puter will create a user called `default_user` with the password
|
||||
`changeme`. A warning will persist in the dev console until this user's
|
||||
password is changed. Please login to this user and change the password as
|
||||
your first step. This user by default has 10GB storage instead of the default
|
||||
(500MB storage) for new/temporary users.
|
||||
|
||||
<br/>
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Configurating Domains for Self-Hosted Puter
|
||||
|
||||
## Local Network Configuration
|
||||
|
||||
### Prerequisite Conditions
|
||||
|
||||
Ensure the hosting device has a static IP address to prevent potential connectivity issues due to IP changes. This setup will enable seamless access to Puter and its services across your local network.
|
||||
|
||||
### Using Hosts Files
|
||||
|
||||
The hosts file is a straightforward way to map domain names to IP addresses on individual devices. It's simple to set up but requires manual changes on each device that needs access to the domains.
|
||||
|
||||
#### Windows
|
||||
1. Open Notepad as an administrator.
|
||||
2. Open the file located at `C:\Windows\System32\drivers\etc\hosts`.
|
||||
3. Add lines for your domain and subdomain with the server's IP address, in the
|
||||
following format:
|
||||
```
|
||||
192.168.1.10 puter.local
|
||||
192.168.1.10 api.puter.local
|
||||
```
|
||||
|
||||
### For macOS and Linux:
|
||||
1. Open a terminal.
|
||||
2. Edit the hosts file with a text editor, e.g., `sudo nano /etc/hosts`.
|
||||
3. Add lines for your domain and subdomain with the server's IP address, in the
|
||||
following format:
|
||||
```
|
||||
192.168.1.10 puter.local
|
||||
192.168.1.10 api.puter.local
|
||||
```
|
||||
4. Save and exit the editor.
|
||||
|
||||
|
||||
### Using Router Configuration
|
||||
|
||||
Some routers allow you to add custom DNS rules, letting you configure domain names network-wide without touching each device.
|
||||
|
||||
1. Access your router’s admin interface (usually through a web browser).
|
||||
2. Look for DNS or DHCP settings.
|
||||
3. Add custom DNS mappings for `puter.local` and `api.puter.local` to the hosting device's IP address.
|
||||
4. Save the changes and reboot the router if necessary.
|
||||
|
||||
This method's availability and steps may vary depending on your router's model and firmware.
|
||||
|
||||
### Using Local DNS
|
||||
|
||||
Setting up a local DNS server on your network allows for flexible and scalable domain name resolution. This method works across all devices automatically once they're configured to use the DNS server.
|
||||
|
||||
#### Options for DNS Software:
|
||||
|
||||
- **Pi-hole**: Acts as both an ad-blocker and a DNS server. Ideal for easy setup and maintenance.
|
||||
- **BIND9**: Offers comprehensive DNS server capabilities for complex setups.
|
||||
- **Dnsmasq**: Lightweight and suitable for smaller networks or those new to running a DNS server.
|
||||
|
||||
**contributors note:** feel free to add any software you're aware of
|
||||
which might help with this to the list. Also, feel free to add instructions here for specific software; our goal is for Puter to be easy to setup with tools you're already familiar with.
|
||||
|
||||
#### General Steps:
|
||||
|
||||
1. Choose and install DNS server software on a device within your network.
|
||||
2. Configure the DNS server to resolve `puter.local` and `api.puter.local` to the IP address of your Puter hosting device.
|
||||
3. Update your router’s DHCP settings to distribute the DNS server's IP address to all devices on the network.
|
||||
|
||||
By setting up a local DNS server, you gain the most flexibility and control over your network's domain name resolution, ensuring that all devices can access Puter and its API without manual configuration.
|
||||
|
||||
## Production Configuration
|
||||
|
||||
Please note the self-hosting feature is still in alpha and a public production
|
||||
deployment is not recommended at this time. However, if you wish to host
|
||||
publically you can do so following the same steps you normally would to configure
|
||||
a domain name and ensuring the `api` subdomain points to the server as well.
|
||||
+1
-1
@@ -4,4 +4,4 @@ services:
|
||||
app:
|
||||
build: ./
|
||||
ports:
|
||||
- 4000:4000
|
||||
- 4100:4100
|
||||
@@ -95,11 +95,8 @@ class Kernel extends AdvancedBase {
|
||||
|
||||
root_context.arun(async () => {
|
||||
await this._install_modules();
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await this._boot_services();
|
||||
})();
|
||||
});
|
||||
|
||||
|
||||
// Error.stackTraceLimit = Infinity;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const { surrounding_box } = require("../fun/dev-console-ui-utils");
|
||||
const { get_user, generate_system_fsentries } = require("../helpers");
|
||||
const { get_user, generate_system_fsentries, invalidate_cached_user } = require("../helpers");
|
||||
const { Context } = require("../util/context");
|
||||
const { asyncSafeSetInterval } = require("../util/promise");
|
||||
const BaseService = require("./BaseService");
|
||||
const { Actor, UserActorType } = require("./auth/Actor");
|
||||
const { DB_WRITE } = require("./database/consts");
|
||||
|
||||
const DEFAULT_PASSWORD = 'changeme';
|
||||
@@ -16,13 +18,24 @@ class DefaultUserService extends BaseService {
|
||||
}
|
||||
async ['__on_ready.webserver'] () {
|
||||
// check if a user named `default-user` exists
|
||||
let user = await get_user({ username: USERNAME });
|
||||
let user = await get_user({ username: USERNAME, cached: false });
|
||||
if ( ! user ) user = await this.create_default_user_();
|
||||
|
||||
// check if user named `default-user` is using default password
|
||||
const require = this.require;
|
||||
const tmp_password = await this.get_tmp_password_(user);
|
||||
console.log(`second input [${tmp_password}]`);
|
||||
const bcrypt = require('bcrypt');
|
||||
const is_default_password = await bcrypt.compare(DEFAULT_PASSWORD, user.password);
|
||||
console.log(...[
|
||||
'THESE ARE THE ARGS',
|
||||
tmp_password,
|
||||
// password_hashed,
|
||||
user.password
|
||||
].map(l => l + '\n'));
|
||||
const is_default_password = await bcrypt.compare(
|
||||
tmp_password,
|
||||
user.password
|
||||
);
|
||||
if ( ! is_default_password ) return;
|
||||
|
||||
// show console widget
|
||||
@@ -30,23 +43,26 @@ class DefaultUserService extends BaseService {
|
||||
const lines = [
|
||||
`Your default user has been created!`,
|
||||
`\x1B[31;1musername:\x1B[0m ${USERNAME}`,
|
||||
`\x1B[32;1mpassword:\x1B[0m ${DEFAULT_PASSWORD}`,
|
||||
`\x1B[32;1mpassword:\x1B[0m ${tmp_password}`,
|
||||
`(change the password to remove this message)`
|
||||
];
|
||||
surrounding_box('31;1', lines);
|
||||
return lines;
|
||||
};
|
||||
this.start_poll_();
|
||||
this.start_poll_({ tmp_password, user });
|
||||
const svc_devConsole = this.services.get('dev-console');
|
||||
svc_devConsole.add_widget(this.default_user_widget);
|
||||
}
|
||||
start_poll_ () {
|
||||
start_poll_ ({ tmp_password, user }) {
|
||||
const interval = 1000 * 3; // 3 seconds
|
||||
const poll_interval = asyncSafeSetInterval(async () => {
|
||||
const user = await get_user({ username: USERNAME });
|
||||
const require = this.require;
|
||||
const bcrypt = require('bcrypt');
|
||||
const is_default_password = await bcrypt.compare(DEFAULT_PASSWORD, user.password);
|
||||
const is_default_password = await bcrypt.compare(
|
||||
tmp_password,
|
||||
user.password
|
||||
);
|
||||
if ( ! is_default_password ) {
|
||||
const svc_devConsole = this.services.get('dev-console');
|
||||
svc_devConsole.remove_widget(this.default_user_widget);
|
||||
@@ -56,25 +72,53 @@ class DefaultUserService extends BaseService {
|
||||
}, interval);
|
||||
}
|
||||
async create_default_user_ () {
|
||||
const require = this.require;
|
||||
const bcrypt = require('bcrypt');
|
||||
const db = this.services.get('database').get(DB_WRITE, 'default-user');
|
||||
await db.write(
|
||||
`
|
||||
INSERT INTO user (uuid, username, password, free_storage)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT INTO user (uuid, username, free_storage)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
[
|
||||
this.modules.uuidv4(),
|
||||
USERNAME,
|
||||
await bcrypt.hash(DEFAULT_PASSWORD, 8),
|
||||
1024 * 1024 * 1024 * 10, // 10 GB
|
||||
],
|
||||
);
|
||||
const user = await get_user({ username: USERNAME });
|
||||
const tmp_password = await this.get_tmp_password_(user);
|
||||
console.log(`first input [${tmp_password}]`);
|
||||
const bcrypt = require('bcrypt');
|
||||
const password_hashed = await bcrypt.hash(tmp_password, 8);
|
||||
await db.write(
|
||||
`UPDATE user SET password = ? WHERE id = ?`,
|
||||
[
|
||||
password_hashed,
|
||||
user.id,
|
||||
],
|
||||
);
|
||||
user.password = password_hashed;
|
||||
await generate_system_fsentries(user);
|
||||
invalidate_cached_user(user);
|
||||
await new Promise(rslv => setTimeout(rslv, 2000));
|
||||
return user;
|
||||
}
|
||||
async get_tmp_password_ (user) {
|
||||
const actor = await Actor.create(UserActorType, { user });
|
||||
return await Context.get().sub({ actor }).arun(async () => {
|
||||
const svc_driver = this.services.get('driver');
|
||||
const driver_response = await svc_driver.call(
|
||||
'puter-kvstore', 'get', { key: 'tmp_password' });
|
||||
|
||||
if ( driver_response.result ) return driver_response.result.value;
|
||||
|
||||
const tmp_password = require('crypto').randomBytes(4).toString('hex');
|
||||
await svc_driver.call(
|
||||
'puter-kvstore', 'set', {
|
||||
key: 'tmp_password',
|
||||
value: tmp_password });
|
||||
return tmp_password;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DefaultUserService;
|
||||
@@ -84,15 +84,28 @@ class DBKVStore extends BaseImplementation {
|
||||
const db = this.services.get('database').get(DB_WRITE, 'kvstore');
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
|
||||
await db.write(
|
||||
`INSERT INTO kv
|
||||
(user_id, app, kkey_hash, kkey, value)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
value = ?`,
|
||||
[ user.id, app?.uid ?? 'global', key_hash, key, value, value ]
|
||||
);
|
||||
try {
|
||||
await db.write(
|
||||
`INSERT INTO kv (user_id, app, kkey_hash, kkey, value)
|
||||
VALUES (?, ?, ?, ?, ?) ` +
|
||||
db.case({
|
||||
mysql: 'ON DUPLICATE KEY UPDATE value = ?',
|
||||
sqlite: ' ',
|
||||
// sqlite: 'ON CONFLICT(user_id, app, kkey_hash) DO UPDATE SET value = ?',
|
||||
}),
|
||||
[
|
||||
user.id, app?.uid ?? 'global', key_hash, key, value,
|
||||
...db.case({ mysql: [value], otherwise: [] }),
|
||||
]
|
||||
);
|
||||
} catch (e) {
|
||||
// if ( e.code !== 'SQLITE_ERROR' && e.code !== 'SQLITE_CONSTRAINT_PRIMARYKEY' ) throw e;
|
||||
// The "ON CONFLICT" clause isn't currently working.
|
||||
await db.write(
|
||||
`UPDATE kv SET value = ? WHERE user_id=? AND app=? AND kkey_hash=?`,
|
||||
[ value, user.id, app?.uid ?? 'global', key_hash ]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -34,18 +34,41 @@ class MonthlyUsageService extends BaseService {
|
||||
|
||||
const maybe_app_id = actor.type.app?.id;
|
||||
|
||||
if ( this.db.case({ sqlite: true, otherwise: false }) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// UPSERT increment count
|
||||
await this.db.write(
|
||||
'INSERT INTO `service_usage_monthly` (`year`, `month`, `key`, `count`, `user_id`, `app_id`, `extra`) ' +
|
||||
'VALUES (?, ?, ?, 1, ?, ?, ?) ' +
|
||||
'ON DUPLICATE KEY UPDATE `count` = `count` + 1',
|
||||
[
|
||||
year, month, key,
|
||||
actor.type.user?.id || null,
|
||||
maybe_app_id || null,
|
||||
JSON.stringify(extra)
|
||||
]
|
||||
);
|
||||
try {
|
||||
await this.db.write(
|
||||
'INSERT INTO `service_usage_monthly` (`year`, `month`, `key`, `count`, `user_id`, `app_id`, `extra`) ' +
|
||||
'VALUES (?, ?, ?, 1, ?, ?, ?) ' +
|
||||
this.db.case({
|
||||
mysql: 'ON DUPLICATE KEY UPDATE `count` = `count` + 1, `extra` = ?',
|
||||
sqlite: ' ',
|
||||
// sqlite: 'ON CONFLICT(`year`, `month`, `key`, `user_id`, `app_id`) ' +
|
||||
// 'DO UPDATE SET `count` = `count` + 1 AND `extra` = ?',
|
||||
}),
|
||||
[
|
||||
year, month, key, actor.type.user.id, maybe_app_id, JSON.stringify(extra),
|
||||
...this.db.case({ mysql: [JSON.stringify(extra)], otherwise: [] }),
|
||||
]
|
||||
);
|
||||
} catch (e) {
|
||||
// if ( e.code !== 'SQLITE_ERROR' && e.code !== 'SQLITE_CONSTRAINT_PRIMARYKEY' ) throw e;
|
||||
// The "ON CONFLICT" clause isn't currently working.
|
||||
await this.db.write(
|
||||
'UPDATE `service_usage_monthly` ' +
|
||||
'SET `count` = `count` + 1, `extra` = ? ' +
|
||||
'WHERE `year` = ? AND `month` = ? AND `key` = ? ' +
|
||||
'AND `user_id` = ? AND `app_id` = ?',
|
||||
[
|
||||
JSON.stringify(extra),
|
||||
year, month, key, actor.type.user.id, maybe_app_id,
|
||||
]
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async check (actor, specifiers) {
|
||||
|
||||
Reference in new issue
Block a user