Citiverse
  • It is not possible to upload zip files to the forum even though it is enabled in the settings.

    Bug Reports
    2 20 2

    amaarets@community.nodebb.orgA
    11
    0

    I enabled the upload of files with the ZIP extension on my forum, and despite this in the Chrome browser on Windows (and other browsers) it writes Invalid MIME type, the content-type header in Chrome is application/x-zip-compressed and not application/zip.
    Since the MIME library returns ZIP to the following code

    node -e "const mime = require('mime').default; console.log(mime.getExtension('application/x-zip-compressed'));"
    

    This fails the upload

  • julian@community.nodebb.orgJ
    37
    0

    @AMAARETS hi, I'm not seeing this on the latest develop. Can you try there?

  • amaarets@community.nodebb.orgA
    11
    0

    > @julian said:
    >
    > @AMAARETS hi, I'm not seeing this on the latest develop. Can you try there?

    I didn't mean that this code snippet exists in the core, it's just a short code sample to see what the MIME package returns on a ZIP extension.

    The relevant code is here I think

    Edit: I just saw that a bug report has already been added to GitHub regarding this matter.
    https://github.com/NodeBB/NodeBB/issues/14359#issue-4702822194

  • julian@community.nodebb.orgJ
    37
    0

    @AMAARETS what I mean is, I think commits in the latest develop fixed this. Can you try again?

  • amaarets@community.nodebb.orgA
    11
    0

    > @julian said:
    >
    > @AMAARETS what I mean is, I think commits in the latest develop fixed this. Can you try again?

    Oh, thanks, Google Translate completely messed up your comment...

    I updated my forum to the develop branch and built and relaunched, and the error is still there

  • julian@community.nodebb.orgJ
    37
    0

    @AMAARETS any chance you can let me know which file it is?

    I tried uploading a ZIP file and it worked okay.

  • amaarets@community.nodebb.orgA
    11
    0

    > @julian said:
    >
    > @AMAARETS any chance you can let me know which file it is?
    >
    > I tried uploading a ZIP file and it worked okay.

    What browser did you use?
    How does it define the file type of a ZIP file?

    I don't have permission to upload files here, so I can't send you the file I tried, but it's not just a specific file, but any file with a ZIP and RAR extension in certain browsers (there may be other extensions that cause problems).

  • julian@community.nodebb.orgJ
    37
    0

    @AMAARETS I don't think it's browser specific since the checks live on the backend.

    I'll try with a RAR file.

  • amaarets@community.nodebb.orgA
    11
    0

    > @julian said:
    >
    > @AMAARETS I don't think it's browser specific since the checks live on the backend.
    >
    > I'll try with a RAR file.

    Since I didn't trust myself to identify the problem with the function for sure, I preferred to actually check it by printing the variable data to the terminal, I changed the function to this:

    async function validateUploadedFileMime(uploadedFile) {
    	console.log('===== MIME Validation Start =====');
    	console.log('1. uploadedFile object metadata:');
    	console.log('   - Name:', uploadedFile.name);
    	console.log('   - Path:', uploadedFile.path);
    	console.log('   - Type (from request/browser):', uploadedFile.type);
    	console.log('   - Size:', uploadedFile.size);
    
    	const detected = await fileTypeFromFile(uploadedFile.path);
    	console.log('2. file-type library detection:', detected || 'null (could not detect by content)');
    
    	const mimeFromExt = mime.getType(uploadedFile.name);
    	console.log('3. mime library detection (by extension):', mimeFromExt || 'null (could not detect by extension)');
    
    	const detectedMimeType = detected ? detected.mime : mimeFromExt;
    	console.log('4. Final detectedMimeType to compare:', detectedMimeType);
    
    	const hasDetected = !!detectedMimeType;
    	const hasUploadedType = !!uploadedFile.type;
    	const isMismatch = detectedMimeType !== uploadedFile.type;
    
    	console.log('5. Conditions check:');
    	console.log('   - Has detected MIME?:', hasDetected);
    	console.log('   - Has uploaded type from request?:', hasUploadedType);
    	console.log('   - Is there a mismatch?:', isMismatch);
    
    	if (detectedMimeType && uploadedFile.type && detectedMimeType !== uploadedFile.type) {
    		console.error('❌ VALIDATION FAILED: MIME mismatch!');
    		console.error(`   Expected (from file content/ext): "${detectedMimeType}"`);
    		console.error(`   Received (from request/browser): "${uploadedFile.type}"`);
    		console.log('==================================');
    		throw new Error('[[error:invalid-mime-type]]');
    	}
    
    	console.log('✅ VALIDATION PASSED');
    	console.log('==================================');
    }
    

    And this is the output

    ===== MIME Validation Start =====
    1. uploadedFile object metadata:
       - Name: בדיקה-מחשב-מק.zip
       - Path: /tmp/91c31e55fb2334122e3c21aace4755f3
       - Type (from request/browser): application/x-zip-compressed
       - Size: 204
    
    2. file-type library detection: 
       { ext: 'zip', mime: 'application/zip' }
    
    3. mime library detection (by extension): 
       application/zip
    
    4. Final detectedMimeType to compare: 
       application/zip
    
    5. Conditions check:
       - Has detected MIME?: true
       - Has uploaded type from request?: true
       - Is there a mismatch?: true
    
    ❌ VALIDATION FAILED: MIME mismatch!
       Expected (from file content/ext): "application/zip"
       Received (from request/browser): "application/x-zip-compressed"
    ==================================
    

    As you can clearly see there is a mismatch of expectations between the browser and the server

  • amaarets@community.nodebb.orgA
    11
    0

    It should be noted that although checking the type that matches the zip extension by the MIME package returns application/zip and not application/x-zip-compressed, checking the extension that matches the application/x-zip-compressed type does return zip, so it may be possible to perform the validation in a way that is compatible with all browsers.

  • julian@community.nodebb.orgJ
    37
    0

    @AMAARETS seems a windows issue. I'll have to special case it.

    RAR files don't work either?

  • amaarets@community.nodebb.orgA
    11
    0

    > @julian said:
    >
    > @AMAARETS seems a windows issue. I'll have to special case it.
    >
    > RAR files don't work either?

    Yes, I think

    I think what was reported here is also the same problem, where he mentioned a number of additional extensions

  • julian@community.nodebb.orgJ
    37
    0

    @AMAARETS can you try now with the latest develop branch commit?

    A mime alias mapping was added to handle the different formats that Windows sends in. I added another entry for rar as well. There is additional handling for file formats that the browser doesn't understand (and thus reports application/octet-stream for.

  • amaarets@community.nodebb.orgA
    11
    0

    > @julian כתב:
    >
    > @AMAARETS can you try now with the latest develop branch commit?
    >
    > A mime alias mapping was added to handle the different formats that Windows sends in. I added another entry for rar as well. There is additional handling for file formats that the browser doesn't understand (and thus reports application/octet-stream for.

    Now it works on the ZIP file I tested before, thanks

    Just a quick question - I'm wondering what information I'm missing here
    Why maintain a list of aliases, and not do the comparison by checking in reverse whether mime.getExtension(application/x-zip-compressed) returns a valid extension like ZIP, so it will work even though a check of mime.getType(file.zip) returns application/zip and not application/x-zip-compressed
    Can't it actually be used as an alias list that is automatically maintained by the mime package?

  • amaarets@community.nodebb.orgA
    11
    0

    Please note that since I updated my forum to the current version of develop there are errors like this in the console:

    nodebb.min.js?v=ee184c5e051:115 ServiceWorker registration succeeded.
    nodebb.min.js?v=ee184c5e051:180  GET https://good-link.org/topic/128/%D7%A1%D7%A7%D7%99%D7%A8%D7%AA-%D7%9E%D7%A1%D7%A4%D7%A8-%D7%90%D7%95%D7%96%D7%A0%D7%99%D7%95%D7%AA-%D7%91%D7%9C%D7%95%D7%98%D7%95%D7%A1-%D7%9E%D7%A0%D7%99%D7%A1%D7%99%D7%95%D7%9F-%D7%90%D7%99%D7%A9%D7%99/undefined/plugins/nodebb-plugin-emoji/emoji/table.json?v=ee184c5e051 404 (Not Found)
    send @ nodebb.min.js?v=ee184c5e051:180
    ajax @ nodebb.min.js?v=ee184c5e051:180
    r. @ nodebb.min.js?v=ee184c5e051:180
    getJSON @ nodebb.min.js?v=ee184c5e051:180
    l @ emoji.a0ae234478cad4ea7fe6.min.js:9
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    dispatch @ nodebb.min.js?v=ee184c5e051:177
    Fe.handle @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:178
    each @ nodebb.min.js?v=ee184c5e051:177
    each @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    n.init @ 95692.92188970b5a7d5a5d218.min.js:1
    F @ topic.e663e5599849111939b5.min.js:3
    b.init @ topic.e663e5599849111939b5.min.js:1
    await in b.init
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    Promise.then
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    await in (אנונימית)
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    Promise.then
    ajaxify.loadScript @ nodebb.min.js?v=ee184c5e051:114
    ajaxify.end @ nodebb.min.js?v=ee184c5e051:114
    ajaxify.coldLoad @ nodebb.min.js?v=ee184c5e051:114
    j @ nodebb.min.js?v=ee184c5e051:177
    dispatch @ nodebb.min.js?v=ee184c5e051:177
    Fe.handle @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:178
    each @ nodebb.min.js?v=ee184c5e051:177
    each @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    l @ nodebb.min.js?v=ee184c5e051:130
    R.fire @ nodebb.min.js?v=ee184c5e051:130
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    Promise.then
    app.load @ nodebb.min.js?v=ee184c5e051:114
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    await in (אנונימית)
    Fe @ nodebb.min.js?v=ee184c5e051:177
    Tt @ nodebb.min.js?v=ee184c5e051:177
    setTimeout
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:177
    B @ nodebb.min.js?v=ee184c5e051:177
    fireWith @ nodebb.min.js?v=ee184c5e051:177
    fire @ nodebb.min.js?v=ee184c5e051:177
    B @ nodebb.min.js?v=ee184c5e051:177
    fireWith @ nodebb.min.js?v=ee184c5e051:177
    ready @ nodebb.min.js?v=ee184c5e051:177
    setTimeout
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:177
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:177
    74692 @ nodebb.min.js?v=ee184c5e051:177
    Je @ nodebb.min.js?v=ee184c5e051:194
    67032 @ nodebb.min.js?v=ee184c5e051:114
    Je @ nodebb.min.js?v=ee184c5e051:194
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:195
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:195
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:195
    emoji.a0ae234478cad4ea7fe6.min.js:9 Error: [[emoji:meta-load-failed]]
        at Object. (emoji.a0ae234478cad4ea7fe6.min.js:9:1882)
        at y (emoji.a0ae234478cad4ea7fe6.min.js:1:1527)
        at Object.next (emoji.a0ae234478cad4ea7fe6.min.js:1:810)
        at emoji.a0ae234478cad4ea7fe6.min.js:1:439
        at new Promise ()
        at D (emoji.a0ae234478cad4ea7fe6.min.js:1:236)
        at emoji.a0ae234478cad4ea7fe6.min.js:9:1787
    (אנונימית) @ emoji.a0ae234478cad4ea7fe6.min.js:9
    y @ emoji.a0ae234478cad4ea7fe6.min.js:1
    (אנונימית) @ emoji.a0ae234478cad4ea7fe6.min.js:1
    (אנונימית) @ emoji.a0ae234478cad4ea7fe6.min.js:1
    D @ emoji.a0ae234478cad4ea7fe6.min.js:1
    (אנונימית) @ emoji.a0ae234478cad4ea7fe6.min.js:9
    Promise.catch
    l @ emoji.a0ae234478cad4ea7fe6.min.js:9
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    dispatch @ nodebb.min.js?v=ee184c5e051:177
    Fe.handle @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:178
    each @ nodebb.min.js?v=ee184c5e051:177
    each @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    n.init @ 95692.92188970b5a7d5a5d218.min.js:1
    F @ topic.e663e5599849111939b5.min.js:3
    b.init @ topic.e663e5599849111939b5.min.js:1
    await in b.init
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    Promise.then
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    await in (אנונימית)
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    Promise.then
    ajaxify.loadScript @ nodebb.min.js?v=ee184c5e051:114
    ajaxify.end @ nodebb.min.js?v=ee184c5e051:114
    ajaxify.coldLoad @ nodebb.min.js?v=ee184c5e051:114
    j @ nodebb.min.js?v=ee184c5e051:177
    dispatch @ nodebb.min.js?v=ee184c5e051:177
    Fe.handle @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:178
    each @ nodebb.min.js?v=ee184c5e051:177
    each @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    l @ nodebb.min.js?v=ee184c5e051:130
    R.fire @ nodebb.min.js?v=ee184c5e051:130
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    Promise.then
    app.load @ nodebb.min.js?v=ee184c5e051:114
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    await in (אנונימית)
    Fe @ nodebb.min.js?v=ee184c5e051:177
    Tt @ nodebb.min.js?v=ee184c5e051:177
    setTimeout
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:177
    B @ nodebb.min.js?v=ee184c5e051:177
    fireWith @ nodebb.min.js?v=ee184c5e051:177
    fire @ nodebb.min.js?v=ee184c5e051:177
    B @ nodebb.min.js?v=ee184c5e051:177
    fireWith @ nodebb.min.js?v=ee184c5e051:177
    ready @ nodebb.min.js?v=ee184c5e051:177
    setTimeout
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:177
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:177
    74692 @ nodebb.min.js?v=ee184c5e051:177
    Je @ nodebb.min.js?v=ee184c5e051:194
    67032 @ nodebb.min.js?v=ee184c5e051:114
    Je @ nodebb.min.js?v=ee184c5e051:194
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:195
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:195
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:195
    19:1 Uncaught (in promise) {readyState: 4, getResponseHeader: ƒ, getAllResponseHeaders: ƒ, setRequestHeader: ƒ, overrideMimeType: ƒ, …}
    setTimeout
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:177
    B @ nodebb.min.js?v=ee184c5e051:177
    fireWith @ nodebb.min.js?v=ee184c5e051:177
    fire @ nodebb.min.js?v=ee184c5e051:177
    B @ nodebb.min.js?v=ee184c5e051:177
    fireWith @ nodebb.min.js?v=ee184c5e051:177
    Tn @ nodebb.min.js?v=ee184c5e051:180
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:180
    XMLHttpRequest.send
    send @ nodebb.min.js?v=ee184c5e051:180
    ajax @ nodebb.min.js?v=ee184c5e051:180
    r. @ nodebb.min.js?v=ee184c5e051:180
    getJSON @ nodebb.min.js?v=ee184c5e051:180
    l @ emoji.a0ae234478cad4ea7fe6.min.js:9
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    dispatch @ nodebb.min.js?v=ee184c5e051:177
    Fe.handle @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:178
    each @ nodebb.min.js?v=ee184c5e051:177
    each @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    n.init @ 95692.92188970b5a7d5a5d218.min.js:1
    F @ topic.e663e5599849111939b5.min.js:3
    b.init @ topic.e663e5599849111939b5.min.js:1
    await in b.init
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    Promise.then
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    await in (אנונימית)
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    Promise.then
    ajaxify.loadScript @ nodebb.min.js?v=ee184c5e051:114
    ajaxify.end @ nodebb.min.js?v=ee184c5e051:114
    ajaxify.coldLoad @ nodebb.min.js?v=ee184c5e051:114
    j @ nodebb.min.js?v=ee184c5e051:177
    dispatch @ nodebb.min.js?v=ee184c5e051:177
    Fe.handle @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:178
    each @ nodebb.min.js?v=ee184c5e051:177
    each @ nodebb.min.js?v=ee184c5e051:177
    trigger @ nodebb.min.js?v=ee184c5e051:178
    l @ nodebb.min.js?v=ee184c5e051:130
    R.fire @ nodebb.min.js?v=ee184c5e051:130
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    Promise.then
    app.load @ nodebb.min.js?v=ee184c5e051:114
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:114
    await in (אנונימית)
    Fe @ nodebb.min.js?v=ee184c5e051:177
    Tt @ nodebb.min.js?v=ee184c5e051:177
    setTimeout
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:177
    B @ nodebb.min.js?v=ee184c5e051:177
    fireWith @ nodebb.min.js?v=ee184c5e051:177
    fire @ nodebb.min.js?v=ee184c5e051:177
    B @ nodebb.min.js?v=ee184c5e051:177
    fireWith @ nodebb.min.js?v=ee184c5e051:177
    ready @ nodebb.min.js?v=ee184c5e051:177
    setTimeout
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:177
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:177
    74692 @ nodebb.min.js?v=ee184c5e051:177
    Je @ nodebb.min.js?v=ee184c5e051:194
    67032 @ nodebb.min.js?v=ee184c5e051:114
    Je @ nodebb.min.js?v=ee184c5e051:194
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:195
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:195
    (אנונימית) @ nodebb.min.js?v=ee184c5e051:195
    19:1 The resource https://good-link.org/assets/uploads/system/notocoloremoji-regular.ttf was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally.
    
    

    I also see errors like this in the forum here

    Here in this forum the errors shown

    nodebb.min.js?v=0f27eef3dcd:12 ServiceWorker registration succeeded.
    undefined/plugins/nodebb-plugin-emoji/emoji/table.json?v=0f27eef3dcd:1  Failed to load resource: the server responded with a status of 404 (Not Found)
    emoji.a0ae234478cad4ea7fe6.min.js:9 Error: [[emoji:meta-load-failed]]
        at Object. (emoji.a0ae234478cad4ea7fe6.min.js:9:1882)
        at y (emoji.a0ae234478cad4ea7fe6.min.js:1:1527)
        at Object.next (emoji.a0ae234478cad4ea7fe6.min.js:1:810)
        at emoji.a0ae234478cad4ea7fe6.min.js:1:439
        at new Promise ()
        at D (emoji.a0ae234478cad4ea7fe6.min.js:1:236)
        at emoji.a0ae234478cad4ea7fe6.min.js:9:1787
    (anonymous) @ emoji.a0ae234478cad4ea7fe6.min.js:9
    14:1 Uncaught (in promise) Object
    
  • julian@community.nodebb.orgJ
    37
    0

    @AMAARETS those seem unrelated to the uploads issue, so perhaps you'll want to start a new topic for this 🙂

  • julian@community.nodebb.orgJ
    37
    0

    > @AMAARETS said:
    >
    > Just a quick question - I'm wondering what information I'm missing here
    > Why maintain a list of aliases, and not do the comparison by checking in reverse whether mime.getExtension(application/x-zip-compressed) returns a valid extension like ZIP

    If you check in reverse, then you are relying on the sender to supply you with the information. That's untrustworthy data — you can't guarantee someone didn't mess around with the data in between, or pretend that they're upload a jpg when it's actually an exe, etc.

  • amaarets@community.nodebb.orgA
    11
    0

    > @julian כתב:
    >
    > @AMAARETS those seem unrelated to the uploads issue, so perhaps you'll want to start a new topic for this 🙂

    This is not a problem that exists in the stable version, only when I updated the forum to develop, so this is indeed just a side note to bring to your attention.

    Edit: I see you just fixed it, didn't you?

  • julian@community.nodebb.orgJ
    37
    0

    @AMAARETS might be @baris, he's working on it 🙂

  • amaarets@community.nodebb.orgA
    11
    0

    > @julian כתב:
    >
    > > @AMAARETS said:
    > >
    > > Just a quick question - I'm wondering what information I'm missing here
    > > Why maintain a list of aliases, and not do the comparison by checking in reverse whether mime.getExtension(application/x-zip-compressed) returns a valid extension like ZIP
    >
    > If you check in reverse, then you are relying on the sender to supply you with the information. That's untrustworthy data — you can't guarantee someone didn't mess around with the data in between, or pretend that they're upload a jpg when it's actually an exe, etc.

    How is this worse than the test that is being performed now? It also relies, among other things, on a comparison of the MIME coming from the user and the MIME checked on the server.
    I just suggest replacing the manual map you added with a function that will automatically generate it by grouping all types that return the same result in mime.getExtension


Citiverse è un progetto che si basa su NodeBB ed è federato! | Categorie federate | Chat | 📱 Installa web app o APK | 🧡 Donazioni | Privacy Policy

Il server utilizzato è quello di Webdock, in Danimarca. Se volete provarlo potete ottenere il 20% di sconto con questo link e noi riceveremo un aiuto sotto forma di credito da usare proprio per mantenere Citiverse.