2023-02-17 pub-dev登錄和pub發(fā)布流程

1. https://pub.dev/ 網(wǎng)站登錄

接入谷歌OAuth2登錄闽晦。

Google 登錄 JavaScript 客戶端參考文檔:

https://developers.google.com/identity/sign-in/web/reference#gapiauth2authorizeconfig

針對(duì)網(wǎng)絡(luò)服務(wù)器應(yīng)用使用 OAuth 2.0:https://developers.google.com/identity/protocols/oauth2/web-server?hl=zh-cn#offline

pub-dev登錄的具體流程:

  1. 用戶打開 pub.dev 網(wǎng)站并點(diǎn)擊登錄按鈕。
  2. 網(wǎng)站重定向到 Google 賬號(hào)登錄界面。
  3. 用戶輸入其 Google 賬號(hào)和密碼孕似。
  4. Google 驗(yàn)證用戶身份并向用戶顯示一個(gè)授權(quán)頁面送朱,列出網(wǎng)站將獲得的權(quán)限列表。
  5. 用戶選擇是否授權(quán)該網(wǎng)站訪問其 Google 賬戶的特定信息搔谴。
  6. 用戶點(diǎn)擊授權(quán)按鈕魁袜。
  7. Google 發(fā)送一個(gè)包含授權(quán)令牌的回調(diào) URL 給 pub.dev 網(wǎng)站
  8. pub.dev 網(wǎng)站收到授權(quán)令牌并使用它來訪問用戶的 Google 賬戶信息(例如用戶的電子郵件地址和用戶名)。
  9. pub.dev 網(wǎng)站將這些信息與其自己的用戶數(shù)據(jù)庫進(jìn)行匹配敦第,如果沒有與用戶的 Google 賬戶關(guān)聯(lián)的本地帳戶峰弹,則 pub.dev 將創(chuàng)建一個(gè)新的帳戶,并將其與用戶的 Google 賬戶關(guān)聯(lián)芜果。

此過程基于 OAuth 2.0 協(xié)議鞠呈。OAuth 2.0 是一種授權(quán)協(xié)議,用于在不共享用戶憑據(jù)的情況下右钾,允許第三方應(yīng)用程序訪問用戶資源蚁吝。在這種情況下,Google 充當(dāng)身份提供者舀射,pub.dev 充當(dāng)客戶端應(yīng)用程序窘茁,而用戶是資源所有者。Google 授權(quán) pub.dev 訪問用戶的資源脆烟,同時(shí)確保不會(huì)向 pub.dev 共享用戶的憑據(jù)山林。


a5.png

pub.dev源碼地址:https://github.com/dart-lang/pub-dev
這里用google授權(quán)登錄,跟pub 組件 的谷歌登錄是一樣的邢羔。pub組件包源碼地址:https://github.com/dart-lang/pub

pub項(xiàng)目和pub-dev項(xiàng)目用到的客戶端ID驼抹,客戶端密鑰都是相同的

pub-dev項(xiàng)目:
/pub-dev/pkg/pub_integration/lib/src/pub_tool_client.dart

// OAuth clientId and clientSecret also hardcoded into the `pub` client.
final _pubClientId =
    '818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.googleusercontent.com';
final _pubClientSecret = 'SWeqj8seoJW0w7_CpEPFLX0K';

登錄pub.dev
https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?redirect_uri=storagerelay://https/pub.dev?id=auth358956&response_type=permission id_token&scope=email profile openid&openid.realm&include_granted_scopes=true&client_id=818368855108-e8skaopm5ih5nbb82vhh66k7ft5o7dn3.apps.googleusercontent.com&ss_domain=https://pub.dev&prompt=select_account&fetch_basic_profile=true&gsiwebsdk=2&service=lso&o2v=1&flowName=GeneralOAuthFlow

pub 項(xiàng)目:
/pub/lib/src/oauth2.dart


/// The pub client's OAuth2 identifier.
const _identifier = '818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.'
    'googleusercontent.com';

/// The pub client's OAuth2 secret.
///
/// This isn't actually meant to be kept a secret.
const _secret = 'SWeqj8seoJW0w7_CpEPFLX0K';

https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?access_type=offline&approval_prompt=force&response_type=code&client_id=818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.googleusercontent.com&redirect_uri=http://localhost:54969&code_challenge=MhrdCM1Z0xkd2GdNgfbxhD5h487Mug_Uj4q8AUevSJY&code_challenge_method=S256&scope=openid https://www.googleapis.com/auth/userinfo.email&service=lso&o2v=1&flowName=GeneralOAuthFlow

2. pub publish 發(fā)布組件

通過調(diào)用 pub 包中的函數(shù)實(shí)現(xiàn)的。而 flutter pub publish 命令本身只是一個(gè)包裝器张抄,它調(diào)用 pub 包中的函數(shù)來完成實(shí)際的操作砂蔽。

dart提供的 pub 包項(xiàng)目地址:

https://github.com/dart-lang/pub

發(fā)布組件的大致整體流程如下:

  1. 解析 pubspec.yaml 文件,獲取包名和版本號(hào)署惯,并檢查包名和版本號(hào)的合法性左驾。
  2. 構(gòu)建上傳包的壓縮文件,包括 lib极谊、bin诡右、example 等文件夾中的文件以及 pubspec.yaml 文件。
  3. 對(duì)上傳包進(jìn)行驗(yàn)證轻猖,包括檢查上傳包的大小帆吻、檢查是否包含非法字符等。
  4. 通過 OAuth2 或 Bearer 身份驗(yàn)證機(jī)制登錄 pub.dev 等服務(wù)器咙边。
  5. 上傳壓縮文件到服務(wù)器猜煮,并等待服務(wù)器響應(yīng)結(jié)果次员。
  6. 如果上傳成功,更新本地緩存并顯示上傳成功的信息王带;如果上傳失敗淑蔚,拋出相應(yīng)的異常并顯示上傳失敗的信息。
a6.png

2.1 組件包解析

  • 檢查命令行參數(shù) --server 是否已解析愕撰,如果已解析刹衫,則輸出一條警告信息,表明此選項(xiàng)已過時(shí)搞挣,應(yīng)該使用 pubspec.yaml 文件中的 publish_to 字段或設(shè)置 $PUB_HOSTED_URL 環(huán)境變量代替带迟。
  • 檢查命令行參數(shù) --force 和 --dry-run 是否同時(shí)存在,如果是囱桨,則拋出一個(gè)使用異常仓犬,表示這兩個(gè)選項(xiàng)不能同時(shí)使用。
  • 檢查當(dāng)前包是否為私有包蝇摸,如果是婶肩,則輸出一條錯(cuò)誤信息,表示私有包不能被發(fā)布貌夕,應(yīng)該通過更改 pubspec.yaml 文件中的 publish_to 字段來啟用律歼。
  • 調(diào)用 entrypoint.acquireDependencies() 函數(shù)獲取包依賴項(xiàng)。
  • 獲取當(dāng)前包的文件列表啡专,打印一條消息险毁,說明正在打包和發(fā)布當(dāng)前包,并展示包含的文件列表们童。
  • 創(chuàng)建并壓縮當(dāng)前包的文件畔况,并獲取壓縮包的字節(jié)數(shù)組。
  • 驗(yàn)證當(dāng)前包是否有效慧库,如果無效跷跪,則設(shè)置退出碼并返回,否則繼續(xù)執(zhí)行下一步齐板。
  • 如果是 --dry-run 模式吵瞻,則打印一條消息,表示服務(wù)器可能會(huì)執(zhí)行額外的檢查甘磨,然后返回橡羞,否則繼續(xù)執(zhí)行下一步。
  • 調(diào)用 _publish() 函數(shù)济舆,將壓縮包上傳到指定的服務(wù)器卿泽,并等待上傳完成。
  @override
  Future runProtected() async {
    if (argResults.wasParsed('server')) {
      await log.errorsOnlyUnlessTerminal(() {
        log.message(
          '''
The --server option is deprecated. Use `publish_to` in your pubspec.yaml or set
the \$PUB_HOSTED_URL environment variable.''',
        );
      });
    }

    if (force && dryRun) {
      usageException('Cannot use both --force and --dry-run.');
    }

    if (entrypoint.root.pubspec.isPrivate) {
      dataError('A private package cannot be published.\n'
          'You can enable this by changing the "publish_to" field in your '
          'pubspec.');
    }

    await entrypoint.acquireDependencies(SolveType.get, analytics: analytics);

    var files = entrypoint.root.listFiles();
    log.fine('Archiving and publishing ${entrypoint.root.name}.');

    // Show the package contents so the user can verify they look OK.
    var package = entrypoint.root;
    log.message(
      'Publishing ${package.name} ${package.version} to $host:\n'
      '${tree.fromFiles(files, baseDir: entrypoint.root.dir, showFileSizes: true)}',
    );

    var packageBytesFuture =
        createTarGz(files, baseDir: entrypoint.root.dir).toBytes();

    // Validate the package.
    var isValid = await _validate(
      packageBytesFuture.then((bytes) => bytes.length),
      files,
    );
    if (!isValid) {
      overrideExitCode(exit_codes.DATA);
      return;
    } else if (dryRun) {
      log.message('The server may enforce additional checks.');
      return;
    } else {
      await _publish(await packageBytesFuture);
    }
  }

pub使用谷歌帳戶來管理包上載權(quán)限滋觉。

acquireDependencies- 》 _validate-》_publish

2.2 使用谷歌賬號(hào)登錄

  • 請(qǐng)求授權(quán)碼:https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force

  • get請(qǐng)求 結(jié)果通過callback URL回調(diào)傳回來:
    pub的啟動(dòng)本地服務(wù)器:

 
Future<Client> _authorize() async {
 
  var completer = Completer();
  var server = await bindServer('localhost', 0);

  shelf_io.serveRequests(server, (request) {
 

    return shelf.Response.found('https://pub.dev/authorized');
  });

 

  var client = await completer.future;
 
  return client;
}

完整代碼:

Future<Client> _authorize() async {
  var grant = AuthorizationCodeGrant(
    _identifier, _authorizationEndpoint, tokenEndpoint,
    secret: _secret,
    // Google's OAuth2 API doesn't support basic auth.
    basicAuth: false,
    httpClient: _retryHttpClient,
  );

  // Spin up a one-shot HTTP server to receive the authorization code from the
  // Google OAuth2 server via redirect. This server will close itself as soon as
  // the code is received.
  var completer = Completer();
  var server = await bindServer('localhost', 0);
  shelf_io.serveRequests(server, (request) {
    if (request.url.path.isNotEmpty) {
      return shelf.Response.notFound('Invalid URI.');
    }

    log.message('Authorization received, processing...');
    var queryString = request.url.query;
    // Closing the server here is safe, since it will wait until the response
    // is sent to actually shut down.
    server.close();
    completer
        .complete(grant.handleAuthorizationResponse(queryToMap(queryString)));

    return shelf.Response.found('https://pub.dev/authorized');
  });

  var authUrl = grant.getAuthorizationUrl(
    Uri.parse('http://localhost:${server.port}'),
    scopes: _scopes,
  );

  log.message(
      'Pub needs your authorization to upload packages on your behalf.\n'
      'In a web browser, go to $authUrl\n'
      'Then click "Allow access".\n\n'
      'Waiting for your authorization...');

  var client = await completer.future;
  log.message('Successfully authorized.\n');
  return client;
}

可以看到,如果之前沒有在pub.dev創(chuàng)建過賬號(hào)的話覆致,這個(gè)過程是有授權(quán)登錄pub.dev上創(chuàng)建賬號(hào)的過程侄旬。

2.3 上傳https://pub.dev/

組件包的meta信息上傳到pub.dev

組件文件存儲(chǔ)實(shí)際是上傳到https://storage.googleapis.com,這個(gè)上傳地址是可以由pub.dev服務(wù)端控制的煌妈。

  • 創(chuàng)建一個(gè)包含官方 pub 服務(wù)器地址和測(cè)試用的本地服務(wù)器地址的集合。

  • 判斷當(dāng)前服務(wù)器地址是否屬于官方 pub 服務(wù)器地址集合中的地址宣羊,如果是璧诵,則檢查本地緩存中是否有對(duì)應(yīng)服務(wù)器地址的令牌,如果沒有仇冯,則使用 OAuth2 身份驗(yàn)證客戶端(oauth2.withClient)來進(jìn)行身份驗(yàn)證之宿,并調(diào)用 _publishUsingClient() 函數(shù)上傳壓縮包;如果有苛坚,則使用 Bearer 身份驗(yàn)證客戶端(withAuthenticatedClient)來進(jìn)行身份驗(yàn)證比被,并調(diào)用 _publishUsingClient() 函數(shù)上傳壓縮包。

  • 如果當(dāng)前服務(wù)器地址不屬于官方 pub 服務(wù)器地址集合中的地址泼舱,則使用 Bearer 身份驗(yàn)證客戶端來進(jìn)行身份驗(yàn)證等缀,并調(diào)用 _publishUsingClient() 函數(shù)上傳壓縮包。

● 如果上傳過程中出現(xiàn) PubHttpResponseException 異常娇昙,則獲取請(qǐng)求的 URL尺迂,判斷該 URL 是否與當(dāng)前服務(wù)器地址相同,如果相同冒掌,則調(diào)用 handleJsonError() 函數(shù)處理錯(cuò)誤噪裕;如果不同,則將異常拋出股毫。

  Future<void> _publish(List<int> packageBytes) async {
    try {
      final officialPubServers = {
        'https://pub.dev',
        // [validateAndNormalizeHostedUrl] normalizes https://pub.dartlang.org
        // to https://pub.dev, so we don't need to do allow that here.

        // Pub uses oauth2 credentials only for authenticating official pub
        // servers for security purposes (to not expose pub.dev access token to
        // 3rd party servers).
        // For testing publish command we're using mock servers hosted on
        // localhost address which is not a known pub server address. So we
        // explicitly have to define mock servers as official server to test
        // publish command with oauth2 credentials.
        if (runningFromTest &&
            Platform.environment.containsKey('_PUB_TEST_DEFAULT_HOSTED_URL'))
          Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'],
      };

      // Using OAuth2 authentication client for the official pub servers
      final isOfficialServer = officialPubServers.contains(host.toString());
      if (isOfficialServer && !cache.tokenStore.hasCredential(host)) {
        // Using OAuth2 authentication client for the official pub servers, when
        // we don't have an explicit token from [TokenStore] to use instead.
        //
        // This allows us to use `dart pub token add` to inject a token for use
        // with the official servers.
        await oauth2.withClient(cache, (client) {
          return _publishUsingClient(packageBytes, client);
        });
      } else {
        // For third party servers using bearer authentication client
        await withAuthenticatedClient(cache, host, (client) {
          return _publishUsingClient(packageBytes, client);
        });
      }
    } on PubHttpResponseException catch (error) {
      var url = error.response.request!.url;
      if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
        handleJsonError(error.response);
      } else {
        rethrow;
      }
    }
  }

其中publishUsingClient過程如下:

又分成3步交互:

  1. https://pub.dev/api/packages/versions/new上傳準(zhǔn)備獲取上傳目標(biāo)地址
  2. 上傳到 https://storage.googleapis.com
  3. https://pub.dev/api/packages/versions/newUploadFinis檢查包權(quán)限,有效性
  Future<void> _publishUsingClient(
    List<int> packageBytes,
    http.Client client,
  ) async {
    Uri? cloudStorageUrl;

    try {
      await log.progress('Uploading', () async {
        /// 1. Initiate upload
        final parametersResponse =
            await retryForHttp('initiating upload', () async {
          final request =
              http.Request('GET', host.resolve('api/packages/versions/new'));
          request.attachPubApiHeaders();
          request.attachMetadataHeaders();
          return await client.fetch(request);
        });
        final parameters = parseJsonResponse(parametersResponse);

        /// 2. Upload package
        var url = _expectField(parameters, 'url', parametersResponse);
        if (url is! String) invalidServerResponse(parametersResponse);
        cloudStorageUrl = Uri.parse(url);
        final uploadResponse =
            await retryForHttp('uploading package', () async {
          // TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
          // should report that error and exit.
          var request = http.MultipartRequest('POST', cloudStorageUrl!);

          var fields = _expectField(parameters, 'fields', parametersResponse);
          if (fields is! Map) invalidServerResponse(parametersResponse);
          fields.forEach((key, value) {
            if (value is! String) invalidServerResponse(parametersResponse);
            request.fields[key] = value;
          });

          request.followRedirects = false;
          request.files.add(
            http.MultipartFile.fromBytes(
              'file',
              packageBytes,
              filename: 'package.tar.gz',
            ),
          );
          return await client.fetch(request);
        });

        /// 3. Finalize publish
        var location = uploadResponse.headers['location'];
        if (location == null) throw PubHttpResponseException(uploadResponse);
        final finalizeResponse =
            await retryForHttp('finalizing publish', () async {
          final request = http.Request('GET', Uri.parse(location));
          request.attachPubApiHeaders();
          request.attachMetadataHeaders();
          return await client.fetch(request);
        });
        handleJsonSuccess(finalizeResponse);
      });
    } on AuthenticationException catch (error) {
      var msg = '';
      if (error.statusCode == 401) {
        msg += '$host package repository requested authentication!\n'
            'You can provide credentials using:\n'
            '    $topLevelProgram pub token add $host\n';
      }
      if (error.statusCode == 403) {
        msg += 'Insufficient permissions to the resource at the $host '
            'package repository.\nYou can modify credentials using:\n'
            '    $topLevelProgram pub token add $host\n';
      }
      if (error.serverMessage != null) {
        msg += '\n${error.serverMessage!}\n';
      }
      dataError(msg + log.red('Authentication failed!'));
    } on PubHttpResponseException catch (error) {
      var url = error.response.request!.url;
      if (url == cloudStorageUrl) {
        // TODO(nweiz): the response may have XML-formatted information about
        // the error. Try to parse that out once we have an easily-accessible
        // XML parser.
        fail(log.red('Failed to upload the package.'));
      } else if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
        handleJsonError(error.response);
      } else {
        rethrow;
      }
    }
  }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末膳音,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子铃诬,更是在濱河造成了極大的恐慌祭陷,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,865評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氧急,死亡現(xiàn)場(chǎng)離奇詭異颗胡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)吩坝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門毒姨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钉寝,你說我怎么就攤上這事弧呐≌⒚裕” “怎么了?”我有些...
    開封第一講書人閱讀 169,631評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵俘枫,是天一觀的道長(zhǎng)腥沽。 經(jīng)常有香客問我,道長(zhǎng)鸠蚪,這世上最難降的妖魔是什么今阳? 我笑而不...
    開封第一講書人閱讀 60,199評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮茅信,結(jié)果婚禮上盾舌,老公的妹妹穿的比我還像新娘。我一直安慰自己蘸鲸,他們只是感情好妖谴,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,196評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酌摇,像睡著了一般膝舅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上窑多,一...
    開封第一講書人閱讀 52,793評(píng)論 1 314
  • 那天仍稀,我揣著相機(jī)與錄音,去河邊找鬼怯伊。 笑死琳轿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的耿芹。 我是一名探鬼主播崭篡,決...
    沈念sama閱讀 41,221評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼吧秕!你這毒婦竟也來了琉闪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,174評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤砸彬,失蹤者是張志新(化名)和其女友劉穎颠毙,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體砂碉,經(jīng)...
    沈念sama閱讀 46,699評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛀蜜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,770評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了增蹭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滴某。...
    茶點(diǎn)故事閱讀 40,918評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出霎奢,到底是詐尸還是另有隱情户誓,我是刑警寧澤,帶...
    沈念sama閱讀 36,573評(píng)論 5 351
  • 正文 年R本政府宣布幕侠,位于F島的核電站帝美,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏晤硕。R本人自食惡果不足惜悼潭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,255評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舞箍。 院中可真熱鬧女责,春花似錦、人聲如沸创译。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽软族。三九已至,卻和暖如春残制,著一層夾襖步出監(jiān)牢的瞬間立砸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評(píng)論 1 274
  • 我被黑心中介騙來泰國打工初茶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留颗祝,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,364評(píng)論 3 379
  • 正文 我出身青樓恼布,卻偏偏與公主長(zhǎng)得像螺戳,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子折汞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,926評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容