From 2796790c7d6ca1f7520b87e570364c7d81a9e205 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Tue, 28 Feb 2017 21:10:46 +0200 Subject: [PATCH] feat(aio): verify uploaded builds based on JWT from Travis --- aio/aio-builds-setup/dockerbuild/Dockerfile | 2 + .../lib/common/github-pull-requests.ts | 7 +- .../scripts-js/lib/upload-server/index.ts | 35 +-- .../upload-server/upload-server-factory.ts | 56 +++- .../upload-server-factory.spec.ts | 249 ++++++++++++++---- .../scripts-sh/upload-server-test.sh | 3 + 6 files changed, 262 insertions(+), 90 deletions(-) diff --git a/aio/aio-builds-setup/dockerbuild/Dockerfile b/aio/aio-builds-setup/dockerbuild/Dockerfile index 0fcfc6e2da..234ddf4214 100644 --- a/aio/aio-builds-setup/dockerbuild/Dockerfile +++ b/aio/aio-builds-setup/dockerbuild/Dockerfile @@ -13,6 +13,8 @@ EXPOSE 80 443 ENV AIO_BUILDS_DIR=/var/www/aio-builds TEST_AIO_BUILDS_DIR=/tmp/aio-builds \ AIO_DOMAIN_NAME=ngbuilds.io TEST_AIO_DOMAIN_NAME=test-ngbuilds.io \ + AIO_GITHUB_ORGANIZATION=angular TEST_AIO_GITHUB_ORGANIZATION=angular \ + AIO_GITHUB_TEAM_SLUGS=angular-core TEST_AIO_GITHUB_TEAM_SLUGS=angular-core \ AIO_NGINX_HOSTNAME=nginx.localhost TEST_AIO_NGINX_HOSTNAME=nginx.localhost \ AIO_NGINX_PORT_HTTP=80 TEST_AIO_NGINX_PORT_HTTP=8080 \ AIO_NGINX_PORT_HTTPS=443 TEST_AIO_NGINX_PORT_HTTPS=4433 \ diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts index cf31f2bd99..3a53259774 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts @@ -3,8 +3,9 @@ import {assertNotMissingOrEmpty} from '../common/utils'; import {GithubApi} from './github-api'; // Interfaces - Types -interface PullRequest { +export interface PullRequest { number: number; + user: {login: string}; } export type PullRequestState = 'all' | 'closed' | 'open'; @@ -28,6 +29,10 @@ export class GithubPullRequests extends GithubApi { return this.post(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body}); } + public fetch(pr: number): Promise { + return this.get(`/repos/${this.repoSlug}/pulls/${pr}`); + } + public fetchAll(state: PullRequestState = 'all'): Promise { console.log(`Fetching ${state} pull requests...`); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index.ts index acfcc98249..85c2631c90 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index.ts @@ -2,15 +2,16 @@ process.setuid('www-data'); // Imports -import {GithubPullRequests} from '../common/github-pull-requests'; import {getEnvVar} from '../common/utils'; -import {CreatedBuildEvent} from './build-events'; import {uploadServerFactory} from './upload-server-factory'; // Constants const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR'); -const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN', true); -const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG', true); +const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION'); +const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS'); +const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN'); +const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN'); +const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG'); const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME'); const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT'); @@ -20,23 +21,13 @@ _main(); // Functions function _main() { uploadServerFactory. - create(AIO_BUILDS_DIR). - on(CreatedBuildEvent.type, createOnBuildCreatedHanlder()). + create({ + buildsDir: AIO_BUILDS_DIR, + githubOrganization: AIO_GITHUB_ORGANIZATION, + githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','), + githubToken: AIO_GITHUB_TOKEN, + repoSlug: AIO_REPO_SLUG, + secret: AIO_PREVIEW_DEPLOYMENT_TOKEN, + }). listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME); } - -function createOnBuildCreatedHanlder() { - if (!AIO_REPO_SLUG) { - console.warn('No repo specified. Preview links will not be posted on PRs.'); - return () => null; - } - - const githubPullRequests = new GithubPullRequests(AIO_REPO_SLUG, AIO_GITHUB_TOKEN); - - return ({pr, sha}: CreatedBuildEvent) => { - const body = `The angular.io preview for ${sha.slice(0, 7)} is available [here][1].\n\n` + - `[1]: https://pr${pr}-${sha}.ngbuilds.io/`; - - githubPullRequests.addComment(pr, body); - }; -} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-server-factory.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-server-factory.ts index b1fcbb82b9..7c91d1fce8 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-server-factory.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-server-factory.ts @@ -1,25 +1,43 @@ // Imports import * as express from 'express'; import * as http from 'http'; -import {assertNotMissingOrEmpty} from '../common/utils'; +import {GithubPullRequests} from '../common/github-pull-requests'; import {BuildCreator} from './build-creator'; import {CreatedBuildEvent} from './build-events'; +import {BuildVerifier} from './build-verifier'; import {UploadError} from './upload-error'; // Constants +const AUTHORIZATION_HEADER = 'AUTHORIZATION'; const X_FILE_HEADER = 'X-FILE'; +// Interfaces - Types +interface UploadServerConfig { + buildsDir: string; + githubOrganization: string; + githubTeamSlugs: string[]; + githubToken: string; + repoSlug: string; + secret: string; +} + // Classes class UploadServerFactory { // Methods - Public - public create(buildsDir: string): http.Server { - assertNotMissingOrEmpty('buildsDir', buildsDir); + public create({ + buildsDir, + githubOrganization, + githubTeamSlugs, + githubToken, + repoSlug, + secret, + }: UploadServerConfig): http.Server { + const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs); + const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug); - const buildCreator = new BuildCreator(buildsDir); - const middleware = this.createMiddleware(buildCreator); + const middleware = this.createMiddleware(buildVerifier, buildCreator); const httpServer = http.createServer(middleware); - buildCreator.on(CreatedBuildEvent.type, (data: CreatedBuildEvent) => httpServer.emit(CreatedBuildEvent.type, data)); httpServer.on('listening', () => { const info = httpServer.address(); console.info(`Up and running (and listening on ${info.address}:${info.port})...`); @@ -29,20 +47,38 @@ class UploadServerFactory { } // Methods - Protected - protected createMiddleware(buildCreator: BuildCreator): express.Express { + protected createBuildCreator(buildsDir: string, githubToken: string, repoSlug: string): BuildCreator { + const buildCreator = new BuildCreator(buildsDir); + const githubPullRequests = new GithubPullRequests(githubToken, repoSlug); + + buildCreator.on(CreatedBuildEvent.type, ({pr, sha}: CreatedBuildEvent) => { + const body = `The angular.io preview for ${sha.slice(0, 7)} is available [here][1].\n\n` + + `[1]: https://pr${pr}-${sha}.ngbuilds.io/`; + + githubPullRequests.addComment(pr, body); + }); + + return buildCreator; + } + + protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express { const middleware = express(); middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => { const pr = req.params[0]; const sha = req.params[1]; const archive = req.header(X_FILE_HEADER); + const authHeader = req.header(AUTHORIZATION_HEADER); - if (!archive) { + if (!authHeader) { + this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req); + } else if (!archive) { this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req); } - buildCreator. - create(pr, sha, archive). + buildVerifier. + verify(+pr, authHeader). + then(() => buildCreator.create(pr, sha, archive)). then(() => res.sendStatus(201)). catch(err => this.respondWithError(res, err)); }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-server-factory.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-server-factory.spec.ts index 26d74ff1e6..110f1893d0 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-server-factory.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-server-factory.spec.ts @@ -2,12 +2,27 @@ import * as express from 'express'; import * as http from 'http'; import * as supertest from 'supertest'; +import {GithubPullRequests} from '../../lib/common/github-pull-requests'; import {BuildCreator} from '../../lib/upload-server/build-creator'; import {CreatedBuildEvent} from '../../lib/upload-server/build-events'; +import {BuildVerifier} from '../../lib/upload-server/build-verifier'; import {uploadServerFactory as usf} from '../../lib/upload-server/upload-server-factory'; // Tests describe('uploadServerFactory', () => { + const defaultConfig = { + buildsDir: 'builds/dir', + githubOrganization: 'organization', + githubTeamSlugs: ['team1', 'team2'], + githubToken: '12345', + repoSlug: 'repo/slug', + secret: 'secret', + }; + + // Helpers + const createUploadServer = (partialConfig: Partial = {}) => + usf.create({...defaultConfig, ...partialConfig}); + describe('create()', () => { let usfCreateMiddlewareSpy: jasmine.Spy; @@ -17,55 +32,77 @@ describe('uploadServerFactory', () => { }); - it('should throw if \'buildsDir\' is empty', () => { - expect(() => usf.create('')).toThrowError('Missing or empty required parameter \'buildsDir\'!'); + it('should throw if \'secret\' is missing or empty', () => { + expect(() => createUploadServer({secret: ''})). + toThrowError('Missing or empty required parameter \'secret\'!'); + }); + + + it('should throw if \'githubToken\' is missing or empty', () => { + expect(() => createUploadServer({githubToken: ''})). + toThrowError('Missing or empty required parameter \'githubToken\'!'); + }); + + + it('should throw if \'githubOrganization\' is missing or empty', () => { + expect(() => createUploadServer({githubOrganization: ''})). + toThrowError('Missing or empty required parameter \'organization\'!'); + }); + + + it('should throw if \'githubTeamSlugs\' is missing or empty', () => { + expect(() => createUploadServer({githubTeamSlugs: []})). + toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!'); + }); + + + it('should throw if \'repoSlug\' is missing or empty', () => { + expect(() => createUploadServer({repoSlug: ''})). + toThrowError('Missing or empty required parameter \'repoSlug\'!'); + }); + + + it('should throw if \'secret\' is missing or empty', () => { + expect(() => createUploadServer({secret: ''})). + toThrowError('Missing or empty required parameter \'secret\'!'); }); it('should return an http.Server', () => { const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough(); - const server = usf.create('builds/dir'); + const server = createUploadServer(); expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue); }); + it('should create and use an appropriate BuildCreator', () => { + const usfCreateBuildCreatorSpy = spyOn(usf as any, 'createBuildCreator').and.callThrough(); + + createUploadServer(); + const buildCreator: BuildCreator = usfCreateBuildCreatorSpy.calls.mostRecent().returnValue; + + expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(jasmine.any(BuildVerifier), buildCreator); + expect(usfCreateBuildCreatorSpy).toHaveBeenCalledWith('builds/dir', '12345', 'repo/slug'); + }); + + it('should create and use an appropriate middleware', () => { const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough(); - usf.create('builds/dir'); + createUploadServer(); const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue; + const buildVerifier = jasmine.any(BuildVerifier); + const buildCreator = jasmine.any(BuildCreator); - expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(jasmine.any(BuildCreator)); expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware); - }); - - - it('should pass \'buildsDir\' to the created BuildCreator', () => { - usf.create('builds/dir'); - const buildCreator: BuildCreator = usfCreateMiddlewareSpy.calls.argsFor(0)[0]; - - expect((buildCreator as any).buildsDir).toBe('builds/dir'); - }); - - - it('should pass CreatedBuildEvents emitted on BuildCreator through to the server', done => { - const server = usf.create('builds/dir'); - const buildCreator: BuildCreator = usfCreateMiddlewareSpy.calls.argsFor(0)[0]; - const evt = new CreatedBuildEvent(42, 'foo'); - - server.on(CreatedBuildEvent.type, (data: CreatedBuildEvent) => { - expect(data).toBe(evt); - done(); - }); - - buildCreator.emit(CreatedBuildEvent.type, evt); + expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildVerifier, buildCreator); }); it('should log the server address info on \'listening\'', () => { const consoleInfoSpy = spyOn(console, 'info'); - const server = usf.create('builds/dir'); + const server = createUploadServer('builds/dir'); server.address = () => ({address: 'foo', family: '', port: 1337}); expect(consoleInfoSpy).not.toHaveBeenCalled(); @@ -79,7 +116,50 @@ describe('uploadServerFactory', () => { // Protected methods + describe('createBuildCreator()', () => { + let buildCreator: BuildCreator; + + beforeEach(() => { + buildCreator = (usf as any).createBuildCreator( + defaultConfig.buildsDir, + defaultConfig.githubToken, + defaultConfig.repoSlug, + ); + }); + + + it('should pass the \'buildsDir\' to the BuildCreator', () => { + expect((buildCreator as any).buildsDir).toBe('builds/dir'); + }); + + + it('should post a comment on GitHub on \'build.created\'', () => { + const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'); + const commentBody = 'The angular.io preview for 1234567 is available [here][1].\n\n' + + '[1]: https://pr42-1234567890.ngbuilds.io/'; + + buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'}); + + expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody); + }); + + + it('should pass the correct \'githubToken\' and \'repoSlug\' to GithubPullRequests', () => { + const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'); + + buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'}); + const prs = prsAddCommentSpy.calls.mostRecent().object; + + expect(prs).toEqual(jasmine.any(GithubPullRequests)); + expect((prs as any).repoSlug).toBe('repo/slug'); + expect((prs as any).requestHeaders.Authorization).toContain('12345'); + }); + + }); + + describe('createMiddleware()', () => { + let buildVerifier: BuildVerifier; let buildCreator: BuildCreator; let agent: supertest.SuperTest; @@ -90,8 +170,15 @@ describe('uploadServerFactory', () => { Promise.all(reqs.map(promisifyRequest)).then(done, done.fail); beforeEach(() => { - buildCreator = new BuildCreator('builds/dir'); - agent = supertest.agent((usf as any).createMiddleware(buildCreator)); + buildVerifier = new BuildVerifier( + defaultConfig.secret, + defaultConfig.githubToken, + defaultConfig.repoSlug, + defaultConfig.githubOrganization, + defaultConfig.githubTeamSlugs, + ); + buildCreator = new BuildCreator(defaultConfig.buildsDir); + agent = supertest.agent((usf as any).createMiddleware(buildVerifier, buildCreator)); spyOn(console, 'error'); }); @@ -100,14 +187,12 @@ describe('uploadServerFactory', () => { describe('GET /create-build//', () => { const pr = '9'; const sha = '9'.repeat(40); + let buildVerifierVerifySpy: jasmine.Spy; let buildCreatorCreateSpy: jasmine.Spy; - let deferred: {resolve: Function, reject: Function}; beforeEach(() => { - const promise = new Promise((resolve, reject) => deferred = {resolve, reject}); - promise.catch(() => null); // Avoid "unhandled rejection" warnings. - - buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(promise); + buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve()); + buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(Promise.resolve()); }); @@ -121,13 +206,27 @@ describe('uploadServerFactory', () => { }); + it('should respond with 401 for requests without an \'AUTHORIZATION\' header', done => { + const url = `/create-build/${pr}/${sha}`; + const responseBody = `Missing or empty 'AUTHORIZATION' header in request: GET ${url}`; + + verifyRequests([ + agent.get(url).expect(401, responseBody), + agent.get(url).set('AUTHORIZATION', '').expect(401, responseBody), + ], done); + }); + + it('should respond with 400 for requests without an \'X-FILE\' header', done => { const url = `/create-build/${pr}/${sha}`; const responseBody = `Missing or empty 'X-FILE' header in request: GET ${url}`; + const request1 = agent.get(url).set('AUTHORIZATION', 'foo'); + const request2 = agent.get(url).set('AUTHORIZATION', 'foo').set('X-FILE', ''); + verifyRequests([ - agent.get(url).expect(400, responseBody), - agent.get(url).field('X-FILE', '').expect(400, responseBody), + request1.expect(400, responseBody), + request2.expect(400, responseBody), ], done); }); @@ -146,34 +245,68 @@ describe('uploadServerFactory', () => { }); - it('should propagate errors from BuildCreator', done => { + it('should call \'BuildVerifier#verify()\' with the correct arguments', done => { const req = agent. get(`/create-build/${pr}/${sha}`). - set('X-FILE', 'foo'). + set('AUTHORIZATION', 'foo'). + set('X-FILE', 'bar'); + + promisifyRequest(req). + then(() => expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo')). + then(done, done.fail); + }); + + + it('should propagate errors from BuildVerifier', done => { + buildVerifierVerifySpy.and.callFake(() => Promise.reject('Test')); + + const req = agent. + get(`/create-build/${pr}/${sha}`). + set('AUTHORIZATION', 'foo'). + set('X-FILE', 'bar'). + expect(500, 'Test'); + + promisifyRequest(req). + then(() => { + expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo'); + expect(buildCreatorCreateSpy).not.toHaveBeenCalled(); + }). + then(done, done.fail); + }); + + + it('should call \'BuildCreator#create()\' with the correct arguments', done => { + const req = agent. + get(`/create-build/${pr}/${sha}`). + set('AUTHORIZATION', 'foo'). + set('X-FILE', 'bar'); + + promisifyRequest(req). + then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar')). + then(done, done.fail); + }); + + + it('should propagate errors from BuildCreator', done => { + buildCreatorCreateSpy.and.callFake(() => Promise.reject('Test')); + const req = agent. + get(`/create-build/${pr}/${sha}`). + set('AUTHORIZATION', 'foo'). + set('X-FILE', 'bar'). expect(500, 'Test'); verifyRequests([req], done); - deferred.reject('Test'); }); it('should respond with 201 on successful upload', done => { const req = agent. get(`/create-build/${pr}/${sha}`). - set('X-FILE', 'foo'). + set('AUTHORIZATION', 'foo'). + set('X-FILE', 'bar'). expect(201, http.STATUS_CODES[201]); verifyRequests([req], done); - deferred.resolve(); - }); - - - it('should call \'BuildCreator#create()\' with appropriate arguments', done => { - promisifyRequest(agent.get(`/create-build/${pr}/${sha}`).set('X-FILE', 'foo').expect(201)). - then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'foo')). - then(done, done.fail); - - deferred.resolve(); }); @@ -183,16 +316,18 @@ describe('uploadServerFactory', () => { it('should accept SHAs with leading zeros (but not ignore them)', done => { + const sha41 = '0'.repeat(41); const sha40 = '0'.repeat(40); - const sha41 = `0${sha40}`; + + const request41 = agent.get(`/create-build/${pr}/${sha41}`); + const request40 = agent.get(`/create-build/${pr}/${sha40}`). + set('AUTHORIZATION', 'foo'). + set('X-FILE', 'bar'); Promise.all([ - promisifyRequest(agent.get(`/create-build/${pr}/${sha41}`).expect(404)), - promisifyRequest(agent.get(`/create-build/${pr}/${sha40}`).set('X-FILE', 'foo')). - then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha40, 'foo')), + promisifyRequest(request41.expect(404)), + promisifyRequest(request40.expect(201)), ]).then(done, done.fail); - - deferred.resolve(); }); }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-test.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-test.sh index c02da29551..7fc90c9de8 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-test.sh +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-test.sh @@ -3,6 +3,9 @@ set -e -o pipefail # Set up env variables for testing export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR +export AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION +export AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS +export AIO_PREVIEW_DEPLOYMENT_TOKEN=$TEST_AIO_PREVIEW_DEPLOYMENT_TOKEN export AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG export AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME export AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT