Hacker mind ‘s blog

Happiness is love what you do

Hacking Pinterest Android App

| Comments

Hacking pinterest

This article show a way to get private key for Pinterest android app

This article is inspired from this one http://blog.will3942.com/reverse-engineering-instagram

This method was tested on Pinterest app version 2.4.2 and was done from MacOSX

Preparation

  • Download Android SDK from http://developer.android.com/sdk/index.html

    Unzip the file we just downloaded. We will see 2 folder: eclipse, sdk

    In sdk folders, there are build-tools, extras, platform-tools, system-images, tools

    Modify $PATH environment variable

1
$> echo "export PATH=$PATH:/DEVELOPMENT/ANDROID/sdk/tools/:/DEVELOPMENT/ANDROID/sdk/platform-tools/:/DEVELOPMENT/ANDROID/sdk/build-tools/android-4.4.2/" >> ~/.bash_profile
  • Download apktools.

    In this article, we tested with Mac OSX 10.8, so we use apktool.1.5.2

  • Set up new android virtual device

1
  $> android avd

android-create-image1 android-create-image2

Start new virtual device we just created

android-virtual-device

Hacking

  • Using apktool to decompile pinterest.2.4.2.apk to smali
1
  $> java -jar /DEVELOPMENT/ANDROID/apktool1.5.2/apktool.jar d pinterest.2.4.2.apk

pinterest-smali

All the code will located in smali folder

The code to get private key is located in smali/com/pinterest/base/Application.smali

  • There are 2 function we will need to hack

    The first one is getClientID at smali/com/pinterest/base/Application.smali:78

1
2
3
4
5
6
7
8
.method public static final getClientID()Ljava/lang/String;
    .locals 1

    .prologue
    .line 268
    sget-object v0, Lcom/pinterest/api/a;->d:Ljava/lang/String;
    return-object v0
.end method

We modify it to

1
2
3
4
5
6
7
8
9
10
11
.method public static final getClientID()Ljava/lang/String;
    .locals 2

    .prologue
    .line 268
    sget-object v0, Lcom/pinterest/api/a;->d:Ljava/lang/String;
    const-string v1, "LOGGING: CLIENT ID"
    invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    return-object v0
.end method

NOTE: because we use one more variable v1, so we will need to modify .locals 1 to .locals 2

The second function is getClientSecret at smali/com/pinterest/base/Application.smali:89

We also add a logging to print out the value. Modify .locals 6 to .locals 7 and modify end of function like this

1
2
3
4
5
6
7
8
9
10
.line 305
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

move-result-object v0

const-string v6, "LOGGING: CLIENT SECRET"
invoke-static {v6, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

goto :goto_0
.end method
  • Using apktool to recompile this code
1
  $> java -jar /DEVELOPMENT/ANDROID/apktool1.5.2/apktool.jar b pinterest.2.4.2

The new apk file is located at pinterest.2.4.2/dist/pinterest.2.4.2

  • Using keytool to generate an key
1
2
  $> cd pinterest.2.4.2/dist
  $> keytool -genkey -v -keystore android.keystore -alias android -keyalg RSA -keysize 2048 -validity 10000

NOTE, we set keystore password and key password is android (https://developer.android.com/tools/publishing/app-signing.html#setup). I am not sure this one is required

  • Sign new apk app
1
  $> jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore android.keystore pinterest.2.4.2.apk android
  • Verify apk
1
  $> zipalign -f -v 4 pinterest.2.4.2.apk pinterest.apk
  • Install apk app on emulator
1
  $> adb install pinterest.apk
  • Check log of emulator
1
  $> adb logcat | grep LOGGING
  • Now in emulator, we only need to start pinterest app

    The client ID and client secret will be show in log like this

1
2
D/LOGGING: CLIENT ID( 1549): 1431602
D/LOGGING: CLIENT SECRET( 1549): 492124fd20e80e0f678f7a03344875f9b6234e2b

Get pinterest algorithm to generate signature

After get client id and client secret, I want to know about how Pinterest generate their signature

  • Firstly, find out where they code signature generation function
1
2
  $> cd pinterest.2.4.2/smali/com/pinterest
  $> grep -r "oauth_signature" .

The file is api/a/i.smali

  • Let look at how pinterest implement there algorithm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
.method private static a(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/String;
    .locals 9
    .parameter
    .parameter
    .parameter

    .prologue
    const/4 v8, 0x1

    const/4 v7, 0x0

    .line 315
    .line 317
    :try_start_0
    const-string v0, "\\?"

    invoke-virtual {p1, v0}, Ljava/lang/String;->split(Ljava/lang/String;)[Ljava/lang/String;

    move-result-object v0

    const/4 v1, 0x0

    aget-object v0, v0, v1

    const-string v1, "UTF-8"

    invoke-static {v0, v1}, Ljava/net/URLEncoder;->encode(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    :try_end_0
    .catch Ljava/io/UnsupportedEncodingException; {:try_start_0 .. :try_end_0} :catch_1

    move-result-object v0

    .line 322
    :goto_0
    new-instance v1, Ljava/lang/StringBuilder;

    invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V

    .line 323
    invoke-virtual {v1, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v2

    const-string v3, "&"

    invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    .line 324
    invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    const-string v2, "&"

    invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    .line 325
    invoke-interface {p2}, Ljava/util/Map;->keySet()Ljava/util/Set;

    move-result-object v0

    invoke-interface {v0}, Ljava/util/Set;->iterator()Ljava/util/Iterator;

    move-result-object v2

    :cond_0
    :goto_1
    invoke-interface {v2}, Ljava/util/Iterator;->hasNext()Z

    move-result v0

    if-eqz v0, :cond_2

    invoke-interface {v2}, Ljava/util/Iterator;->next()Ljava/lang/Object;

    move-result-object v3

    .line 327
    :try_start_1
    invoke-interface {p2, v3}, Ljava/util/Map;->get(Ljava/lang/Object;)Ljava/lang/Object;

    move-result-object v0

    .line 328
    instance-of v4, v0, Ljava/util/List;

    if-eqz v4, :cond_1

    .line 329
    check-cast v0, Ljava/util/List;

    .line 330
    invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator;

    move-result-object v4

    :goto_2
    invoke-interface {v4}, Ljava/util/Iterator;->hasNext()Z

    move-result v0

    if-eqz v0, :cond_0

    invoke-interface {v4}, Ljava/util/Iterator;->next()Ljava/lang/Object;

    move-result-object v0

    check-cast v0, Ljava/lang/String;

    .line 331
    invoke-virtual {v1, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder;

    move-result-object v5

    const-string v6, "="

    invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v5

    invoke-static {v0}, Lcom/pinterest/api/a/i;->c(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v0

    invoke-virtual {v5, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    const-string v5, "&"

    invoke-virtual {v0, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    goto :goto_2

    .line 338
    :catch_0
    move-exception v0

    goto :goto_1

    :catch_1
    move-exception v0

    move-object v0, p1

    goto :goto_0

    .line 334
    :cond_1
    invoke-virtual {v1, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder;

    move-result-object v3

    const-string v4, "="

    invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v3

    check-cast v0, Ljava/lang/String;

    invoke-static {v0}, Lcom/pinterest/api/a/i;->c(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v0

    invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    const-string v3, "&"

    invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    :try_end_1
    .catch Ljava/io/UnsupportedEncodingException; {:try_start_1 .. :try_end_1} :catch_0

    goto :goto_1

    .line 342
    :cond_2
    invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v0

    invoke-virtual {v1}, Ljava/lang/StringBuilder;->length()I

    move-result v1

    add-int/lit8 v1, v1, -0x1

    invoke-virtual {v0, v7, v1}, Ljava/lang/String;->substring(II)Ljava/lang/String;

    move-result-object v1

    .line 343
    const-string v0, ""

    .line 346
    :try_start_2
    new-instance v2, Ljavax/crypto/spec/SecretKeySpec;

    sget-object v3, Lcom/pinterest/api/a;->f:Ljava/lang/String;

    const-string v4, "UTF-8"

    invoke-virtual {v3, v4}, Ljava/lang/String;->getBytes(Ljava/lang/String;)[B

    move-result-object v3

    const-string v4, "HMACSHA256"

    invoke-direct {v2, v3, v4}, Ljavax/crypto/spec/SecretKeySpec;-><init>([BLjava/lang/String;)V

    .line 348
    const-string v3, "HMACSHA256"

    invoke-static {v3}, Ljavax/crypto/Mac;->getInstance(Ljava/lang/String;)Ljavax/crypto/Mac;

    move-result-object v3

    .line 349
    invoke-virtual {v3, v2}, Ljavax/crypto/Mac;->init(Ljava/security/Key;)V

    .line 350
    const-string v4, "LOGGING: MESSAGE"
    invoke-static {v4, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    const-string v2, "UTF-8"

    invoke-virtual {v1, v2}, Ljava/lang/String;->getBytes(Ljava/lang/String;)[B

    move-result-object v1

    invoke-virtual {v3, v1}, Ljavax/crypto/Mac;->doFinal([B)[B

    move-result-object v1

    .line 351
    new-instance v2, Ljava/lang/String;

    invoke-static {v1}, Lorg/apache/commons/codec/binary/Hex;->encodeHex([B)[C

    move-result-object v1

    invoke-direct {v2, v1}, Ljava/lang/String;-><init>([C)V

    const-string v1, " "

    const-string v3, ""

    invoke-virtual {v2, v1, v3}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;

    move-result-object v1

    const-string v2, "<"

    const-string v3, ""

    invoke-virtual {v1, v2, v3}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;

    move-result-object v1

    const-string v2, ">"

    const-string v3, ""

    invoke-virtual {v1, v2, v3}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
    :try_end_2
    .catch Ljava/lang/Exception; {:try_start_2 .. :try_end_2} :catch_2

    move-result-object v0

    .line 357
    :goto_3
    const-string v1, "oauth_signature=%s"

    new-array v2, v8, [Ljava/lang/Object;

    aput-object v0, v2, v7

    invoke-static {v1, v2}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;

    move-result-object v0

    .line 358
    const-string v1, "%s&%s"

    const/4 v2, 0x2

    new-array v2, v2, [Ljava/lang/Object;

    aput-object p1, v2, v7

    aput-object v0, v2, v8

    invoke-static {v1, v2}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;

    move-result-object v0

    return-object v0

    .line 353
    :catch_2
    move-exception v1

    invoke-virtual {v1}, Ljava/lang/Exception;->printStackTrace()V

    goto :goto_3
.end method

From this code, I can only guess they use HMACSHA256 for generate their signature.

They will use CLIENT_SECRET as key for the sha algorithm. We only need to know what they use as message

Search .line 350, this is the message will be pass to sha1 algorithm. Again, we add these two line

1
2
const-string v4, "LOGGING: MESSAGE"
invoke-static {v4, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

This will print for us the format of message Pinterest use.

Now run the pinterest app again, we will see the format of that message

1
  POST&https%3A%2F%2Fapi.pinterest.com%2Fv3%2Flogin%2F&client_id=1431602&password=k&timestamp=1395914520&username_or_email=trungkien2288%40gmail.com

Here is the code I use to simulate request in python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    import unittest
    import urllib
    import hashlib
    import hmac

    client_secret = '492124fd20e80e0f678f7a03344875f9b6234e2b'
    client_id = '1431602'


    def generate_signature(method, url, data):
        data['client_id'] = client_id
        sorted_keys = sorted(data.keys())
        message = '&'.join(["%s=%s" % (k, urllib.quote_plus(data[k])) for k in sorted_keys])
        message = "%s&%s&%s" % (method.upper(), urllib.quote_plus(url), message)
        signature = hmac.new(client_secret, message.encode('utf-8'), hashlib.sha256).hexdigest()
        return signature


    class TestCase(unittest.TestCase):

        def test_generate_signature_1(self):
            data = {
                'username_or_email': 'trungkien2288@gmail.com',
                'password': 'k',
                'timestamp': '1395914520'
            }
            url = 'https://api.pinterest.com/v3/login/'

            signature = generate_signature("POST", url, data)
            self.assertEquals(signature, '6ee35c775b5f92668530d9cc2b91d9380c4bf01f1b17ccfa73ecfd2867b7b562')

        def test_generate_signature_2(self):
            data = {
                'timestamp': '1395914294'
            }
            url = 'https://api.pinterest.com/v3/callback/post_install/'

            signature = generate_signature("GET", url, data)
            self.assertEquals(signature, '0783e2deb355326bb998876387445f2d20bafefc930762cb216e4bc6a2ed748e')

Apt-get Install Failed

| Comments

Today, when setting up my debian server, I get this error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[chef]:~$ sudo apt-get install -f
Reading package lists... Done
Building dependency tree
Reading state information... Done
Correcting dependencies... Done
The following extra packages will be installed:
  libgtk2.0-0
  Suggested packages:
    librsvg2-common gvfs
    The following NEW packages will be installed:
      libgtk2.0-0
      0 upgraded, 1 newly installed, 0 to remove and 235 not upgraded.
      90 not fully installed or removed.
      Need to get 0 B/2,224 kB of archives.
      After this operation, 6,230 kB of additional disk space will be used.
      Do you want to continue [Y/n]? Y
      (Reading database ... 37319 files and directories currently installed.)
    Unpacking libgtk2.0-0 (from .../libgtk2.0-0_2.24.18-1_amd64.deb) ...
    dpkg: error processing /var/cache/apt/archives/libgtk2.0-0_2.24.18-1_amd64.deb (--unpack):
     triggers ci file contains unknown directive `interest-noawait'
     configured to not write apport reports
Errors were encountered while processing:
/var/cache/apt/archives/libgtk2.0-0_2.24.18-1_amd64.deb
E: Sub-process /usr/bin/dpkg returned an error code (1)

This error happened because of failure install of some packages. It make we cannot use apt-get to install any packages.

To solve it, firstly we need to find which packages are fail to install

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
[chef]:-$ sudo dpkg --configure -a
Setting up libpth20 (2.0.7-16) ...
Setting up libxdmcp6 (1:1.1.1-1) ...
Setting up libdbus-1-3 (1.6.12-1) ...
Setting up libjs-jquery (1.7.2+dfsg-2) ...
Setting up libtasn1-3 (2.14-3) ...
Setting up python-zope.interface (4.0.5-1) ...
Setting up libgraphite2-3 (1.2.3-1) ...
Setting up libgpg-error0 (1.10-3.1) ...
Setting up hicolor-icon-theme (0.12-1) ...
Setting up libglib2.0-data (2.36.1-2build1) ...
Setting up libgssglue1 (0.4-2) ...
Installing new version of config file /etc/gssapi_mech.conf ...
Setting up python-configobj (4.7.2+ds-5) ...
Setting up libthai-data (0.1.19-2) ...
Setting up python-lazr.uri (1.0.3-1) ...
Setting up fonts-dejavu-core (2.33+svn2514-3) ...
Setting up python-keyring (0.9.3-1) ...
Setting up libjbig0 (2.0-2) ...
Setting up krb5-locales (1.10.1+dfsg-6.1) ...
dpkg: dependency problems prevent configuration of pinentry-gtk2:
 pinentry-gtk2 depends on libgtk2.0-0 (>= 2.10.0); however:
  Package libgtk2.0-0 is not installed.
dpkg: error processing pinentry-gtk2 (--configure):
 dependency problems - leaving unconfigured
Setting up libgtk2.0-common (2.24.18-1) ...
Setting up libtiff4 (3.9.7-1) ...
Setting up libsystemd-login0 (44-12) ...
Setting up libassuan0 (2.1.0-1) ...
Setting up libkrb5support0 (1.10.1+dfsg-6.1) ...
Setting up python-bzrlib (2.6.0~bzr6574-1) ...
Setting up libgdk-pixbuf2.0-common (2.28.2-1) ...
Setting up libksba8 (1.3.0-2) ...
Setting up libdatrie1 (0.2.6-2) ...
Setting up libavahi-common-data (0.6.31-2) ...
Setting up libjasper1 (1.900.1-14) ...
Setting up libavahi-common3 (0.6.31-2) ...
Setting up python-simplejson (2.6.2-1) ...
Setting up python-crypto (2.6-5) ...
Setting up ttf-dejavu-core (2.33+svn2514-3) ...
Setting up python-wadllib (1.3.2-2) ...
Setting up libavahi-client3 (0.6.31-2) ...
Setting up fontconfig-config (2.10.2-2) ...
Setting up python-oauth (1.0.1-3) ...
Setting up libp11-kit0 (0.18.3-2) ...
dpkg: dependency problems prevent configuration of libgtk2.0-bin:
 libgtk2.0-bin depends on libgtk2.0-0 (= 2.24.18-1); however:
  Package libgtk2.0-0 is not installed.
dpkg: error processing libgtk2.0-bin (--configure):
 dependency problems - leaving unconfigured
Setting up libatk1.0-data (2.8.0-2) ...
Setting up libx11-data (2:1.6.0-1) ...
Setting up python-httplib2 (0.7.4-2) ...
Setting up libpixman-1-0 (0.26.0-4) ...
dpkg: dependency problems prevent configuration of gnupg-agent:
 gnupg-agent depends on pinentry-gtk2 | pinentry-curses | pinentry; however:
  Package pinentry-gtk2 is not configured yet.
  Package pinentry-curses is not installed.
  Package pinentry is not installed.
  Package pinentry-gtk2 which provides pinentry is not configured yet.
dpkg: error processing gnupg-agent (--configure):
 dependency problems - leaving unconfigured
Setting up python-lazr.restfulclient (0.13.3-1) ...
Setting up libglib2.0-0 (2.36.1-2build1) ...
No schema files found: doing nothing.
Setting up python-launchpadlib (1.9.12-2) ...
Setting up libxau6 (1:1.0.8-1) ...
Setting up wwwconfig-common (0.2.2) ...
Setting up shared-mime-info (1.0-1+b1) ...
Warning: program compiled against libxml 208 using older 207
Setting up libk5crypto3 (1.10.1+dfsg-6.1) ...
Setting up bzr (2.6.0~bzr6574-1) ...
Setting up libfontconfig1 (2.10.2-2) ...
Setting up dbus (1.6.12-1) ...
Starting system message bus: dbus.
Setting up libthai0 (0.1.19-2) ...
Setting up javascript-common (7) ...
Setting up libxcb1 (1.9.1-3) ...
dpkg: dependency problems prevent configuration of gnupg2:
 gnupg2 depends on gnupg-agent (= 2.0.20-1); however:
  Package gnupg-agent is not configured yet.
dpkg: error processing gnupg2 (--configure):
 dependency problems - leaving unconfigured
Setting up libxcb-render0 (1.9.1-3) ...
Setting up libharfbuzz0a (0.9.18-3) ...
Setting up python-paramiko (1.7.7.1-3.1) ...
Setting up libx11-6 (2:1.6.0-1) ...
Setting up libatk1.0-0 (2.8.0-2) ...
Setting up libgnutls26 (2.12.23-5) ...
Setting up libkrb5-3 (1.10.1+dfsg-6.1) ...
Setting up libxcb-shm0 (1.9.1-3) ...
Setting up fontconfig (2.10.2-2) ...
Regenerating fonts cache... done.
Setting up libgssapi-krb5-2 (1.10.1+dfsg-6.1) ...
Setting up libgdk-pixbuf2.0-0 (2.28.2-1) ...
Setting up libxext6 (2:1.3.1-2+deb7u1) ...
Setting up libxrender1 (1:0.9.7-1+deb7u1) ...
Setting up libpango-1.0-0 (1.32.5-5+b1) ...
Setting up libxrandr2 (2:1.3.2-2+deb7u1) ...
Setting up libxcomposite1 (1:0.4.4-1) ...
dpkg: dependency problems prevent configuration of libgpgme11:
 libgpgme11 depends on gnupg2 (>> 2.0.4); however:
  Package gnupg2 is not configured yet.
dpkg: error processing libgpgme11 (--configure):
 dependency problems - leaving unconfigured
Setting up libcups2 (1.5.3-5) ...
dpkg: dependency problems prevent configuration of python-gpgme:
 python-gpgme depends on libgpgme11 (>= 1.2.0); however:
  Package libgpgme11 is not configured yet.
dpkg: error processing python-gpgme (--configure):
 dependency problems - leaving unconfigured
Setting up libxi6 (2:1.6.1-1+deb7u1) ...
Setting up libxfixes3 (1:5.0-4+deb7u1) ...
Setting up libxdamage1 (1:1.1.4-1) ...
Setting up libxft2 (2.3.1-1) ...
Setting up libpangoft2-1.0-0 (1.32.5-5+b1) ...
Setting up libpangox-1.0-0 (0.0.2-4) ...
Setting up libxcursor1 (1:1.1.14-1) ...
Setting up libcairo2 (1.12.14-4) ...
Setting up libxinerama1 (2:1.1.2-1+deb7u1) ...
Setting up libpangocairo-1.0-0 (1.32.5-5+b1) ...
Setting up libpangoxft-1.0-0 (1.32.5-5+b1) ...
Setting up libpango1.0-0 (1.32.5-5+b1) ...
Errors were encountered while processing:
 pinentry-gtk2
 libgtk2.0-bin
 gnupg-agent
 gnupg2
 libgpgme11
 python-gpgme

All failed packaged was list in end of result. Now, we can just remove it

1
[chef]:-$ sudo apt-get remove --purge pinentry-gtk2 libgtk2.0-bin gnupg-agent gnupg2 libgpgme11 python-gpgme

Problem solved!!!

How to Deploy a Feature Branch to Heroku

| Comments

Why we need deploy a branch to heroku?

We often have many issues/feature which need to fix/enhance/add.

When developing an feature, we write code in local, the code is not finished. But we found out there is a critical bugs in production, and that bug need to fix immediately. We will need to deploy a hot fix the bug, but the hot fix should not contains the incomplete code we wrote for the new feature.

This case, we will need to create another branch in our git local repository. And we will want to deploy the hot fix branch to our heroku dyno?

How we deploy a branch to heroku

First, we need to create a branch for the hotfix, and checkout to the branch in our local

For example, our local git repository have 3 branches:

  • master
  • feature-A
  • hotfix-B (current branch)

We want to deploy hotfix-B to staging. We will use command

1
$> git push staging hotfix-B:master

And now, we will write code to fix the bug, testing on local, and push to staging (if needed) to test

When we sure that the bug is fixed, in local, we will merge branch hotfix-B to master, and move to branch feature-A to continue our work on new feature.

Giới Thiệu về Unix Process (Vietnamese)

| Comments

Là một kỹ sư lập trình hệ thống, một server guy, hay là một sys admin, sys dev, sys ops,… phần lớn thời gian bạn sẽ phải làm việc trên hệ thống Unix. Để làm việc trên Unix, chúng ta tương tác với hệ điều hành thông qua các lệnh (command). Mỗi lệnh trên Unix khi thực thi sẽ run một process hoặc một group các processes.

Trong bài viết này mình giới thiệu các kiến thức và kỹ thuật cơ bản để làm việc với Process trên Unix. Bài viết sẽ trình bày với code minh hoạ bằng Ruby (rồi bạn sẽ thấy Ruby rất đơn giản). Tất cả các code mình hoạ được chạy trên môi trường Unix (Linux của chính là Unix - nếu bạn chưa biết, vì thế đừng ngần ngại thử nó trên máy bạn).

Dù mình đã rất cố gắng, nhưng có thể vẫn có sai sót, mình rất cám ơn các ý kiến đóng góp

I. Một số kiến thức tổng quan

Tất cả các chương trình trong Unix thực chất đều là các processes. terminal bạn chạy, apache, nginx, vim, hay bất cứ lệnh nào bạn gõ vào terminal. Process chính là đơn vị cấu thành nên Unix. Nó chính là một instance của chương trình bạn viết ra. Nói cách khác mỗi dòng code của bạn, sẽ được thực thi trên một process.

Unix cung cấp tool ps để list ra tất cả các process đang chạy trên hệ thống

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$> ps -e -opid,ppid,user,rss,command
PID   PPID  USER     RSS      COMMAND
1     0     root     152      init [2]
1695  1     root     428      /usr/sbin/sshd
1863  1     root     48       /sbin/getty 38400 tty1
1864  1     root     48       /sbin/getty 38400 tty2
1865  1     root     48       /sbin/getty 38400 tty3
1866  1     root     48       /sbin/getty 38400 tty4
1867  1     root     48       /sbin/getty 38400 tty5
1868  1     root     48       /sbin/getty 38400 tty6
24477 1695  root     2888     sshd: vagrant [priv]
24479 24477 vagrant  1996     sshd: vagrant@pts/0
24480 24479 vagrant  2328     -bash
24591 24480 vagrant  1060     ps -e -opid,ppid,user,rss,command

Ở đây, mình chạy lênh ps và show ra các thuộc tính pid,ppid,user,rss,command của process (chú ý (1) ps có rất nhiều option để chạy, nếu bạn muốn hiểu chỉ tiết, hãy sử dụng man ps để biết, (2) kểt quả trả về chỉ là một phần các process trên máy mình). Các thông tin mình muốn hiện thị ở đây bao gồm:

  1. PID - Process ID (id của process),
  2. PPID - Parent Process ID (id process cha của process đó),
  3. USER (tên user trên Unix start process),
  4. RSS (Resident Set Size) có thể coi bộ nhớ mà process sử dụng,
  5. COMMAND - command mà user sử dụng để chạy processs

Chú ý rằng dòng cuối trong kết quả trả về show ra COMMAND là ps -e -opid,ppid,user,rss,command - chính là lệnh mà chúng ta dùng để chạy. Điều đó chứng tỏ, mỗi một command chính là một process !?

Ngoài ra lệnh ps cũng cho chúng ta thấy, mỗi một Process sẽ có một Process ID, và thuộc về một Process cha nào đó. Process ID là duy nhất đối với mỗi một process, tức là 2 process khác nhau chắc chắn phải có PID khác nhau. Ngoài ra Process ID là không thể thay đổi trong khi chạy process.

1. Làm sao hệ điều hành đánh số các Process ID?

Process ID được đánh số theo thứ tự tăng dần. Bắt đầu từ 0 và tăng lên cho tới khi gặp giá trị maximum. Giá trị maximum của Process ID là có thể cấu hình được tuỳ vào từng hệ thống.

Trên Linux bạn có thể xem và thay đổi giá trị mặc định của Process ID maximum bằng cách thay đổi file /proc/sys/kernel/pid_max

1
2
3
4
5
6
# read current maximum value of process id
$> cat /proc/sys/kernel/pid_max
32768

# set maximum value for process id
$> echo 40000 > /proc/sys/kernel/pid_max

Khi process ID tăng đến giá trị maximum value, hệ điều hành (OS) sẽ quay trở lại đánh số từ một giá trị cụ thế (một số tài liệu nói giá trị này với Linux là 300, và với Mac OS là 100 - mình chưa biết cách để kiểm nghiệm điều này một cách an toàn)

UNIX cung cấp syscall getpid trả về Process ID của process hiện tại. Bạn có thể viết một chương trình C đơn gian để lấy ra process id với getpid. Tuy nhiên, bài viết này của tôi sẽ tập trung vào ngôn ngữ Ruby

Trong Ruby, muốn lấy Process ID của process hiện tai, bạn sử dụng Process.pid.

1
2
# file test_pid.rb
puts "Process pid: #{Process.pid}"

Dòng code trên gọi tới hàm puts - hàm này có tác dụng in một String ra màn hình. Chúng ta có thể manipulate các String trong Ruby thông qua các syntax #{}. Code ruby trong #{ } sẽ được thực hiện trước khi truyền cho String

1
2
3
4
5
$> irb

irb(main):001:0> puts "Example for String manipulate: 1 + 2 = #{1 + 2}"
Example for String manipulate: 1 + 2 = 3
=> nil

(Các file Ruby có extension là .rb. Để chạy một file ruby, bạn dùng lệnh ruby <file_name>. Không cần phải compile, rất đơn giản phải không)

2. Liệu có phải process nào cũng có Process cha?

Ở trên tôi đã nói rằng, mỗi một process đều thuộc về một Process cha nào đó. Nếu bạn suy nghĩ kỹ, bạn sẽ thấy có điều gì đó không ổn? À, thực ra điều này liên quan đến quá trình khởi động của UNIX. Khi UNIX được khởi động, nó sẽ start một process số 0 (với PID = 0) (process này là process của Kernel UNIX). Process 0 sẽ tạo ra cho nó một Process con, Process 1. Trong phần lớn hệ thống, Process 1 được đặt tên là init process, các process khác được tạo ra đều từ init process.

Hãy quay lại ví dụ về lệnh ps như ở phần đầu mục I. Bạn có thể để ý thấy PPID của dòng đầu tiên là 0. Đó chính là process đầu tiên của OS.

Vậy là process trong Unix thực chất được tổ chức dưới dạng cây. Mỗi một node trong cây đại diện cho một process trong Unix. Gốc chính là process 0, các con của một node chính là các process con của process ứng với node đó.

Trong Ruby, để lấy ra parent process id của một process, chúng ta sử dung Process.ppid

1
2
# file test_ppid.rb
puts "Process id #{Process.pid}, parent process id #{Process.ppid}"

Cũng rõ ràng đấy chứ. Liệu tôi có quên gì nữa không nhỉ?

Vấn đề là làm sao một process có thể sinh ra một process con? À đừng lo, tôi sẽ nói kỹ về điều này ở phần 2

3. Process Resource

Ngoài ra lệnh ps của chúng ta còn cho thấy, mỗi Process đều có RSS khác nhau. RSS chính là bộ nhớ mà Process sử dụng. Các process khác nhau, có bộ nhớ khác nhau. Nói cách khác, không gian địa chỉ của các Process là riêng biệt. Nhớ thiết kế này mà các Process là độc lập với nhau. Nếu một Process bị chết, thì nó cũng không ảnh hưởng gì tới các Process khác.

Ngoài bộ nhớ, hệ điều hành còn cấp phát cho Process các tài nguyên khác đó là các file descriptor. Nhớ rằng trên UNIX, mọi thứ đều là file. Điều đó có nghĩa là, devide được coi như file, socket được coi như file, pipe cũng là file, và file cũng là file!!! Để cho đơn giản, chúng ta sẽ dùng Resource thay cho khái niệm file nói chung, và file đại diện cho khái niệm file thông thường.

Bất cứ khi nào bạn mở một Resource trong một process, resource đó sẽ được gán với một số file descriptor. File descriptor là không được chia sẽ giữa các process không liên quan. Các resource sẽ sống và tồn tại cùng với process mà nó thuộc về. Khi process chết đi, các resource gắn với nó sẽ được close và exit.

Mỗi một process sẽ có 3 files descriptor mặc định, bạn hẳn rất quen thuộc với chúng, đó chính là stdin, stdout và stderr. Các file descriptor được đánh số tăng dần từ 0 đến giá trị lớn nhất. Mỗi một process sẽ có một số giới hạn các file descriptor nó được quyền sử dụng.

II. forking

Ở phần I.2, chúng ta đã nói về process cha và process con, và đưa ra câu hỏi, làm sao một process có thể sinh ra các process khác.

UNIX cung cấp một công cụ tuyệt vời để làm điều đó. Bạn chắc đã đoán ra, đó chính là fork. Theo cá nhân tôi, fork có lẽ là một trong những chức năng tốt nhất của UNIX. Vì sao ư? Vì process con được tạo ra với fork có 2 đặc điểm:

  • process con được copy tất cả các memory từ process cha.
  • process con sẽ được kế thứa từ process cha các resource

Điều này có nghĩa là nếu trong process cha, bạn đã định nghĩa biến a, và gán giá trị cho nó, process con cũng có thể sử dụng biến đó.

Uhm… Không phải như thế sẽ dẫn đến tình trạng 2 process cùng thay đổi một biến hay sao, vả lại chẳng phải các process là độc lập về bộ nhớ.

À, tức là thế này, khi fork một process mới, bộ nhớ của process con và process cha vẫn là độc lập, nhưng hệ điều hành sẽ sử dụng cơ chế copy-on-wright (COW) để thực hiện việc đó. Tức là nếu process con không thay đổi các giá trị trong process cha, process con và process cha sẽ vẫn dùng chung bộ nhớ. Điều này làm cho các process con chỉ đọc, sẽ có memory rất nhỏ. Hay nói cách khác, UNIX cung cấp cho chúng ta một công cụ để chạy các multiprogram với một lượng resource vửa đủ.

Điều này đặc biết tốt khi bạn cần load các library. Process cha sẽ đảm nhiệm việc load các library khác nhau. Sau khi load xong, nó fork ra các process con, và thực hiện việc điều khiển các process con. Các process con nhờ cơ chế COW, không cần phải tốn thời gian load library nữa mà vẫn có thể truy xuất vào các library

Ngoài ra các process cha chia sẻ với process con các resource cũng dẫn đến một kỹ thuật khá thú vị: pre-forking - đặc biệt hiệu quả trong việc lập trình server.

Kỹ thuật này được mô tả như sau:

  • Main process khởi tạo một listening socket
  • Main process fork ra một list các children process. Chú ý các children process này cũng sẽ listen trên socket mà main process tạo ra. Nhưng việc dispatch các incomming connection tới các children process là được thực hiện trên kernel. Điều này làm cho việc dispatch các incomming connection là rất nhanh
  • Mỗi process sẽ accept các connection từ shared socket và xử lý chúng riêng biệt
  • Main process sẽ kiểm soát các children process. (cung cấp lệnh để tắt tất cả các children process, tạo một child process mới khi một child process bị crash…)

Kỹ thuật pre-forking được sử dụng rất nhiều. ví dụ: apache (httpd), nginx, celery, postgresql, rabbitmq, ….

Process trong Unix là một lĩnh vực rất thú vị, đặc biệt là trong lập trình hệ thống và lập trình server. Bài viết chỉ mới đề cập đến một vài kiến thức và kỹ thuật ban đầu với Process. Còn rất nhiều vấn đề chưa đề cập, như

  • Tương tác giữa các process (IPC)
  • Điều khiển các process
  • Orphaned, daemon, zoombie, process …

Hy vọng trong tương lai, mình sẽ có thể viết về các vấn đề này kỹ hơn.

Update

Bản slide tôi trình bày tại công ty Framgia về UNIX Process

Running Celery Task Synchronous When Testing Django App

| Comments

Celery support running task synchronous by config variable CELERY_ALWAYS_EAGER. But where to put this variable? and could I can select run celery task synchronous with some testcase?

I do some googling but did not find any good result. So I post this on my blog to help someone like me.

To set this variable in a testcase, just modify default django config variable at beginning of test case

1
2
3
# run celery task synchronous
from django.conf import settings
settings.CELERY_ALWAYS_EAGER = True

You can put this code in any testcase you want or in setUp function of a testcase.

Django Show Sql Query in Shell

| Comments

To show sql query in `python manage.py shell`, we use this code
1
2
3
4
import logging
l = logging.getLogger('django.db.backends')
l.setLevel(logging.DEBUG)
l.addHandler(logging.StreamHandler())

Authentication Django-tastypie in Right Way

| Comments

When using django-tastypie, I got some problems with security. In my pinterest-clone application, I build a pin model. A pin is a image with a description and tags for it`

I want to build API for that model with some constrains:

  • Everyone can see pins of other people
  • Only author of pin can delete/modify his owner pin, other people are not allow to do this action

So the API should be

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1. Get list pin

* HTTP Method: GET
* URL endpoint: http://<domain>/api/pin/
* Return:
    [
      objects : [{
        <pin infomation>
      }]
    ]

2. Delete a pin
* HTTP Method: DELETE
* URL endpoint: http://<domain>/api/pin/<pin_id>/
* Return:
    + { "error": "Pin not found" } if dont have <pin_id> in database
    + { "error": "Authorization error" } if user request this API is not owner of <pin_id>
    + Nothing if delete pin successfully

3. Modify a pin
* HTTP Method: DELETE
* URL endpoint: http://<domain>/api/pin/<pin_id>/
* POST params:
    {
        "description": <string>
        "tags": List of string
    }
* Return:
    + { "error": "Pin not found" } if dont have <pin_id> in database
    + { "error": "Authorization error" } if user request this API is not owner of <pin_id>
    + Nothing if modify pins successful

To solve security issue with django-tastypie, I subclass tastypie.authorization.Authorization class. And modify the method apply_limits in this custom class

Here is the source code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from tastypie.resources import ModelResource
from tastypie.exceptions import BadRequest
from tastypie.serializers import Serializer
from tastypie.authorization import Authorization

from django.contrib.auth.models import User

from pinry.pins.models import Pin
from pinry.pins.models import Like
from pinry.pins.models import Comment
from pinry.core.models import Member

class PinAuthorization(Authorization):
    def is_authorized(self, request, object=None):
        # only logged in user will can modify pins
        if request.method in ("DELETE", "PUT"):
            if not request.user.is_authenticated():
                raise BadRequest(json.dumps({"error": "Authorization error"}))
        return True

    def apply_limits(self, request, object_list=None):
        # only allow delete/modify pin belong to this user
        if request.method in ("DELETE", "PUT"):
            filter_list = object_list.filter(submitter=request.user.get_profile())
            if not filter_list:
                raise BadRequest(json.dumps({"error": "Authorization error"}))
            return filter_list

        return object_list


class PinResource(ModelResource):
    class Meta:
        queryset = Pin.objects.all()
        resource_name = 'pin'
        list_allowed_method = ["GET"]
        details_allowd_method = ["GET", "PUT", "DELETE"]
        include_resource_uri = False
        authorization = PinAuthorization()
        serializer = Serializer(["json"])
        filtering = {
            'published': ['gt'],
        }

There are some note in my code:

  1. In PinResource class, i only accept GET method for list request and GET, PUT, DELETE for detail method
  2. I set default serializer is json format, so i dont need pass paramter format=json in every request
  3. To return error in response, i raise BadRequest exception in 2 functions is_authorized and apply_limits. Type of exception (BadRequest) is very important because tastypie only handle that exception when process your request.

Here is the code i got from lastest source code of tastypie on github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# file tastypie/resource class Resource
def wrap_view(self, view):
    """
    Wraps methods so they can be called in a more functional way as well
    as handling exceptions better.

    Note that if ``BadRequest`` or an exception with a ``response`` attr
    are seen, there is special handling to either present a message back
    to the user or return the response traveling with the exception.
    """
    @csrf_exempt
    def wrapper(request, *args, **kwargs):
        try:
            callback = getattr(self, view)
            response = callback(request, *args, **kwargs)

            # Our response can vary based on a number of factors, use
            # the cache class to determine what we should ``Vary`` on so
            # caches won't return the wrong (cached) version.
            varies = getattr(self._meta.cache, "varies", [])

            if varies:
                patch_vary_headers(response, varies)

            if self._meta.cache.cacheable(request, response):
                if self._meta.cache.cache_control():
                    # If the request is cacheable and we have a
                    # ``Cache-Control`` available then patch the header.
                    patch_cache_control(response, **self._meta.cache.cache_control())

            if request.is_ajax() and not response.has_header("Cache-Control"):
                # IE excessively caches XMLHttpRequests, so we're disabling
                # the browser cache here.
                # See http://www.enhanceie.com/ie/bugs.asp for details.
                patch_cache_control(response, no_cache=True)

            return response
        except (BadRequest, fields.ApiFieldError), e:
            return http.HttpBadRequest(e.args[0])
        except ValidationError, e:
            return http.HttpBadRequest(', '.join(e.messages))
        except Exception, e:
            if hasattr(e, 'response'):
                return e.response

            # A real, non-expected exception.
            # Handle the case where the full traceback is more helpful
            # than the serialized error.
            if settings.DEBUG and getattr(settings, 'TASTYPIE_FULL_DEBUG', False):
                raise

            # Re-raise the error to get a proper traceback when the error
            # happend during a test case
            if request.META.get('SERVER_NAME') == 'testserver':
                raise

            # Rather than re-raising, we're going to things similar to
            # what Django does. The difference is returning a serialized
            # error message.
            return self._handle_500(request, e)

    return wrapper

wrap_view is method was called when tastypie get request from client.

In this code, tastypie only return HttpBadRequest for BadRequest and ApiFieldError exception, other Exception will handle by 500 internal error request

Mysql Without Password

| Comments

When dealing with database, sometime we want to make backup database. I usually use mysqldump to store all database in sql file. The problem is when running, mysqldump ask us to provide password. If you must enter password everytime you run the command, you cannot make it run automatically with crontab script

So, how to solve it???

Well, mysqldump allow us can run automaticall with an configuration file was located at ~/.my.cnf. Format of this file is

1
2
3
4
[mysqldump]
host=<your_host_name>
username=<your_user_name>
password=<your_password>

And the file need to set in mod 600

Watch Directory Change With Python Watchdog Library

| Comments

Watchdog is Python API and shell utilities to monitor file system events.

Watchdog come with a tool call watchmedo to call shell command when we get change on a directiory. The “change” include: delete/modify/create a file in directory.

For example, i want to terminate a tornaldo server (which was run by supervise tool) everytime i change a python file. Here i what i make

I create a file /service/tornaldo-local/run

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env sh
TORNADO_PID=/var/run/tonardo.pid

export PYTHONPATH=$PYTHONPATH:/home/vagrant/current

. /home/vagrant/current/bin/envvars.local.sh

exec /usr/bin/python /usr/local/bin/watchmedo shell-command \
    --patterns="*.py;*.sh" \
    --recursive \
    --command="pgrep -f '/*manage.py' | xargs kill -9" \
    `ls -l /home/vagrant | grep current | awk '{print $11}'`/.. 2>&1 >/dev/null &
exec /usr/bin/python /home/vagrant/current/bin/manage.py start >> /var/log/tornado/tornado-local.log 2>&1