Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: completely scripted activation #6

Open
alexanderbuhler opened this issue Jan 20, 2019 · 15 comments
Open

Draft: completely scripted activation #6

alexanderbuhler opened this issue Jan 20, 2019 · 15 comments
Labels
enhancement New feature or request

Comments

@alexanderbuhler
Copy link

Hey there,

since all your content has helped me alot with setting up dockerized unity I wanted to share my working approach to activating unity by script.

node/puppeteer script:

const puppeteer = require('puppeteer');
const fs = require('fs');

try {
    fs.mkdirSync('debug_images');
    console.log('created debug folder.');

} catch (e) {
    console.log('debug folder already present.');
}


(async () => {

    const browser = await puppeteer.launch({
        args: ["--no-sandbox"]
    });
    const page = await browser.newPage();

    // open manual page & wait for login redirect

	await page.goto('https://license.unity3d.com/manual');

	const mailInputSelector = '#conversations_create_session_form_email',
		  passInputSelector = '#conversations_create_session_form_password';

	await page.waitForSelector(mailInputSelector);

    // enter credentials

	await page.type(mailInputSelector, process.env.UNITY_USERNAME);
	await page.type(passInputSelector, process.env.UNITY_PASSWORD);

	await page.screenshot({ path: 'debug_images/01_entered_credentials.png' });

    // click submit

	await page.click('input[name=commit]');

    // wait for license upload form

	const licenseUploadfield = '#licenseFile';

	await page.waitForSelector(licenseUploadfield);

	await page.screenshot({ path: 'debug_images/02_opened_form.png' });

    // enalbe interception
    
	await page.setRequestInterception(true);

    // upload license

	page.once("request", interceptedRequest => {
		
        interceptedRequest.continue({
            method: "POST",
            postData: fs.readFileSync(process.env.UNITY_ACTIVATION_FILE, 'utf8'),
            headers: { "Content-Type": "text/xml" },
        });

	});

	await page.goto('https://license.unity3d.com/genesis/activation/create-transaction');

	await page.screenshot({ path: 'debug_images/03_created_transaction.png' });

    // set license to be personal

    page.once("request", interceptedRequest => {
        interceptedRequest.continue({
            method: "PUT",
            postData: JSON.stringify({ transaction: { serial: { type: "personal" } } }),
            headers: { "Content-Type": "application/json" }
        });
    });

	await page.goto('https://license.unity3d.com/genesis/activation/update-transaction');

    await page.screenshot({ path: 'debug_images/04_updated_transaction.png' });
    
    // get license content

    page.once("request", interceptedRequest => {
        interceptedRequest.continue({
            method: "POST",
            postData: JSON.stringify({}),
            headers: { "Content-Type": "application/json" }
        });
    });

    page.on('response', async response => {  
                
        // write license

        try {
            const data = await response.text();
            const dataJson = await JSON.parse(data);
            fs.writeFileSync(process.env.UNITY_LICENSE_FILE, dataJson.xml);
            console.log('license file written.');

            await page.screenshot({ path: 'debug_images/05_received_license.png' });
            
        } catch (e) {
            console.log(e);
            console.log('failed to write license file.');
        }

    });

    await page.goto('https://license.unity3d.com/genesis/activation/download-license');
    await page.waitFor(1000);
    await browser.close();



})();

And then there's just a small shell script handling the rest. Please note that (only?) Unity 2018.3 cli supports manual generation of activation file, which comes in handy here.

#!/bin/sh

xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
  /opt/Unity/Editor/Unity -batchmode -createManualActivationFile -nographics -logfile

find -name "Unity*" -exec mv {} ../$UNITY_ACTIVATION_FILE \;
if [ $? -ne 0 ]
then
    echo "error while preparing generated license activation file."
	exit 3
fi


cd ../

# run puppeteer activation macro
node index.js
if [ $? -ne 0 ]
then
    echo "error while retrieving license."
	exit 3
fi

mv $UNITY_LICENSE_FILE /root/.local/share/unity3d/Unity/

if [ $? -eq 0 ]
then
    echo "license file installed."
	exit 0
fi

Involved env vars:

UNITY_USERNAME=XXXXX@XXXXX.XXX
UNITY_PASSWORD=XXXXXXXXXXXXX
UNITY_ACTIVATION_FILE=unity3d.alf
UNITY_LICENSE_FILE=Unity_lic.ulf

You might want to take a look at how to implement this into your images 👍

Best
Alex

@GabLeRoux
Copy link
Member

GabLeRoux commented Jan 22, 2019

Hey thanks Alex, that is some amazing work and I just discovered puppeteer! There's definitely place for this in the project.

I'm wondering what would be the best way to include this;

  1. A separate pipeline that can be invoked manually (but where and how should it be stored)
  2. Running it every time and only rely on username and password
  3. Running it every time, but cache the license so it gets reused on next build. Check if the file exists before running and whenever we clear the cache, it will download it again.

I think the best solutions are 2 or 3. I really like this as it makes it much easier and reliable for supporting any CI. And cool thing, this can be in a separate image (we don't need node in the base image).

@GabLeRoux GabLeRoux added the enhancement New feature or request label Jan 22, 2019
@alexanderbuhler
Copy link
Author

alexanderbuhler commented Jan 22, 2019

Yeah, puppeteer is great with a lot of documentation and resources out there. I remember scripting with "plain" phantomjs in async way, which was a painful experience.

Cache would be an idea, but you'd have to keep in mind that the license can only be used if the docker container wasn't rebuilt and has the same machine name. I've incorporated this activation into the container build process, so when the container is ready, unity is already activated.

On a side note: while intensive testing and building I've noticed that some load balancer or application firewall blocks your attempt to authenticate/create new auth sessions with unity id. It worked again after a day or so. But you should be able to easily get ~20 license files until it stops working...

@YAMLcase
Copy link

YAMLcase commented Apr 20, 2019

oh, I'm bookmarking this issue. Thanks Alex!

KonH added a commit to KonH/UnityCiPipeline that referenced this issue May 30, 2019
KonH added a commit to KonH/UnityCiPipeline that referenced this issue May 30, 2019
@KonH
Copy link

KonH commented May 30, 2019

@alexanderbuhler, thanks for sharing your approach!

I tried it in my test project (https://github.com/KonH/UnityCiPipeline) but faced with request verification code issue, unfortunately.
When the script is waiting for license upload form, I got another window:

Enter your code
We detected some changes during your login, to give you better security, we have sent a verification code to your login email address. Please input the code below.

I got an email with that code, but I obviously can't use it, because the build is running on CI env.

Now I think that only two options are working for CI with Unity:

  • Plus/Pro subscription (potentially it doesn't have such issues, you can run Unity with serial)
  • Unity Cloud Build

It's sad. I don't use Unity for commercial projects at home, so I don't want to buy a subscription. And Cloud Build isn't a good solution for CI, it's slow and has its own issues.

@GabLeRoux
Copy link
Member

GabLeRoux commented May 30, 2019

@KonH I never faced this Enter your code issue when running on gitlab-ci, but I did the license technique explained in the repository's readme, not the above script.

I don't exactly know what leads to the error you just shared, but maybe you can give a try with manually obtaining the xml license and set it in the environment variable as documented. I don't really like it, but it did work for me

@KonH
Copy link

KonH commented May 30, 2019

Thanks, I will try it (have problems with it and decided to use this approach before digging into a variable usage)

@GabLeRoux
Copy link
Member

GabLeRoux commented May 30, 2019

Don't hesitate to open an issue if you hit a problem with the procedure described in the documentation :) The main repo is on gitlab so you'll find more details there, I've tagged issues related to unity activation.

@alexanderbuhler
Copy link
Author

@KonH I was running this script either from within my local docker or a server roughly at the same place where I live. I could imagine unity having some security mechanism in place e.g. if you used to login from EU and suddenly trigger a login from the USA. Where's your machine running the script located?

@KonH
Copy link

KonH commented May 31, 2019

It's strange, but I got this error even if I just run it locally, on the same machine, when I can log in correctly in a browser.
Now I tried to create a fresh new account and nothing changes)
@alexanderbuhler when you tried it last time? Maybe it's a new security feature?

@alexanderbuhler
Copy link
Author

@KonH Just tried creating a fresh container, license generation still works perfectly fine.

(Sorry it took so long, was on vacation and stuff)

@KonH
Copy link

KonH commented Jul 9, 2019

It's strange. Anyway, I fall back to manual activation inside container now.

@GabLeRoux
Copy link
Member

GabLeRoux commented Sep 3, 2019

I just tried the script today and it's failing with the following error:

(node:1) UnhandledPromiseRejectionWarning: TimeoutError: waiting for selector "#licenseFile" failed: timeout 30000ms exceeded
    at new WaitTask (/node_modules/puppeteer/lib/DOMWorld.js:561:28)
    at DOMWorld._waitForSelectorOrXPath (/node_modules/puppeteer/lib/DOMWorld.js:490:22)
    at DOMWorld.waitForSelector (/node_modules/puppeteer/lib/DOMWorld.js:444:17)
    at Frame.waitForSelector (/node_modules/puppeteer/lib/FrameManager.js:628:47)
    at Frame.<anonymous> (/node_modules/puppeteer/lib/helper.js:111:23)
    at Page.waitForSelector (/node_modules/puppeteer/lib/Page.js:1046:29)
    at /app/index.js:35:16
    at process._tickCallback (internal/process/next_tick.js:68:7)
(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:1) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

I updated the script a little, but that's basically the step where it presses the login button. I also noticed an email which shows the following content:

Hello,  Verification code for your Unity ID is SNIP (5 digit code) and will be expired in 5 minutes. You are getting this email because we have noticed you are trying to log in via another geographic location. You can avoid these emails by adding a TFA system to your account by going to id.unity.com.For security purpose, please do not share your account information to anyone else and thanks for your understanding.

Sincerely,The Unity Team

Funny thing is that I use my license in random servers from CIs runners in many locations, but this time, I just ran it locally only for testing.

Anyway, I logged in manually in my browser locally, but I'm wondering if this is just going to also happen on a random CI runner. I'll update here with more details, but I still think this is possible.

Here's what could be done even if it's totally insane:

  1. Setup a redirect email from personal email account coming from accounts@unity3d.com with title Security Notice - Verification code for Unity ID
  2. Send the mail to a custom made api / smtp server which only serves this purpose.
  3. Extract the auth code (with a regex and/or sed command)
  4. Expose it on a public api with credentials or api key and https
  5. Update the script to figure out it needs to fill the email 2 factor
  6. Request the latest exposed code from the custom made api
  7. Continue (and maybe try a few times for 5 minutes until code is considered expired)

Overkill, but hey, I wish unity did something better about it like exposing an api for doing activation instead. This would work ^^.

@alexanderbuhler
Copy link
Author

I'm happy the script still "works" for me, since the verification step seems to be based on some random logic and didn't appear for me yet.

But yeah, I've kinda went though necessary steps here aswell @GabLeRoux. You've summerized them precisely there. Insane indeed.

@GabLeRoux
Copy link
Member

Yes, worked for me too after filling the form once 🎉

I bundled this in a docker image locally, I will publish this somewhere so we can use it as a docker image in any pipeline with no efforts shortly. Thanks again for this amazing work 🍰

@kuler90
Copy link

kuler90 commented Nov 14, 2020

Hi there,

@alexanderbuhler, thanks for sharing your approach.

I solved the problem with code verification! You just need account with enabled Two-Factor Authentication by Authenticator App. Verification code can be generated using authenticator key. You can find the details in my activate-unity action.

@GabLeRoux I think you can use my solution in your docker images because I have no plan to support Gitlab CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants